From 84dade314b26cff86aef797c8b0ef5ad71f0ae38 Mon Sep 17 00:00:00 2001 From: Brandon Lucas <38222767+thebrandonlucas@users.noreply.github.com> Date: Thu, 5 Nov 2026 18:57:57 +0000 Subject: [PATCH 1/2] Give every Sandbox session an environment guide Sessions got instructions only for cluster diagnostics and sub-agents, and only when credentials were mounted. An agent therefore concluded it had no Linear access because nothing in its instructions said that enrolled services are reached through via. Add one guide, installed unconditionally, covering via discovery, Sandbox detection, app identities, untrusted code in a credentialed Pod, and vulnerability reporting. Signed-off-by: Brandon Lucas <38222767+thebrandonlucas@users.noreply.github.com> Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/voltage-agent-images.yml | 1 + voltage/images/buzz-agent/Dockerfile | 1 + .../images/buzz-agent/Dockerfile.dockerignore | 1 + voltage/images/buzz-agent/README.md | 3 + voltage/images/buzz-agent/environment.md | 19 +++++++ voltage/images/buzz-agent/profile.py | 19 ++++++- voltage/images/buzz-agent/test-environment.py | 56 +++++++++++++++++++ voltage/images/buzz-agent/test-profile.py | 2 +- 8 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 voltage/images/buzz-agent/environment.md create mode 100644 voltage/images/buzz-agent/test-environment.py diff --git a/.github/workflows/voltage-agent-images.yml b/.github/workflows/voltage-agent-images.yml index fa30354d16a..c482d3e2c12 100644 --- a/.github/workflows/voltage-agent-images.yml +++ b/.github/workflows/voltage-agent-images.yml @@ -284,6 +284,7 @@ jobs: python3 test-codex-login.py "$IMAGE" python3 -O test-profile.py "$IMAGE" python3 test-cluster.py "$IMAGE" + python3 test-environment.py "$IMAGE" python3 test-skills.py "$IMAGE" # main: resolve engineering-skills master once, so every platform image of diff --git a/voltage/images/buzz-agent/Dockerfile b/voltage/images/buzz-agent/Dockerfile index 267ef6429c1..86a91eb7be0 100644 --- a/voltage/images/buzz-agent/Dockerfile +++ b/voltage/images/buzz-agent/Dockerfile @@ -98,6 +98,7 @@ COPY --chmod=0644 voltage/images/buzz-agent/cluster.md /etc/buzz-agent/cluster.m # not change it; profile.py links each skill into the harnesses. # hadolint ignore=DL3022 COPY --from=engineering-skills . /opt/engineering-skills/ +COPY --chmod=0644 voltage/images/buzz-agent/environment.md /etc/buzz-agent/environment.md COPY --chmod=0755 voltage/images/buzz-agent/heavy-bin/ /opt/buzz-heavy/bin/ COPY --chmod=0755 voltage/images/buzz-agent/gh.sh /usr/local/bin/gh COPY --chmod=0755 voltage/images/buzz-agent/via.sh /usr/local/bin/via diff --git a/voltage/images/buzz-agent/Dockerfile.dockerignore b/voltage/images/buzz-agent/Dockerfile.dockerignore index efc42168e34..a021be5b0b5 100644 --- a/voltage/images/buzz-agent/Dockerfile.dockerignore +++ b/voltage/images/buzz-agent/Dockerfile.dockerignore @@ -99,3 +99,4 @@ voltage/ !voltage/images/buzz-agent/subagent.py !voltage/images/buzz-agent/subagents.md !voltage/images/buzz-agent/cluster.md +!voltage/images/buzz-agent/environment.md diff --git a/voltage/images/buzz-agent/README.md b/voltage/images/buzz-agent/README.md index e6ae73c858c..7222b524d11 100644 --- a/voltage/images/buzz-agent/README.md +++ b/voltage/images/buzz-agent/README.md @@ -45,6 +45,7 @@ python3 test-cache.py buzz-agent:local python3 -O test-profile.py buzz-agent:local python3 test-cluster.py buzz-agent:local python3 test-skills.py buzz-agent:local +python3 test-environment.py buzz-agent:local ``` To build with the engineering skills, replace `--none` with a local checkout: @@ -151,6 +152,8 @@ The bootstrap exports `KUBECONFIG` and appends [the diagnostic guide](cluster.md It skips configuration when neither token is mounted, and rejects credentials for another developer namespace. It performs no cluster API calls during startup. +Every session and worker also gets [the environment guide](environment.md), appended to the same instructions whether or not cluster credentials are mounted. It covers how to find enrolled services with `via capabilities`, how to detect a Sandbox, whose identity a `via` service uses, running untrusted code in a credentialed Pod, and where to report vulnerabilities. + The staging Terraform root owns the observer RBAC, worker projection, and API egress policy. Diagnostic permissions cover reads across staging namespaces, including logs, events, metrics, routing, and Argo CD state. They exclude Secrets, exec, attach, port forwarding, node/service proxies, and mutations. diff --git a/voltage/images/buzz-agent/environment.md b/voltage/images/buzz-agent/environment.md new file mode 100644 index 00000000000..10356ed1bc4 --- /dev/null +++ b/voltage/images/buzz-agent/environment.md @@ -0,0 +1,19 @@ + +## Working in the Sandbox + +Before you say you lack access to a service, run `via capabilities` and `buzz --help`. +Enrolled services (for example Linear, Grafana and Argo CD) are reached with `via api ...`, not with a dedicated CLI or an environment variable. +Depot is different: the image wires it into build caches (for example `depot gocache`), so it has no `via depot api`; check the cache before calling it unavailable. +When you report that something is unavailable, say which places you checked. + +Check that you are in a Sandbox with `KUBERNETES_SERVICE_HOST`, not with the path `/var/run/secrets/kubernetes.io`, which is not mounted in this Pod. + +A service reached through `via` acts as its own app identity, not as the person you work for. +Say so when you report a change made through it, such as a ticket you created. + +This Pod holds live credentials: a cluster token, your Buzz key, and `via` access. +Do not build, test, install dependencies for, or run code from a repository you do not trust. +Read its source, or ask the person you work for first. + +Keep details of an unfixed vulnerability out of public issues, pull requests, commit messages and public channels. +Report them only where the person you work for has chosen, such as a private channel or the company tracker. diff --git a/voltage/images/buzz-agent/profile.py b/voltage/images/buzz-agent/profile.py index e73709b948b..3a649d1dc69 100644 --- a/voltage/images/buzz-agent/profile.py +++ b/voltage/images/buzz-agent/profile.py @@ -279,9 +279,10 @@ def bootstrap(): env['CLAUDE_CODE_OAUTH_TOKEN'] = profile['models']['claude_oauth_token'] # The agent's tool shell starts from a cleared environment. Forward the model # credentials so tools it runs, such as buzz-agent-review, reach the runtime; - # they stay in process environments and are never written to files. + # they stay in process environments and are never written to files. The + # Kubernetes host is not secret; the environment guide tells agents to check it. env['BUZZ_ACP_MCP_FORWARD_ENV'] = ','.join( - name for name in (*INHERITED_MODEL_VARIABLES, 'CODEX_HOME') if env.get(name)) + name for name in (*INHERITED_MODEL_VARIABLES, 'CODEX_HOME', 'KUBERNETES_SERVICE_HOST') if env.get(name)) step('configuring Git commit signing') command('/usr/local/bin/buzz-agent-account', 'init', env=env) return env @@ -304,6 +305,7 @@ def restore_checkpoint(): SUBAGENT_GUIDE = Path('/etc/buzz-agent/subagents.md') CLUSTER_CREDENTIALS = Path('/var/run/secrets/buzz-cluster') CLUSTER_GUIDE = Path('/etc/buzz-agent/cluster.md') +ENVIRONMENT_GUIDE = Path('/etc/buzz-agent/environment.md') def install_cluster_access(env): @@ -336,6 +338,15 @@ def install_cluster_access(env): handle.write(guide) +def install_environment_guide(env): + """Give every Sandbox session the same rules for the services and credentials in its Pod.""" + guide = ENVIRONMENT_GUIDE.read_text() + for target in (Path('/home/agent/.claude/CLAUDE.md'), Path(env.get('CODEX_HOME', '/home/agent/.codex')) / 'AGENTS.md'): + target.parent.mkdir(parents=True, exist_ok=True) + with open(target, 'a') as handle: + handle.write(guide) + + def install_subagent_guide(env): """Tell Claude Code and Codex how to delegate when this session may spawn workers.""" if not (SUBAGENT_CREDENTIALS / 'token').exists(): @@ -403,6 +414,8 @@ def main(): install_cluster_access(env) step('installing engineering skills') install_skills(env) + step('installing the environment guide') + install_environment_guide(env) step('starting the worker') worker = ['/usr/local/bin/buzz-agent-cache', '/usr/local/bin/buzz-agent-worker'] os.execvpe(worker[0], worker, env) @@ -412,6 +425,8 @@ def main(): restore_checkpoint() step('configuring staging cluster diagnostics') install_cluster_access(env) + step('installing the environment guide') + install_environment_guide(env) step('installing the sub-agent guide') install_subagent_guide(env) step('installing engineering skills') diff --git a/voltage/images/buzz-agent/test-environment.py b/voltage/images/buzz-agent/test-environment.py new file mode 100644 index 00000000000..cad14ee76b4 --- /dev/null +++ b/voltage/images/buzz-agent/test-environment.py @@ -0,0 +1,56 @@ +"""Check that every session receives the environment guide, with or without cluster credentials.""" +import subprocess +import sys + +SCRIPT = r''' +import os, runpy, sys, tempfile +from pathlib import Path + +profile = runpy.run_path('/usr/local/bin/buzz-agent-profile', run_name='probe') +install = profile['install_environment_guide'] +settings = install.__globals__ +home = Path(tempfile.mkdtemp(prefix='environment-test-', dir='/home/agent')) +claude = Path('/home/agent/.claude/CLAUDE.md') + +def require(condition, message): + if not condition: raise RuntimeError(message) + +guide = settings['ENVIRONMENT_GUIDE'].read_text() +require('via capabilities' in guide and 'KUBERNETES_SERVICE_HOST' in guide, 'guide lost its rules') +settings['CLUSTER_CREDENTIALS'] = home / 'no-worker' +settings['SUBAGENT_CREDENTIALS'] = home / 'no-primary' +claude.parent.mkdir(parents=True, exist_ok=True) +claude.write_text('existing instructions\n') +env = {**os.environ, 'CODEX_HOME': str(home / 'codex')} +install(env) +install(env) +require(claude.read_text().startswith('existing instructions\n' + guide), 'Claude Code instructions were replaced or lack the guide') +require(claude.read_text().count('## Working in the Sandbox') == 2, 'each install call should append the guide once') +require((home / 'codex/AGENTS.md').read_text() == guide * 2, 'Codex instructions lack the guide') + +# Bind the production seam: each startup path must install the guide before it launches its child. +main = settings['main'] +for action in ('run', 'work'): + claude.write_text('') + events = [] + class Launched(Exception): pass + def launch(path, argv, env): events.append('launch'); raise Launched() + settings.update( + step=lambda message: None, bootstrap=lambda: env, restore_checkpoint=lambda: None, + install_cluster_access=lambda env: None, install_subagent_guide=lambda env: None, + install_skills=lambda env: None, + install_environment_guide=lambda env, real=install: (events.append('guide'), real(env))) + settings['os'].execvpe = launch + sys.argv = ['profile', action] + try: main() + except Launched: pass + require(events == ['guide', 'launch'], action + ' did not install the guide before launching: ' + repr(events)) + require(claude.read_text() == guide, action + ' did not write the guide') +print('PASS: the guide reaches Claude Code and Codex, and both startup paths install it') +''' + +if __name__ == '__main__': + result = subprocess.run(['docker', 'run', '--rm', '-i', '--network', 'none', + '--entrypoint', 'python3', sys.argv[1] if len(sys.argv) > 1 else 'buzz-agent:local', '-'], + input=SCRIPT, text=True, timeout=90) + raise SystemExit(result.returncode) diff --git a/voltage/images/buzz-agent/test-profile.py b/voltage/images/buzz-agent/test-profile.py index 1ecab3423f3..86ad61ad9b1 100644 --- a/voltage/images/buzz-agent/test-profile.py +++ b/voltage/images/buzz-agent/test-profile.py @@ -112,7 +112,7 @@ def rotate(models, git=None): # Exactly the configured model credentials reach the agent's tool shell. forwarded = set(filter(None, os.environ.get('BUZZ_ACP_MCP_FORWARD_ENV', '').split(','))) models = {k for k, v in EXPECTED.items() if v and k in INHERITED} -if not models <= forwarded or forwarded - models - {'CODEX_HOME'}: +if not models <= forwarded or forwarded - models - {'CODEX_HOME', 'KUBERNETES_SERVICE_HOST'}: raise RuntimeError('forwarded tool-shell variables differ from the profile credentials') if subprocess.check_output(['git','config','--global','user.email'], text=True).strip() != 'alice@example.invalid': raise RuntimeError('Git identity missing') From 30f4090e34b3c7504a05640d11ea22c94937bf80 Mon Sep 17 00:00:00 2001 From: Brandon Lucas <38222767+thebrandonlucas@users.noreply.github.com> Date: Fri, 6 Nov 2026 00:03:15 +0000 Subject: [PATCH 2/2] fix(images): tighten sandbox guide wording and assert Kubernetes host forwarding Co-Authored-By: Claude Sonnet 5.5 --- voltage/images/buzz-agent/environment.md | 6 +++--- voltage/images/buzz-agent/test-profile.py | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/voltage/images/buzz-agent/environment.md b/voltage/images/buzz-agent/environment.md index 10356ed1bc4..fed29a45b05 100644 --- a/voltage/images/buzz-agent/environment.md +++ b/voltage/images/buzz-agent/environment.md @@ -2,14 +2,14 @@ ## Working in the Sandbox Before you say you lack access to a service, run `via capabilities` and `buzz --help`. -Enrolled services (for example Linear, Grafana and Argo CD) are reached with `via api ...`, not with a dedicated CLI or an environment variable. +Enrolled services (for example Linear, Grafana and Argo CD) are reached with the capability that `via capabilities` lists for the service, usually `via api ...`, not with a dedicated CLI or an environment variable. Depot is different: the image wires it into build caches (for example `depot gocache`), so it has no `via depot api`; check the cache before calling it unavailable. When you report that something is unavailable, say which places you checked. Check that you are in a Sandbox with `KUBERNETES_SERVICE_HOST`, not with the path `/var/run/secrets/kubernetes.io`, which is not mounted in this Pod. -A service reached through `via` acts as its own app identity, not as the person you work for. -Say so when you report a change made through it, such as a ticket you created. +A service reached through `via` may act as its own app identity or as a user, depending on how it is configured. +When you report a change made through it, such as a ticket you created, name the identity it used, or say that you do not know. This Pod holds live credentials: a cluster token, your Buzz key, and `via` access. Do not build, test, install dependencies for, or run code from a repository you do not trust. diff --git a/voltage/images/buzz-agent/test-profile.py b/voltage/images/buzz-agent/test-profile.py index 86ad61ad9b1..8a31dc2deeb 100644 --- a/voltage/images/buzz-agent/test-profile.py +++ b/voltage/images/buzz-agent/test-profile.py @@ -112,6 +112,8 @@ def rotate(models, git=None): # Exactly the configured model credentials reach the agent's tool shell. forwarded = set(filter(None, os.environ.get('BUZZ_ACP_MCP_FORWARD_ENV', '').split(','))) models = {k for k, v in EXPECTED.items() if v and k in INHERITED} +if 'KUBERNETES_SERVICE_HOST' not in forwarded: + raise RuntimeError('Kubernetes host is not forwarded to the tool shell') if not models <= forwarded or forwarded - models - {'CODEX_HOME', 'KUBERNETES_SERVICE_HOST'}: raise RuntimeError('forwarded tool-shell variables differ from the profile credentials') if subprocess.check_output(['git','config','--global','user.email'], text=True).strip() != 'alice@example.invalid': @@ -177,6 +179,7 @@ def main(): '--tmpfs', '/tmp:uid=10001,gid=10001,mode=0700,size=128m,exec', '-e', 'AWS_ACCESS_KEY_ID=testing', '-e', 'AWS_SECRET_ACCESS_KEY=testing', '-e', 'AWS_DEFAULT_REGION=us-west-2', '-e', 'AWS_EC2_METADATA_DISABLED=true', + '-e', 'KUBERNETES_SERVICE_HOST=192.0.2.1', '-e', 'AWS_ENDPOINT_URL=http://'+server+':5000', '--entrypoint', 'python3'] result = subprocess.run([*base, '-e', 'FIXTURE_PROFILE='+json.dumps(PROFILE), image, '-'], input=SEED, text=True, capture_output=True, timeout=180)