Skip to content

Process termination guardrail - #3

Open
collin-kierce wants to merge 41 commits into
mainfrom
process_termination_guardrail
Open

Process termination guardrail#3
collin-kierce wants to merge 41 commits into
mainfrom
process_termination_guardrail

Conversation

@collin-kierce

Copy link
Copy Markdown
Owner

No description provided.

TJ Webb and others added 30 commits July 23, 2026 15:15
Migrate 49 user-facing emit_* strings to the t() catalog; 49 → 0 raw sites.

Key groupings (48 new oauth.* keys):
  oauth.server.*      — callback server startup, ports, redirect-URI, paste hint
  oauth.pasteback.*   — paste-back parse/provider errors, no-code, no-state
  oauth.state_mismatch / oauth.callback.* — state check, callback error/timeout
  oauth.browser.*     — headless mode, opening, fallback URL, manual URL, open-failed
  oauth.auth.*        — token exchange, save, success, model discovery
  oauth.reauth.*      — refresh-failed, restored, no-token
  oauth.cmd.auth.*    — starting, overwrite-warning
  oauth.cmd.status.*  — authenticated, expires, models, no-models, not-authenticated, hint
  oauth.cmd.fast.*    — wrong-model, enabled, enabled-detail, disabled
  oauth.cmd.logout.*  — tokens-removed, models-removed, success
  oauth.model.*       — no-api-key

Tests: tests/i18n/test_claude_oauth_i18n.py (14 tests).
All i18n tests pass. ruff clean.
Two bugs fixed:

1. f-string false positives (_has_string_literal)
   Previously any ast.JoinedStr was unconditionally classified as 'raw',
   meaning pure-variable f-strings like f"{result}" or f"  {msg}" were
   counted as un-extracted literals even though they contain no translatable
   content. Fixed by walking the f-string's values list and only returning
   True when at least one ast.Constant part has non-whitespace text.
   Impact: 23 false positives reclassified as 'dynamic' (1302 -> 1279 raw).

2. Single-file path silently returning 0 sites (_iter_py_files)
   os.walk(file_path) on a regular file yields nothing, so
   'python -m code_puppy.i18n.audit path/to/module.py' always reported
   0 sites. Fixed by detecting a file path up front and yielding it
   directly if it ends in .py.

Tests added (5 new, 25 total):
  test_fstring_pure_variable_is_dynamic
  test_fstring_whitespace_only_literal_is_dynamic
  test_fstring_with_content_and_variable_is_raw
  test_fstring_with_only_arrow_is_raw
  test_single_file_path_is_accepted
  test_single_file_pure_variable_fstring_not_raw

All 25 audit tests pass. ruff clean.
Adds a 'dangerous_command_guard_allow' config key: a comma-separated list of pattern names (e.g. 'git reset --hard, --force') that bypass the destructive command guard AND the force push guard, while every other dangerous pattern stays protected.

Previously the only escape was 'disable_dangerous_command_guard', which is all-or-nothing and kills both guards at once. The allowlist is shared across both guards (keyed off the detector pattern_name, normalized for case/whitespace) and is fully additive: the legacy disable flag is untouched and still wins as a global kill-switch.

config.py: normalize_guard_pattern_name(), get_dangerous_command_guard_allowlist(), is_dangerous_command_allowlisted(); register key for /set autocomplete. Both guard callbacks wave through allowlisted patterns before prompting/blocking. Adds tests/plugins/test_command_guard_allowlist.py (14 tests); updates test_config key-set assertions.
Nested AGENTS.md so any agent (including Code Puppy itself) editing the destructive command guard auto-loads the rules: file map, callback precedence, the add-a-pattern recipe, the pattern_name-is-an-API-contract rule, false-positive discipline, and the shared allowlist. Scoped to the destructive command guard only (force-push guard noted as a sibling).
…OME)

Run `code-puppy` from your home directory and every launch warns about a
file it already loaded:

    Project MCP config '/home/you/.code_puppy/mcp_servers.json' is not
    trusted yet; its servers are NOT loaded. Review it, then run
    '/mcp trust' to accept

When CWD is $HOME, <CWD>/.code_puppy/mcp_servers.json and
~/.code_puppy/mcp_servers.json are the SAME PATH. get_project_mcp_servers_file()
returned that path as a project candidate, so the trust gate flagged the
user-level config as an untrusted stranger -- while load_mcp_server_configs()
had already merged its servers in via the user-level source. The warning is
false: the servers are loaded, and it says they are not.

That is worse than noise. It tells users their MCP tools are unavailable
when they are working fine, and the remedy it names is ceremony over a
decision the user already made by writing the file. It also fires on every
launch, since nothing was ever written to trusted_mcp.json.

Fix: get_project_mcp_servers_file() returns None when the candidate IS the
user-level config. There is no second, less-trusted source to gate, so
there is nothing to ask about. Compared with samefile() rather than path
equality so a symlink or bind-mount pointing at the user-level config is
recognized too.

One chokepoint by design: get_trust_status(), load_project_mcp_server_configs()
and `/mcp trust` all already handle "no project file" correctly, so a single
guard fixes the whole path without growing a second code path.

The real gate is untouched. A genuinely separate project config is still
withheld until trusted, asserted in both directions:

    assert cp_config.load_mcp_server_configs() == {"user_only": "u"}  # gated
    assert pc.trust_project_mcp() is True
    assert "proj_only" in cp_config.load_mcp_server_configs()         # honored

The check targets config.MCP_SERVERS_FILE, never $HOME, so it stays correct
when XDG_CONFIG_HOME relocates the user-level config out of ~/.code_puppy.

4 tests added; 3 fail without the change (14 -> 17 in that file).
- Rename `_supports_ultra_reasoning` to `_supports_max_reasoning` and update its docstring
- Replace `supports_ultra_reasoning` config key with `supports_max_reasoning` in model metadata
- Update `SETTING_DEFINITIONS` reasoning_effort choices from `["minimal", ..., "ultra"]` to `["none", ..., "max"]`
- Update capability-gating logic in `_get_setting_choices` to filter `max` instead of `ultra`
- Update docstring to reflect the renamed `max` effort level
… effort values

- Normalize legacy effort values at request time: `minimal` → `none`, `ultra` → `max`
- Ensures existing saved configs with old effort labels continue to work transparently
- Aliases are applied only for GPT-5 models before sending `openai_reasoning_effort`
…nfig keys

- Replace references to `ultra` with `max` and `minimal` with `none` in all test assertions
- Rename test methods `test_reasoning_effort_with_xhigh_but_without_ultra` → `…without_max` and `test_reasoning_effort_with_ultra` → `…with_max`
- Update model config fixture keys from `supports_ultra_reasoning` to `supports_max_reasoning`
- Remove redundant `supports_xhigh_reasoning` assertion for `gpt-5.5` (already covered elsewhere)
Adds a 'Custom Params' entry to the /model_settings TUI where free-form
key = value pairs can be added, edited, and deleted per model. Pairs are
stored as a JSON blob under the reserved model_settings_<model>_custom
config key, and merged into extra_body by make_model_settings.

- Dotted keys expand into nested dicts:
  chat_template_kwargs.thinking = medium ->
  {"chat_template_kwargs": {"thinking": "medium"}}
- Values parse as bool/int/float, falling back to string
- Applied last, after every model-specific branch, so custom values
  override built-in settings on conflict
- Corrupt/non-dict JSON fails closed to an empty mapping
- Reserved 'custom' key is excluded from the generic scalar settings
  namespace so it can never leak raw JSON into ModelSettings
/fork called _invoke_agent_impl with no session_id, so every fork
started with a blank context instead of branching the conversation
(unlike ACP's fork_session, which copies history properly).

- _seed_fork_session snapshots the current agent's live message
  history synchronously in _start_fork (point-in-time, safe mid-run:
  the pause gate parks the agent and the history processor only
  commits at model-request boundaries ending in a ModelRequest) and
  persists it via the existing sub-agent session machinery
- the seeded session_id flows into _invoke_agent_impl, which loads it
  as a continuing session; empty history or snapshot failure falls
  back to a fresh context with a warning
- tests: snapshot copy semantics, session id threading, fresh-context
  fallback, failure fallback; fix fake impl session_id kwarg mismatch

Also repair stale/leaky tests found running the full suite:
- model_settings_menu: Custom Params is now universally offered
  (d08b6c9), so 'no configurable settings' premises were unreachable;
  assert new behavior + keep defensive empty-state branches covered
- computer_use inline_image: scrub terminal-identity env vars so
  running the suite inside iTerm2/kitty can't leak into detection
…ot-be-its-own-project-twin

fix(mcp): a config can't be its own untrusted project twin (CWD == $HOME)
…-claude-oauth

feat(i18n): extract claude_code_oauth/register_callbacks.py strings
…command-guard-allowlist

feat(guards): granular per-pattern allowlist for command guards
…lse-positives

fix(i18n/audit): eliminate false positives in raw-site classification
@collin-kierce
collin-kierce force-pushed the process_termination_guardrail branch from 68e1068 to 25f9985 Compare August 3, 2026 19:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants