diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d32bfd81..90d7fb35 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -128,7 +128,7 @@ repos: - id: sandbox-env-reserved-parity name: reserved sandbox_env set stays in sync language: system - files: ^(generator/src/tend/config\.py|proxy/setup-sandbox\.sh)$ + files: ^(generator/src/tend/config\.py|proxy/setup-sandbox\.sh|docs/tend\.example\.yaml)$ entry: python3 args: - -c @@ -154,6 +154,17 @@ repos: heredoc_keys = set(re.findall(r'^([A-Za-z_][A-Za-z0-9_]*)=', heredoc, re.M)) heredoc_keys |= set(re.findall(r'ANTHROPIC_DUMMY="([A-Za-z_][A-Za-z0-9_]*)=', sh)) + # The adopter only meets this set as a rejection at `init`, so the + # `sandbox_env` docs have to name every member — a key reserved in + # code but absent from the docs reads as settable right up until + # the run fails. Scope the search to that bullet: file-wide, an + # unrelated mention elsewhere (the auth section names + # ANTHROPIC_API_KEY and CLAUDE_CODE_OAUTH_TOKEN) would stand in for + # the list entry and hide the drift this leg exists to catch. + docs = open("docs/tend.example.yaml").read() + section = re.search(r"^# - `sandbox_env`:(.*?)^# - `", docs, re.S | re.M).group(1) + documented = set(re.findall(r"`([A-Za-z_][A-Za-z0-9_]*)`", section)) + problems = [] for name in sorted(config_set - shell_set): problems.append(f"'{name}' is reserved in config.py but NOT guarded in setup-sandbox.sh") @@ -161,15 +172,18 @@ repos: problems.append(f"'{name}' is guarded in setup-sandbox.sh but NOT reserved in config.py") for name in sorted(heredoc_keys - config_set): problems.append(f"'{name}' is written to AGENT_ENV_FILE but NOT reserved in config.py — an adopter's sandbox_env could shadow it") + for name in sorted(config_set - documented): + problems.append(f"'{name}' is reserved in config.py but NOT named in docs/tend.example.yaml's sandbox_env list") for p in problems: print(p) if problems: print( "\nERROR: the reserved sandbox_env set drifted between " - "config.py, setup-sandbox.sh, and the AGENT_ENV_FILE " - "heredoc. The shell case is the security boundary; keep " - "every env-file key reserved and the two sets aligned." + "config.py, setup-sandbox.sh, the AGENT_ENV_FILE heredoc, " + "and docs/tend.example.yaml. The shell case is the security " + "boundary; keep every env-file key reserved, the two sets " + "aligned, and every reserved name documented." ) sys.exit(1) pass_filenames: false diff --git a/docs/tend.example.yaml b/docs/tend.example.yaml index c7a0b987..c934ded1 100644 --- a/docs/tend.example.yaml +++ b/docs/tend.example.yaml @@ -183,8 +183,19 @@ bot_name: my-project-bot # - ~/.cargo/bin # # - `sandbox_env`: NAME: VALUE pairs added to the agent's launch env. Reserved -# keys (proxy routing, CA trust, dummy credentials, PATH) are rejected — use -# `sandbox_path` for PATH: +# keys are rejected at `init`, and again inside the sandbox so a hand-edited +# workflow can't smuggle one past: +# +# - proxy routing — `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, `http_proxy`, +# `https_proxy`, `no_proxy` +# - CA trust — `NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE` +# - dummy credentials — `GH_TOKEN`, `GITHUB_TOKEN`, `ANTHROPIC_API_KEY`, +# `CLAUDE_CODE_OAUTH_TOKEN` +# - prompt suppression — `CLAUDE_CODE_REMOTE` +# - the sandbox's own locations — `HOME`, `PATH`, `XDG_CONFIG_HOME`, +# `XDG_CACHE_HOME`, `XDG_DATA_HOME`, `XDG_STATE_HOME` +# +# Use `sandbox_path` to extend PATH: # # sandbox_env: # RUST_BACKTRACE: "1" diff --git a/plugins/install-tend/skills/install-tend/references/tend.example.yaml b/plugins/install-tend/skills/install-tend/references/tend.example.yaml index c7a0b987..c934ded1 100644 --- a/plugins/install-tend/skills/install-tend/references/tend.example.yaml +++ b/plugins/install-tend/skills/install-tend/references/tend.example.yaml @@ -183,8 +183,19 @@ bot_name: my-project-bot # - ~/.cargo/bin # # - `sandbox_env`: NAME: VALUE pairs added to the agent's launch env. Reserved -# keys (proxy routing, CA trust, dummy credentials, PATH) are rejected — use -# `sandbox_path` for PATH: +# keys are rejected at `init`, and again inside the sandbox so a hand-edited +# workflow can't smuggle one past: +# +# - proxy routing — `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, `http_proxy`, +# `https_proxy`, `no_proxy` +# - CA trust — `NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE` +# - dummy credentials — `GH_TOKEN`, `GITHUB_TOKEN`, `ANTHROPIC_API_KEY`, +# `CLAUDE_CODE_OAUTH_TOKEN` +# - prompt suppression — `CLAUDE_CODE_REMOTE` +# - the sandbox's own locations — `HOME`, `PATH`, `XDG_CONFIG_HOME`, +# `XDG_CACHE_HOME`, `XDG_DATA_HOME`, `XDG_STATE_HOME` +# +# Use `sandbox_path` to extend PATH: # # sandbox_env: # RUST_BACKTRACE: "1"