Skip to content

Commit 37d6165

Browse files
committed
refactor(sandbox-kit): trim the spec to what it declares (PFM-7373)
Move the long-form rationale out of spec.yaml and leave short notes with pointers to the README, which already carries the same material under "Why nightly" and "Why the token is passed in". Removes the build-requirement banner, the gRPC/ALPN analysis, the commented-out proxy-managed credential block, the v1-to-v2 migration asides and the per-host debugging notes. No functional change: the egress allowlist, environment variables and wrapper behaviour are unchanged. Assisted-by: Claude Code Signed-off-by: Miguel Martinez <miguel@chainloop.dev> Chainloop-Trace-Sessions: 7da50f6b-428c-4749-82b5-562ae1ba890e
1 parent f33a2d3 commit 37d6165

1 file changed

Lines changed: 44 additions & 180 deletions

File tree

‎devel/sandbox-kit/claude/spec.yaml‎

Lines changed: 44 additions & 180 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,8 @@
1717
# sbx run --clone -e CHAINLOOP_TOKEN ./devel/sandbox-kit/claude
1818
# See devel/sandbox-kit/README.md for the sharp edges.
1919
#
20-
# !!! BUILD REQUIREMENT - this kit needs an sbx build where `extends:` merges
21-
# !!! ADDITIVELY. A child `setup:` block silently dropping the parent's
22-
# !!! (docker/sbx-releases#415) takes Claude's trust-flag seeding, MCP gateway
23-
# !!! registration and the ~/.claude/* volume ownership with it, so the sandbox
24-
# !!! starts but cannot write the transcript `chainloop trace` reads: it attests
25-
# !!! NOTHING, with no error. Known: <=v0.38.1 broken, nightly rc1-245 ok,
26-
# !!! v0.39.0 stable broken (#415 regressed in the release), rc1-441 ok.
27-
# !!! Check a build with:
28-
# !!! sbx exec <name> -- stat -c '%U:%G' /home/agent/.claude/projects # want agent:agent
20+
# Needs an sbx build whose `extends:` merges additively; on an affected build the
21+
# sandbox starts but records nothing. See "Why nightly" in the README.
2922
schemaVersion: "2"
3023
kind: sandbox # REQUIRED: only kind:sandbox may set an entrypoint
3124
name: chainloop-trace-claude
@@ -39,28 +32,11 @@ description: >-
3932
for `chainloop trace`: identity comes from its committed .chainloop.yml and the
4033
attestation is pushed on `git push`. Fork of the built-in `claude` agent.
4134
42-
# Everything the caller has to supply arrives as a kit ARGUMENT, so this file
43-
# holds no secrets and is committed as-is. `sbx kit validate ./kit` names any
44-
# argument it still needs.
45-
#
46-
# ONE MODE ON PURPOSE - persistent, i.e. `chainloop trace init` has already
47-
# written organization/projectName (and optionally the workflow, else
48-
# "ai-coding-session") into the repo's .chainloop.yml, and the managed pre-push
49-
# hook reads them from there. The kit contributes NOTHING but the token, which is
50-
# why there are no org/project/workflow arguments.
51-
#
52-
# The other mode the CLI offers, `chainloop trace run`, is deliberately NOT
53-
# supported here:
54-
# - it "ignores .chainloop.yml entirely. The attestation identity must come
55-
# from the --org, --project, --workflow flags (mandatory)"
56-
# (`chainloop trace run --help`), so it needs three arguments that are dead
57-
# weight for every repo this kit is actually pointed at;
58-
# - its teardown wipes .git/chainloop-trace/ and strips the committed agent
59-
# hooks from .claude/settings.json - destructive on a trace-initialized repo,
60-
# which is the only kind this kit accepts.
61-
# The trade-off that buys: a session whose work is never pushed attests nothing.
62-
# That is intended - unpushed work is discarded work, and the sandbox is
63-
# disposable. Push from inside the sandbox before it is reclaimed.
35+
# No secrets live here - everything the caller supplies is a kit argument, so
36+
# this file is committed as-is. There is no mode or identity argument: the kit
37+
# does persistent tracing only and reads org/project/workflow from the repo's
38+
# committed .chainloop.yml. See the README for why `chainloop trace run` is not
39+
# offered.
6440
args:
6541
chainloopToken:
6642
default: ""
@@ -86,31 +62,18 @@ args:
8662
worthwhile for self-hosted. NOTE its [auth] token is your personal
8763
login session: short-lived (~days) and your full identity. Prefer
8864
chainloopToken for anything unattended.
89-
# Inherit the built-in `claude` agent instead of restating it. This kit then
90-
# declares ONLY the Chainloop delta; everything below comes for free and stays in
91-
# step with sbx upgrades instead of drifting in a hand-copied spec:
92-
# - sandbox.image (no tag to verify or bump)
93-
# - the `anthropic` credential: proxy-injected x-api-key AND the OAuth block
94-
# - Anthropic/Claude egress (api.anthropic.com, claude.com, downloads.claude.ai,
95-
# mcp-proxy.anthropic.com, platform.claude.com, bridge.claudeusercontent.com)
96-
# - IS_SANDBOX=1
97-
# - ~/.claude.json trust+onboarding flags, ~/.claude/settings.json seeding, and
98-
# `claude mcp add mcp-gateway` (MCP is invisible to the session without it)
99-
# - the persistent ~/.claude/{projects,sessions,todos,shell-snapshots,statsig}
100-
# volumes - projects/ holds the transcript `chainloop trace` reads
101-
# - agentInstructions.filename: CLAUDE.md
65+
# Inherit the built-in `claude` agent - its image, Anthropic credential and
66+
# egress, MCP gateway registration and ~/.claude volumes - and declare only the
67+
# Chainloop delta below.
10268
extends: claude
10369

10470
sandbox:
105-
# REPLACES the parent's [claude, --dangerously-skip-permissions] -> the wrap
106-
# point. The wrapper re-adds that flag when it execs claude.
71+
# Replaces the parent's entrypoint; the wrapper re-adds its flags.
10772
entrypoint: ["/home/agent/.local/bin/cl-trace-wrap.sh"]
10873

10974
setup:
11075
install:
111-
# Install the chainloop CLI. The installer requires the target dir to exist,
112-
# so create it first. Run as root so we can write to /usr/local/bin (hooks
113-
# invoke `chainloop` by bare name -> must be on PATH).
76+
# The git hooks invoke `chainloop` by bare name, so it has to be on PATH.
11477
- command: >-
11578
mkdir -p /usr/local/bin &&
11679
curl -sfL https://dl.chainloop.dev/cli/install.sh | bash -s -- --path /usr/local/bin &&
@@ -128,10 +91,8 @@ setup:
12891
exec claude --dangerously-skip-permissions "$@"
12992
}
13093
131-
# Every path that would hand back an UNTRACED agent is fatal instead.
132-
# This kit exists to make a session provable, and a sandbox that quietly
133-
# records nothing is the one outcome worse than not starting: you find
134-
# out only when the attestation you expected is not there.
94+
# Anything that would hand back an untraced agent is fatal: a sandbox
95+
# that quietly records nothing is worse than one that refuses to start.
13596
die() {
13697
echo "[chainloop-trace] ERROR: $1" >&2
13798
exit 1
@@ -142,15 +103,8 @@ setup:
142103
(check egress to dl.chainloop.dev). Refusing to start an untraced agent."
143104
fi
144105
145-
# Adopt a mounted host config.toml when one was pointed at. sbx mounts an
146-
# extra read-only workspace at the SAME absolute path it has on the host,
147-
# which is NOT where the CLI looks: `chainloop config view` inside the
148-
# sandbox reports /home/agent/.config/chainloop/config.toml (Linux XDG),
149-
# verified. Copy rather than pass --config, because in persistent mode the
150-
# managed git hooks invoke `chainloop` as their own processes and a flag
151-
# from this wrapper would never reach them.
152-
# Deliberately not named CHAINLOOP_CONFIG: the CLI's viper prefix is
153-
# CHAINLOOP, so that name could bind to its own --config flag.
106+
# Adopt a mounted host config.toml. Copied into place rather than passed
107+
# with --config, so the git hooks find it in their own processes too.
154108
if [ -n "$CL_TRACE_CONFIG_SOURCE" ]; then
155109
if [ ! -r "$CL_TRACE_CONFIG_SOURCE" ]; then
156110
die "chainloopConfig points at '$CL_TRACE_CONFIG_SOURCE', which is not
@@ -163,23 +117,17 @@ setup:
163117
echo "[chainloop-trace] Adopted chainloop config from $CL_TRACE_CONFIG_SOURCE" >&2
164118
fi
165119
166-
# Something has to authenticate the attestation. An explicit token wins
167-
# over a config-file login session - the CLI prefers an exported
168-
# CHAINLOOP_TOKEN and logs "Both user credentials and $CHAINLOOP_TOKEN
169-
# set. Ignoring user credentials." (app/cli/cmd/root.go) - but with
170-
# neither, nothing can be pushed, so refuse rather than record nothing.
120+
# Without credentials nothing can be pushed, so refuse rather than
121+
# record nothing. A token wins over a config-file login session.
171122
if [ -z "$CHAINLOOP_TOKEN" ] && [ ! -r "$HOME/.config/chainloop/config.toml" ]; then
172123
die "no Chainloop credentials. Pass an org-scoped API token with
173124
--kit-arg chainloopToken=<token>
174125
or mount your host config and point at it with
175126
--kit-arg chainloopConfig=<path-inside-sandbox>"
176127
fi
177128
178-
# Persistent tracing is the only mode: the repo must already be
179-
# initialized, and the identity comes from its committed .chainloop.yml.
180-
# (Every kit arg has a default, so the vars above are always defined and
181-
# bare $VAR is safe under `set -u`; the validator rejects shell
182-
# default-expansion inside setup.files content.)
129+
# Persistent tracing only: the repo must already be initialized, and the
130+
# identity comes from its committed .chainloop.yml.
183131
repo_root=$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")
184132
detected=0
185133
for f in "$repo_root/.chainloop.yml" "$repo_root/.chainloop.yaml"; do
@@ -211,129 +159,45 @@ setup:
211159
212160
environment:
213161
variables:
214-
# NOTE: HOME is deliberately NOT declared. The agent and the hook subprocess
215-
# MUST share it (transcript discovery is os.UserHomeDir()-based; a mismatch
216-
# silently loses usage/cost/tools), but sbx already sets HOME=/home/agent on
217-
# PID 1, so every child inherits it - verified in a plain `claude` sandbox.
218-
# Self-hosted overrides only (defaults are the SaaS endpoints):
219-
# CHAINLOOP_CONTROL_PLANE_API: "api.cp.chainloop.dev:443"
220-
# CHAINLOOP_ARTIFACT_CAS_API: "api.cas.chainloop.dev:443"
221-
#
222-
# Path B: the REAL org-scoped API token is set directly in the sandbox env,
223-
# from the required `chainloopToken` arg - so no secret is stored in this
224-
# file and it is committed as-is.
225-
# Do NOT use `sbx secret set-custom` / proxy-managed header injection for this
226-
# token (see the commented-out Path A block under `credentials:`): injection
227-
# requires the proxy to TERMINATE TLS, and the intercepted path does not
228-
# negotiate the `h2` ALPN - which is exactly what grpc-go >=1.67 refuses.
162+
# HOME is deliberately not declared: the agent and the hook subprocess must
163+
# share it, and sbx already sets it on PID 1.
229164
CHAINLOOP_TOKEN: ${{ kit.args.chainloopToken }}
230-
# Path (inside the sandbox) of a mounted host config.toml for the wrapper to
231-
# adopt. Not CHAINLOOP_-prefixed on purpose - see the wrapper's note.
165+
# Path inside the sandbox of a mounted host config.toml for the wrapper to
166+
# adopt. Not CHAINLOOP_-prefixed, so it cannot bind to the CLI's own flags.
232167
CL_TRACE_CONFIG_SOURCE: ${{ kit.args.chainloopConfig }}
233-
#
234-
# CRITICAL (gRPC vs the egress proxy): `chainloop trace` talks to the control
235-
# plane over gRPC (HTTP/2 + ALPN). sbx sets HTTP(S)_PROXY=gateway.docker.internal:3128
236-
# and grpc-go honors it, dialing via HTTP CONNECT. The proxy handles a host in
237-
# one of two ways, and only one of them works for gRPC (probe any host with
238-
# `curl -sv --http2 --proxy "$HTTPS_PROXY" https://<host>/` and read the issuer):
239-
# TUNNEL - no credential targets the host. TLS is end-to-end (real
240-
# Let's Encrypt cert), ALPN reaches the server, `h2` negotiated.
241-
# INTERCEPTED - some credential targets the host, so the proxy terminates TLS
242-
# to rewrite headers (issuer "Docker Sandboxes Proxy CA"). It
243-
# answers `ALPN: server did not agree on a protocol` and drops
244-
# to HTTP/1.1; grpc-go >=1.67 aborts with "missing selected ALPN
245-
# property" and NO attestation is sent.
246-
# A host flips to INTERCEPTED the moment ANY credential targets it - a kit
247-
# apiKey.inject[].domain, OR a host-side `sbx secret set-custom --host
248-
# api.cp.chainloop.dev --env CHAINLOOP_TOKEN`, which is easy to add from outside
249-
# this repo and silently breaks tracing (verified on sbx v0.38.0: same kit,
250-
# same empty token, tunnel before the custom secret existed and intercepted
251-
# after; re-verified unchanged on v0.39.0-rc1-441). Bypassing the proxy for the
252-
# Chainloop hosts makes tracing immune to that decision; egress is still
253-
# enforced by sbx's allowlist. Keep localhost / gateway.docker.internal, which
254-
# is what sbx's own default NO_PROXY carries.
168+
# The Chainloop hosts MUST bypass the egress proxy: `chainloop trace` speaks
169+
# gRPC, and the proxy's intercepted path cannot negotiate h2, so anything that
170+
# routes these hosts through it silently breaks attestation. See the README.
255171
NO_PROXY: "localhost,127.0.0.1,::1,gateway.docker.internal,api.cp.chainloop.dev,api.cas.chainloop.dev,api.app.chainloop.dev"
256172
no_proxy: "localhost,127.0.0.1,::1,gateway.docker.internal,api.cp.chainloop.dev,api.cas.chainloop.dev,api.app.chainloop.dev"
257173

258174
credentials:
259-
# No `anthropic` entry: it is inherited from the parent, including the OAuth
260-
# block that a hand-written fork would have had to copy. Do NOT redeclare it -
261-
# named arrays merge by identity key (credentials[].service) and a duplicate
262-
# service across parent and child is an ERROR, not an override.
263-
264-
# --- Path A (NOT USABLE TODAY - verified v0.38.0, re-verified on the v0.39 line)
265-
# Proxy-managed injection of the Chainloop token, so the real secret never
266-
# enters the VM. Uncomment this entry, drop the CHAINLOOP_TOKEN variable above
267-
# and the chainloopToken arg, and store the secret on the host with
268-
# sbx secret set-custom --host api.cp.chainloop.dev --env CHAINLOOP_TOKEN
269-
#
270-
# Why it cannot be enabled yet - TESTED END-TO-END, not inferred: injection only
271-
# works on hosts the proxy INTERCEPTS (it must terminate TLS to rewrite the
272-
# header), and the intercepted path answers `ALPN: server did not agree on a
273-
# protocol`, dropping to HTTP/1.1. grpc-go >=1.67 then aborts with "missing
274-
# selected ALPN property" and NO attestation is sent.
275-
#
276-
# !! This also applies to `sbx secret set-custom --host api.cp.chainloop.dev
277-
# !! --env CHAINLOOP_TOKEN`, which is the same mechanism by another route (it
278-
# !! substitutes a `sbx-cs-...` placeholder in outbound headers). A global custom
279-
# !! secret for the Chainloop hosts is INERT only because this kit sets
280-
# !! CHAINLOOP_TOKEN itself, which suppresses the binding. Stop setting the token
281-
# !! here and the placeholder goes live, the hosts flip to the intercepted path,
282-
# !! and tracing dies with the ALPN error. Verified on sbx v0.38.0:
283-
# !! CHAINLOOP_TOKEN=sbx-cs-... -> issuer "Docker Sandboxes Proxy CA", HTTP/1.x
284-
# !! kit-set CHAINLOOP_TOKEN -> issuer "Let's Encrypt", h2, gRPC works
285-
#
286-
# Re-test with:
287-
# curl -sv --http2 --proxy "$HTTPS_PROXY" https://api.cp.chainloop.dev/
288-
# and enable this once the issuer is "Docker Sandboxes Proxy CA" AND the server
289-
# still agrees on h2.
175+
# No `anthropic` entry: it is inherited from the parent. Do NOT redeclare it -
176+
# a duplicate credentials[].service across parent and child is an ERROR.
290177
#
291-
# - service: chainloop
292-
# apiKey:
293-
# name: CHAINLOOP_TOKEN
294-
# proxyManaged: true
295-
# inject:
296-
# - domain: api.cp.chainloop.dev
297-
# scheme: bearer # v2 shorthand for Authorization: Bearer %s
298-
# - domain: api.cas.chainloop.dev
299-
# scheme: bearer
300-
# - domain: api.app.chainloop.dev
301-
# scheme: bearer
302-
# ---------------------------------------------------------------------------
178+
# Proxy-managed injection of the Chainloop token is not usable here: it needs
179+
# the proxy to terminate TLS, which breaks the kit's gRPC traffic. See the
180+
# README for the full reasoning and how to re-test it.
303181

304182
permissions:
305-
# Sandbox egress is deny-by-default. The Anthropic/Claude hosts come from the
306-
# parent (set union, deduplicated, parent order first); these are the Chainloop
307-
# additions.
183+
# Egress is deny-by-default; the Anthropic/Claude hosts come from the parent.
308184
network:
309185
allow:
310-
- "api.cp.chainloop.dev:443" # control plane - ALWAYS (attest + keyless signing CSR)
311-
- "api.cas.chainloop.dev:443" # CAS - only if the org CAS backend is external
312-
- "api.app.chainloop.dev:443" # platform backend - the CLI also dials this
313-
- "t.chainloop.dev:443" # CLI usage telemetry (PostHog) - current builds
314-
- "crb.chainloop.dev:443" # CLI usage telemetry (PostHog) - binaries released
315-
# before the endpoint moved. The host is compiled
316-
# in, so an older tag checked out in a sandbox
317-
# still reaches for it. Commands succeed either
318-
# way, but a connection that is dropped rather
319-
# than refused costs each tracked command a
320-
# couple of seconds waiting on the send deadline.
321-
- "timestamp.digicert.com:80" # RFC 3161 timestamp authority (plain HTTP, signed response)
322-
# used during attestation signing; host comes from the
323-
# control plane's signing options
324-
- "buf.build:443" # Buf Schema Registry - `make api` / `buf generate`
325-
# resolve the buf.yaml module deps and the remote
326-
# plugins declared in the buf.gen.yaml files
186+
- "api.cp.chainloop.dev:443" # control plane
187+
- "api.cas.chainloop.dev:443" # CAS, when the org backend is external
188+
- "api.app.chainloop.dev:443" # platform backend
189+
- "t.chainloop.dev:443" # CLI telemetry
190+
- "crb.chainloop.dev:443" # CLI telemetry, older binaries
191+
- "timestamp.digicert.com:80" # RFC 3161 timestamp authority, used when signing
192+
- "buf.build:443" # Buf Schema Registry, for `make api`
327193
- "dl.chainloop.dev:443" # CLI installer
328194
- "chainloop-baafegchfnekdcde.z02.azurefd.net:443" # CLI download CDN
329-
- "github.com:443" # git push over HTTPS in persistent mode - the
330-
# pre-push attestation egresses on push. Other
331-
# forges/self-hosted: add your own host here.
195+
- "github.com:443" # git push - the attestation egresses here.
196+
# Other forges: add your own host.
332197

333198
agentInstructions:
334-
# filename is inherited (CLAUDE.md). NOTE: `content` is a scalar, so this
335-
# REPLACES the parent's CLAUDE.md text (its CLAUDE_ENV_FILE / shell-completion
336-
# guidance) rather than appending to it.
199+
# filename is inherited (CLAUDE.md). `content` is a scalar, so this REPLACES
200+
# the parent's text rather than appending to it.
337201
content: |
338202
## Chainloop tracing
339203
This session is recorded by Chainloop trace. Tool/MCP usage and coding

0 commit comments

Comments
 (0)