You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Per SECURITY.md I checked whether this belonged in a private
advisory. I do not think it does, and I want that reasoning visible rather than assumed:
P2 §9 makes intra-pod cross-leaf isolation an explicit non-goal: "A compromised leaf could
read sibling worktrees and the shared object store. This is acceptable… one trust domain in P2,"
with Kata isolation in P3 (P3: Sandbox sharing-ratio experiments (CAP/N tuning) #48) as the resolution.
ADR-0031 already records that "memory files reaching the sandbox means they land on a shared
pool's volume."
So a leaf reading another leaf's files is known and accepted. What I am filing is narrower and, I
think, not yet recorded: the promoted config cache outlives the leaf that created it, and a
later leaf with no configRef at all can answer from it. If a maintainer reads the threat model
differently, say so and I will move it to an advisory.
What happens
configOverlay materialises the bundle into a digest-keyed cache in the shared pool sandbox:
Nothing removes it. buildCleanupScript (harness/src/converge.ts:44) removes only the leaf
worktree (/workspace/leaves/<run>); grep -rn "sh-config" harness/src finds no rm of the shared
path. That is deliberate — the cache exists to be reused, and config-overlay.ts notes the per-leaf
link is kept "under the leaf workspace so cleanupWorkspace remains the only teardown path" — but
the consequence is that promoted CLAUDE.md and memory/ persist on the pool volume indefinitely,
world-readable (dr-xr-xr-x), after every leaf that used them has finished.
Why it is worth tracking anyway: it makes goal 6 false end to end
Spec §2 goal 6 is "Absent a promoted bundle, harness behavior is unchanged," and harness/src/run-turn.ts:382-384 deliberately makes that assertable — with no promoted bundle
the loader options have "EXACTLY the four base keys, which is what makes an absent configRef
byte-identical to today rather than merely intended to be."
That guarantee holds in the harness process. It does not hold observably, because the agent's
tools execute in the sandbox, and the sandbox still has the previous run's bundle on disk. A configRef-less leaf can explore the filesystem, find another workflow's skill and memory, and use
them.
Evidence
Measured while building the promoted-workflow demo (#215), not hypothesised. The demo dispatches one
prompt twice — once bare, once promoted — as a control. Second run of the demo, bare arm, on a
sandbox where the previous run's promoted leaf had run:
Perfect! Now I can write the ship note following the house rules. Here's the ship note:
SHIP NOTE · auth-idle-reaper-v2
WHAT: Fixed auth sessions dropping after 30 seconds of inactivity…
TICKET: KAG-4471
RISK: low — fix ships behind auth.idle_reaper_v2 flag and is reversible without redeploy
TOKEN: SHIPNOTE-7F3A-SANDBOX-OK
That request carried no configRef. "The house rules" is language from a promoted CLAUDE.md; KAG-4471 exists only in a promoted memory file; the token exists only in a promoted skill's references/ subdirectory. The first run's bare arm, before any promoted leaf had run on that
sandbox, answered "The workspace appears to be empty. Could you provide me with…" — so the delta is
the cache, not model variance.
Two practical consequences beyond confidentiality:
A bare leaf's behaviour becomes a function of what unrelated leaves promoted earlier, which is
a reproducibility problem for the replay contract, not only a privacy one.
Any A/B that uses a bare run as a control is silently invalid on a warm pool. Mine looked
perfectly plausible while proving nothing; only an explicit "the control must NOT know this"
assertion caught it.
Reproduce
# 1. promote anything and dispatch it once
kubectl exec -n default sandbox-0 -- find /workspace/.sh-config -type f
# ... bundle contents, still present after the leaf finished# 2. dispatch the same prompt with NO configRef and let the agent look around
Or run make demo-promoted-workflow twice with the purge in demo-promoted-workflow.sh disabled
(it exists precisely to work around this).
Options, not a prescription
Tear the digest cache down when the last lease referencing it is released (keeps reuse within a
run, drops it between).
Key the cache path by tenant/lease as well as digest, so it cannot be found by an unrelated leaf.
TTL or LRU it, so persistence is bounded rather than indefinite.
Or decide this is simply P3's (P3: Sandbox sharing-ratio experiments (CAP/N tuning) #48) to fix, and record it explicitly in ADR-0031 and P2 §9 —
ADR-0031 currently speaks to write-protection (chmod -R a-w), not visibility or lifetime, so a
reader can reasonably finish it believing the shared-volume exposure ends with the leaf.
Whichever way it goes, the demo's purge step and the note in its "Notes and limits" can be dropped
once the cache no longer outlives its leaf.
Refs: #215 (where this was measured), #48 (P3 Kata isolation), ADR-0030, ADR-0031, P2 §9, spec §2
goal 6.
Not a security report — routing note first
Per
SECURITY.mdI checked whether this belonged in a privateadvisory. I do not think it does, and I want that reasoning visible rather than assumed:
read sibling worktrees and the shared object store. This is acceptable… one trust domain in P2,"
with Kata isolation in P3 (P3: Sandbox sharing-ratio experiments (CAP/N tuning) #48) as the resolution.
pool's volume."
So a leaf reading another leaf's files is known and accepted. What I am filing is narrower and, I
think, not yet recorded: the promoted config cache outlives the leaf that created it, and a
later leaf with no
configRefat all can answer from it. If a maintainer reads the threat modeldifferently, say so and I will move it to an advisory.
What happens
configOverlaymaterialises the bundle into a digest-keyed cache in the shared pool sandbox:Nothing removes it.
buildCleanupScript(harness/src/converge.ts:44) removes only the leafworktree (
/workspace/leaves/<run>);grep -rn "sh-config" harness/srcfinds normof the sharedpath. That is deliberate — the cache exists to be reused, and
config-overlay.tsnotes the per-leaflink is kept "under the leaf workspace so
cleanupWorkspaceremains the only teardown path" — butthe consequence is that promoted
CLAUDE.mdandmemory/persist on the pool volume indefinitely,world-readable (
dr-xr-xr-x), after every leaf that used them has finished.Why it is worth tracking anyway: it makes goal 6 false end to end
Spec §2 goal 6 is "Absent a promoted bundle, harness behavior is unchanged," and
harness/src/run-turn.ts:382-384deliberately makes that assertable — with no promoted bundlethe loader options have "EXACTLY the four base keys, which is what makes an absent
configRefbyte-identical to today rather than merely intended to be."
That guarantee holds in the harness process. It does not hold observably, because the agent's
tools execute in the sandbox, and the sandbox still has the previous run's bundle on disk. A
configRef-less leaf can explore the filesystem, find another workflow's skill and memory, and usethem.
Evidence
Measured while building the promoted-workflow demo (#215), not hypothesised. The demo dispatches one
prompt twice — once bare, once promoted — as a control. Second run of the demo, bare arm, on a
sandbox where the previous run's promoted leaf had run:
That request carried no
configRef. "The house rules" is language from a promotedCLAUDE.md;KAG-4471exists only in a promoted memory file; the token exists only in a promoted skill'sreferences/subdirectory. The first run's bare arm, before any promoted leaf had run on thatsandbox, answered "The workspace appears to be empty. Could you provide me with…" — so the delta is
the cache, not model variance.
Two practical consequences beyond confidentiality:
a reproducibility problem for the replay contract, not only a privacy one.
perfectly plausible while proving nothing; only an explicit "the control must NOT know this"
assertion caught it.
Reproduce
Or run
make demo-promoted-workflowtwice with the purge indemo-promoted-workflow.shdisabled(it exists precisely to work around this).
Options, not a prescription
run, drops it between).
ADR-0031 currently speaks to write-protection (
chmod -R a-w), not visibility or lifetime, so areader can reasonably finish it believing the shared-volume exposure ends with the leaf.
Whichever way it goes, the demo's purge step and the note in its "Notes and limits" can be dropped
once the cache no longer outlives its leaf.
Refs: #215 (where this was measured), #48 (P3 Kata isolation), ADR-0030, ADR-0031, P2 §9, spec §2
goal 6.