Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -154,22 +154,36 @@ 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")
for name in sorted(shell_set - config_set):
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
15 changes: 13 additions & 2 deletions docs/tend.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading