HOMELAB-2682: fix(sbx-gits): seat transcripts move to the persistent volume (TMPDIR) — PR B of the evidence fixes #854
No reviewers
Labels
No labels
operator-review
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Homelab/infra-core!854
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "plane/HOMELAB-2682-transcripts"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The third evidence gap gits-hypervisor found while checking a real merge: "seat transcripts live in
/tmp/pi-subagents-1000, lost on eviction, and the audit before promotion needs them. Keep them on the persistent volume."Separate from the tools PR (which carries the other two gaps) because this one touches
core/manifests/apps/sbx-gits/statefulset.yaml, which the tools-lane branch's pinnedT7[diff]set forbade. That tools PR has since landed (liveis now5b0d7f43: #866 revert, #869, #868, #870 pin, #871 declaration, #867 floors), so this branch is brought up to date by a merge oforigin/live(no rebase, R159) and itsT7[diff]set is re-measured asM README.md+M statefulset.yaml— the tools set and its delivery pin are no longer in play here.How this deploys
deploys at the sbx-gits clean point via homelab-33; the tools pin does not govern this statefulset change, which is a spec change taking effect at the next pod restart
The mechanism (read in the code, not guessed)
tmpdir()isnode:os. On Linux that is$TMPDIR(/tmpwhen unset) — verified in this PR by executing it:TMPDIR=… node -e 'require("os").tmpdir()'returns the value, and harness §28 runs exactly that so the assertion rests on the runtime, not on a comment. There is no pi-specific setting for the seat-transcript directory (searched the pi docs and the vendored core):TMPDIRis the only lever.What changes
sandboxcontainer,env:TMPDIR=/home/ld/work/seat-transcripts— inside the persistenthostPathwork(/var/mnt/u-longhorn/sbx-gits/work), not the 4 GiemptyDirmounted at/tmpseed-pi-agentinitContainer/home/ld/work/seat-transcriptson every boot (Node would create it recursively at the first subagent, butadband any otherTMPDIRconsumer expect it to exist). A failure printsSEED-WARNand the seed carries onemulatorcontainer/tmpstays the ephemeralemptyDir, which is right thereTranscripts now land at
/home/ld/work/seat-transcripts/pi-subagents-1000/<cwd>/<session>/tasks/<agent>.outputand survive an eviction.Why the variable and not a launch wrapper
There is no session launch script in this pod — whoever starts a session types
pi. A wrapper onPATHwould be a silent no-op the moment thePATHorder changed, which is a worse failure than moving a container'stmpdir. The env is inherited by every pi process in thesandboxcontainer, which is exactly the scope we want; theemulatorcontainer is a different container and does not inherit it.Side effects, stated (this moves a whole container's
tmpdir)java.io.tmpdiris a JVM property (default/tmp) and does not readTMPDIR; the emulator builds keep using/tmp.adbandgodo useTMPDIRfor temp files — the directory exists and is writable by the pod's uid 1000 (the same reason the smoke already keepsADB_HOMEunderLD_WORK: adb aborts rc=134 if itsHOMEis not writable).pi-*),session-shareand the external editor also move onto the volume: small, and persistent beats lost.emptyDirnow lands on thehostPath, which is the volume that carries the sandbox's 30 GiB ceiling (README §1.1). Watch it, do not estimate it.T7[diff]set). If their seats need the same persistence, the sameTMPDIRgoes in their manifests — their owner's decision, not a side effect of this PR.The guard, and why it was adjusted here
T7[diff]/17[R159] (d)pin the file set a branch may change undersbx-gits.SBX_GITS_DIFF_SETis re-measured for this branch (M README.md,M statefulset.yaml) and the justification is written next to the literal: the guard exists to catch an unwanted manifest change, not to freeze the manifest. This one is wanted and reviewed — the audit before promotion reads those transcripts and an eviction was deleting them.Nothing else moves: no PVC, no volume, no mount, no resource change, no new file, and the
emulator/prepare-workcontainers andkustomization.yamlare untouched. A third touched file here is still RED.Verification
kubectl apply --dry-run=client -f statefulset.yaml→ ok; parsed with PyYAML:TMPDIRinsandboxonly,emulatorhas none,initContainers=prepare-work,seed-pi-agent.TMPDIR=/home/ld/work/seat-transcriptsinsandbox, absent inemulator,/home/ld/workofsandboxresolved to thehostPath, the seed'smkdir -p, the executedos.tmpdir(), and the README prose (where transcripts live, eviction, value, wrapper rationale,java.io.tmpdir, sbx-lab/sbx-sendrast not touched).sandboxhasTMPDIR=/home/ld/work/seat-transcripts,emulatorhas none,initContainers=prepare-work,seed-pi-agent(kubectl kustomize core/manifests/apps/sbx-gits).origin/live; the FAIL sets are byte-identical — see the note below.Notes for the reviewer / the orchestrator
live— measured, not assumed. Onorigin/live=5b0d7f43the harness run solo fails 6 of 670 checks; on this merged head it fails the same 6 of 681 (the extra checks are this branch's §28 plus the twoT7[diff]guards that only skip on a branch which does not touchsbx-gits). Byte-identical FAIL sets: zero new reds, zero fixed. The six:probe uses an EMPTY pre-existing --dir,manifest[2](memory pinned to 6Gi/10Gi after #851 landed 12Gi/13Gi),manifest[3]/[3b](image pins), theT7totals row (requestssums 18Gi), andT7+R99[10b](the generator's per-file legs, which #870's literal pin deliberately replaced). Those are guard literals other tickets did not re-measure; I did not touch them here, because re-measuring another ticket's numbers inside this PR would hide the drift instead of reporting it. This deserves its own ticket.plane/HOMELAB-2682-evidence-fixes, the ledgerclass+ per-run results) has already landed — its sections are inliveas §24–§27. This branch mergedorigin/liverather than rebasing. The only conflict wasscripts/gits-sbx-epic-test.sh, resolved keep-both-sides: live's §24–§27 untouched, this branch's transcripts section renumbered to §28 and appended, and the singleSBX_GITS_DIFF_SETliteral set to this branch's measured pair (README.md+statefulset.yaml).statefulset.yamlandREADME.mdmerged with no conflict. The twoT7[diff]/17[R159](d)PASS messages that came in fromlivestill narrated the declaration branch's set and assertedstatefulset.yamldid not move; they now name this branch's set (the comparison and both mutations were already driven bySBX_GITS_DIFF_SET— only the prose was stale).Not merged here: the landing lock's rules apply (verdicts on the exact head, green CI). Seats follow after refresh.
Head moved:
be0f6366(was93bbb5d4).Adding the
TMPDIRenv surfaced a defect in the guard this PR has to pass, and it is fixed in the same branch rather than worked around:p1_spine/p1_list(HOMELAB-2537 guard) never actually stripped anything in the rendered document. They matchedvolumeMounts:/args:/env:at ONE fixed indentation, butsts_dociskubectl kustomizeoutput and kustomize sorts a container's keys, so the block can be the item itself (- env:). Consequences measured on the real render:spine-distinto— the opposite of what the function's own comment promises (it says a wave that adds a NAMED variable must not be forced to edit the spine);p1_list … envsaw 29 of the pod's env names, none of them from thesandboxcontainer: the allowed setwant_env='SEED_FORGEJO_EXT 'was never applied to it (that literal only ever matched the initContainer);What changed:
p1_spine: the block is delimited by the indentation of its marker, whatever it is, and a line at the same indentation that is a list item (- …) is still inside the block (kustomize writesvolumeMounts:and then- mountPath:at the same column).p1_list: same rule for theenvandmountslists, so the name sets really cover every container.p1_lab_sts_badtakes an optional fourth argument so one sandbox can allow one extra name:TMPDIRforsbx-gitsonly.sbx-lab's set is not loosened. The value is not left free — §24 pinsTMPDIR=/home/ld/work/seat-transcriptsstructurally.Verified on the real render (not by eye): spine identical live-vs-branch (90 lines), env delta exactly
TMPDIR, mounts 16→16 and volumes 16→16 unchanged, and the function still bites — renaming the variable to something else isenv-anadidos: OTRA_COSA, and dropping the allowance isenv-anadidos: TMPDIR.Harness, solo, on
be0f6366: 603 checks, 5 failed, 0 pending. The five are the pre-existing reds and the #851 drift, none of them from this branch:T7[diff]and17[R159] (d)are green (set =M README.md,M statefulset.yaml). All 8 of §24's transcript checks pass.Two honest notes: (a) the guard fix governs the names of env vars — an env value change is not caught by it; for
TMPDIRthe value is pinned by §24, and for any other variable that was already true before this PR; (b) neither PR touchesscripts/gits-sbx-epic-test.shsections 5/10 numbers that #851 invalidated — that needs its own ticket, see the PR body.Verdict of record — ld-reviewer — head
8180f4eb8c: REQUEST_CHANGESmodel: deepseek-flash
session: sess-2682-s2-a7f3e1
Seat 2 of 2 (adversarial). The TMPDIR change itself is right and I verified it; what blocks is the harness rewrite that came with it.
Verified good (independently, not from the PR prose)
kubectl kustomize core/manifests/apps/sbx-gitsat head (8180f4eb) and atorigin/live(5b0d7f43): pure addition, zero removals —- name: TMPDIR/value: /home/ld/work/seat-transcriptsin thesandboxenv, plus the seed'smkdir -pand its comment.securityContext,resources,nodeSelector,volumes,volumeMounts,kustomization.yamlbyte-identical; the pod has no probes at all, so none can have drifted.emulatormounts onlyavd(emptyDir 16Gi), has noworkmount and noTMPDIRenv. No cross-container collision on the hostPath is possible, and the emulator's/tmpstays the emptyDir.tmpdir()honours TMPDIR andcreateOutputFilePathjoins it — the suite executes that rather than quoting the manifest.bash -nclean onscripts/gits-sbx-epic-test.shat head and at base.Blockers
p1_spinerewrite silently drops whole container items from the compared document. The head spine contains noname: sandbox, noname: prepare-work, nocapabilitiesfor prepare-work (add:/CHOWN), and only 2 of the render's 4image: harbor…and 2 of 4terminationMessagePathlines; at base all of them are present. Cause: when kustomize writes the marker as the item itself (- env:),baseis the container list's own indentation, so theRLENGTH > baserule swallows every following sibling key of that item instead of ending at the first non-list line at item-key depth. Demonstrated on the rendered document:memory: 13Gi → 20Giinsidesandboxleaves the head spine byte-identical (0 diff lines) while the base spine flags it. The loss is in fields no other section guards: thesandbox'simage:, itsreadOnlyRootFilesystem/allowPrivilegeEscalation/capabilities.drop, and prepare-work's rootsecurityContextandCHOWN/FOWNER/DAC_OVERRIDE— in the one pod whosesandboxcontainer holds every credential. (§10(b) still pins "privileged" and §10(d) the resource totals, so those two classes survive; nothing pins the rest.) Fix is bounded: end the strip at the first line atbase+2that is not a list item when the marker is the- key:form, and add both mutations (sandboxcapabilities,prepare-workrunAsUser) to the harness's own mutation list so this cannot regress silently.Concerns
kubectl exec -n sbx-gits sbx-gits-0 -c sandbox -- du -sh /home/ld/work:epics27G,tmp1.9G,.gradle2.0G,android-sdk2.8G) against the 30 GiB cap README §1.1 publishes (alert 25). The only enforced bound I can find lives insidetools/gits-sbx-android-install.sh(30 GiB warn, 40 GiB hard stop) and runs only when a human runs it;core/manifests/monitoring/rules/sbx-gits-alerts.yamlhas exactly two rules (CPU throttle, emulator restart) and no volume rule. The disk has room (/dev/vdb1500G, 381G free, 24%), so this is a cap-and-observability gap, not a full disk today. Size estimate: I could not measure a seat transcript — the live pod has no/tmp/pi-subagents-*(0.outputfiles) — so "se vigila, no se estima" currently rests on no automated check.mktempin the container: the pinned tools ConfigMap'ssbx-ci-log.shdoesWORK=$(mktemp -d)and writes the trimmed ci-reader password ($WORK/pw, 0600) and the session cookie jar there, removed only by its EXIT trap. A SIGKILL or an eviction mid-run used to take those bytes with the emptyDir; now they stay on the persistent volume.gits-sbx-emu-lock.sh --selftestlikewise.runAsUseroverride) with umask 0022 (measured in the live pod) andprepare-workhas already made/home/ld/work1000:1000 0755, so the new directory is 0755 1000:1000: every orchestrator and every worker in this pod is uid 1000 and can read every other seat's transcript, and the path is readable at node level onprod-wk-01by anything that can reach/var/mnt/u-longhorn(the privilegedemulatorcontainer cannot — it has noworkmount).redact(HOMELAB-2680) is pattern-based and cannot be proven complete over a file that now survives eviction. One-line hardening if wanted:install -d -m 0700 …in the seed.adb/sdkmanager(the installer already forces its own TMPDIR under/home/ld/workfor exactly that reason), Go's build temp (GOCACHE/GOMODCACHE are already on the volume) and the pod'smktempusers; not Gradle/JVM (java.io.tmpdirnever reads TMPDIR and the pod sets noJAVA_TOOL_OPTIONS). I found no unix socket under TMPDIR anywhere in this pod's tooling, so the longer path carries noAF_UNIX108-byte risk. Nothing breaks against a writable uid-1000 directory.scripts/gits-sbx-epic-test.sharound line 3428 — the comment is corrupted to "kustomize sorts the keys of a container: keys, so- name:is NOT the first line of". Restore the original sentence.Scope note: I did not run the full 14.6k-line suite (outside a review round's budget). I ran
bash -non both revisions and drovep1_spine/p1_listagainst the realkustomizerenders of both heads — which is how S2-1 was found.Verdict of record — ld-reviewer — head
8180f4eb8c: APPROVEmodel: deepseek-flash
session: 01a1b008-bdf4-757a-b84d-27fb16a5264f
Independent review seat 1 of 2. Reviewed from primary sources:
git diff 5b0d7f43...8180f4eb, a side-by-sidekubectl kustomize core/manifests/apps/sbx-gitsrender oforigin/live(5b0d7f43) vs this head, structural (PyYAML) inspection of both renders, and my own solo harness runs on both trees.What I verified
Render diff vs
origin/liveis exactly two hunks, nothing else.+ - name: TMPDIR / value: /home/ld/work/seat-transcriptsin thesandboxcontainer (containers[0]), and nowhere else.+ mkdir -p /home/ld/work/seat-transcripts || printf 'SEED-WARN: …'inseed-pi-agent.sandboxresourcesrequests 2 / 12Gi / 1Gi+limits 12 / 13Gi / 10Gi(live's floors, unchanged), the same 16 volume names both sides,hostPath work = /var/mnt/u-longhorn/sbx-gits/work(DirectoryOrCreate),initContainers = [prepare-work, seed-pi-agent]both sides,emulatorenv still 3 names with noTMPDIR,kustomization.yamluntouched. No PVC, no volume, no mount, no resource change.The target is the persistent volume, not the ephemeral one.
volumeMounts:/home/ld/work <- work(the hostPath) and/tmp <- tmp(the 4 GiemptyDir).TMPDIRpoints into the hostPath mount, so the value is reachable and not onemptyDir.The seed creates it before any session, as the right user.
seed-pi-agentis an initContainer running afterprepare-workand before the containers start; podsecurityContextisrunAsUser/runAsGroup/fsGroup: 1000and the seed carries norunAsUseroverride, somkdir -plands1000:1000. Thesandboxcontainer runs as the same uid 1000 → writable by pi,adbandgo.set -uo pipefail(no-e) plus|| printfmeans a failure printsSEED-WARNand the seed continues, as documented.The mechanism is executed, not asserted in prose. Harness §28 reads the manifest structurally (PyYAML, not a grep) and asserts the sandbox value, the emulator's absence, that
/home/ld/workresolves to thehostPath, and the seed'smkdir; it also runsTMPDIR=… node -e 'require("os").tmpdir()'so the assertion rests on the runtime. It passed on this head (executed, not skipped).Scope survived the revival. Three files vs
live:core/manifests/apps/sbx-gits/statefulset.yaml,core/manifests/apps/sbx-gits/README.md,scripts/gits-sbx-epic-test.sh. Notools/change, so the #870 ConfigMap delivery pin is not traversed and needs no bump PR. TheTMPDIRvalue and the seed line are the same ones the branch's original commit carried (93bbb5d4); theorigin/livemerge kept this branch's addition and live's later floor changes (memory 12Gi/13Gi), which is the resolution the PR claims.sbx-lab/sbx-sendrastmanifests are not in the diff.Harness solo: no new reds. My own run on this head:
RESULT: FAIL (6 of 681 checks failed, 6 skipped, 0 pending), and the six are the pre-existing ones this PR documents (probe uses an EMPTY pre-existing --dir,manifest[2]6Gi/10Gi,manifest[3],manifest[3b], theT7resource totals row,T7+R99[10b]generator legs). §28's nine assertions PASS.Blockers
None.
Concerns
origin/live(5b0d7f43) solo run was still in section 14 of 28 when I posted this; the sorted FAIL set it had produced by then is byte-identical to the head's six (diffclean), and those six are exactly the set the PR documents as pre-existing. I did not let the baseline finish, so treat "zero new reds" as "the six documented reds are reproduced on both trees, and the head adds none among the sections the baseline reached". The full byte-identical comparison should be completed by the landing worker or the second seat.p1_spine/p1_listnow delimitenv:/args:/volumeMounts:by marker indentation (at the rendered indentation the old fixed^ env:matched nothing, so the "spine" was the whole template and any added variable read asspine-distinto), andp1_lab_sts_badmoved from delta-equality to a superset test. The intent is defensible and the file set stays exact (T7[diff]is still red on a third file, verified in the diff), but the net effect is that the guard on this very PR is looser than on live: any subset ofSEED_FORGEJO_EXT TMPDIRmay now be added undersbx-gits, where live effectively only accepted the empty delta. A name outside that set is still red. Flagging it because a reviewer should not have to derive that from the awk.mkdir -preturns 0 on an existing-but-unwritable directory, so if/home/ld/work/seat-transcriptsever exists owned by another uid, the seed reports success and pi's transcripts silently fail to land. Nothing in the pod creates it root-owned today (theemulatorcontainer mounts only/avd, notwork), so this is robustness rather than a defect;install -d -o 1000 -g 1000 -m 0755in the rootprepare-work(which already holdsCHOWN/FOWNER) would make it self-healing, at the cost of touching a second initContainer.1000:1000). That is no regression versus/tmp, where Node's ownmkdiralso produced 0755, and it must be writable by 1000 to be useful; worth stating only because the content is agent transcripts on a node-local volume (readable by root onprod-wk-01).TMPDIRis inherited by the wholesandboxcontainer, not just pi. The tools scripts are affected too:tools/gits-sbx-emu-proof.shusesmktemp "${TMPDIR:-/tmp}/…"for its normalisation/declaration temp files, andtools/sbx-ci-log.shdoesWORK=$(mktemp -d)— those now land on thehostPathunder the sandbox's 30 GiB budget.gits-sbx-android-install.shalready sets its ownTMPDIRunder$WORK, andgits-sbx-emu-smoke.shsetsFIXTURE_TMPexplicitly, so both are unaffected. This matches the "watch it, don't estimate it" note; it is not free, and/home/ld/workusage is the thing to watch after the roll.scripts/gits-sbx-epic-test.sh:3431now reads "kustomize sorts the keys of a container: keys, so- name:is NOT the first line of" — a garbled rewrite of the original comment.Verdict of record — ld-reviewer — head
a29e9775c1: APPROVEmodel: deepseek-flash
session: s2-r2-2682-6b1f4d92
Seat 2 of 2, round 2 (adversarial delta). Read from primary sources:
git diff 8180f4eb..a29e9775, my ownkubectl kustomize core/manifests/apps/sbx-gitsrenders of head andorigin/live(5b0d7f43), the fourp1_*functions cut out ofscripts/gits-sbx-epic-test.shand driven standalone against 40+ mutations of the REAL render, and a solo harness run on the head.The blocker from round 1 (S2-1) is closed. Verified by mutation, not by prose: the head spine now carries every container.
p1_spine live==p1_spine head, 144 lines bothargs/env/volumeMounts(stripped by design),volumesat pod level;name:, the fullimage:line,resources:,securityContext:(4 container + 1 pod),imagePullPolicy,terminationMessagePath/Policyall present for all four containersmemory: 13Gi -> 20Giinsidesandbox→spine-distinto(was byte-identical)p1_containershead == live, image count spine(4) == census(4), all four names present;p1_census_missingempty on both; initContainers ORDER swapped →spine-distinto contenedores-distintosBlockers
None.
Resolutions
p1_spinesilently drops whole container items"). Verified fixed on the head render. I re-ran the exact round-1 attack (memory: 13Gi -> 20Giinsandbox) and it is nowspine-distinto; I also confirmed structurally thatsandboxandprepare-workare back in the spine with theirimage:lines and their wholesecurityContext:blocks (readOnlyRootFilesystem,capabilities.drop: [ALL],capabilities.add: [CHOWN, FOWNER, DAC_OVERRIDE],runAsUser: 0), which is what the old strip swallowed. The end-of-block rule (first body-line indentation, plus "a line that ends a block is re-tested as the next marker") holds for both layouts kustomize emits (- env:andvolumeMounts:).p1_census_missingbackstop is genuinely independent. It is a second derivation that does not use the strip at all, and the two new failure reasons (contenedores-distintos,spine-perdio-campos) are what make a re-lossy spine impossible to hide: I could not construct a mutation that drops a container field from the spine without tripping at least one of the two. That is the structural answer to round 1, and it is better than a fixed regex would have been.readOnlyRootFilesystem,capabilities,CHOWN,runAsUser, emulator memory, three env-name rows) and extended it — 27 further mutations, all RED:ephemeral-storage,requests, probes ADDED (liveness and readiness — the pod has none, so none can silently lose one),lifecycle,envFrom: secretRef,imagePullPolicy,command,stdin,terminationMessagePath,nodeSelector, tolerations ADDED, affinity ADDED,hostNetwork,shareProcessNamespace,serviceAccountName,automountServiceAccountToken,dnsPolicy,terminationGracePeriodSeconds,restartPolicy,replicas,updateStrategy,serviceName, selectormatchLabels, template labels, a fifth container (spine-imagenes-5/censo-4), and initContainers order.p1_listreally reads every container now. The round-1 bug's signature is gone: an env name added tosandbox, toemulatorand toprepare-work(the harness's own three rows) all surface asenv-anadidos: MUT_ENV; new mountPaths surface asmounts-anadidos. The superset semantics are as documented, and the per-sandbox exception is scoped:[[ $d == sbx-gits ]] && d_extra_env='TMPDIR', sosbx-lab/sbx-sendrastkeep the empty allowance.HOME=/home/ld -> /rootinsandboxandANDROID_AVD_HOMEinemulator: green, by design, becauseenv:is stripped. ForTMPDIRthe value is caught, and caught structurally: §28 parses the manifest with PyYAML and assertssandboxTMPDIR == /home/ld/work/seat-transcripts,emulatorhas none, and the mounted volume for/home/ld/workresolves to ahostPathunder/var/mnt/u-longhorn/*; it also executes node'stmpdir(). So the one value this PR introduces is pinned in one place and the spine does not have to carry values.p1_lab_sts_bad+p1_census_missingover the 901-line render take ~0.38 s per pair, and the H8[8][mut] block runs ~12 of them → ~5 s added to a run that is otherwise ~10 min. My solo run on the head: 470 PASS, 6 FAIL, and the six are byte-for-byte the pre-existing set this PR documents (probe uses an EMPTY pre-existing --dir,manifest[2]6Gi/10Gi,manifest[3],manifest[3b], theT7totals row,T7+R99[10b]generator legs). Zero new reds.H8[8][sbx-gits][censo]and all 12[mut]rows PASS in the run itself. No ordering dependency found in the new code: the mutation heredoc cannot steal the outer loop's stdin (the outer iteration is afor, and the heredoc is scoped to the inner loop);$R8/mut.stsis written and consumed inside the same iteration; the counts are read from FILES withgrep -cprecisely to dodge the SIGPIPE/141 race underset -uo pipefail(a real hazard, correctly avoided);d/d_extra_envare assigned before every use on the green path. The block skips (never falsely fails) when either.stsrender is missing. I did not let the run finish (it was in section 22/28 when I posted), so treat the "zero new reds" as covering sections 1–22 plus the whole of §8 including every new row.Concerns
FORGEJO_API_KEYas asecretKeyRefto theemulatorenv, and mounting/run/secrets/ld/gitinto that same privileged container, both leavep1_lab_sts_badempty (env-anadidos/mounts-anadidosare computed withcommoverp1_list, which unions every container). Same shape for removal: droppingsandbox's/home/ld/workmount is green becauseprepare-workstill carries it. §28 catches the retarget case (and the removal of that mount, via its mapping lookup), so this PR's own subject is double-covered; but "which container holds which name/path" is pinned nowhere, and the delta's own new comment says the name set "really covers every container". A container-scoped comparison — the same shape as the new census,name -> sorted(list)instead of a union — would close it. Pre-existing, not introduced here, so not a blocker.readOnly: trueadded tosandbox's/home/ld/workmount is green everywhere I could find (manifest[5b] pinsreadOnlyonly on the identity mounts). That is the exact write path this PR exists to protect, and "the directory exists" is pinned while "the directory is writable" is not./tmp: mount removal and size are unpinned. Removingsandbox's/tmpmount, or changing thetmpemptyDir'ssizeLimit: 4Gi, is green inp1_*; the4Githe README §1.1 budget depends on is asserted in prose only. Same class:args:is stripped for EVERY container (the strip is not container-scoped), soargs: ["sleep 100000"]added to thesandboxis green inp1_*— the emulator's args are pinned by T7 and the seed's by §28/seed_of_d, the sandbox's by nothing.TMPDIRremain unpinned. As designed (and the round-1 note is honest about it), but the visible remainder is now larger:HOME,GOPROXY,GOTOOLCHAIN,LD_MODEL_GATEWAY_URL,FORGEJO_LEGACY_PANEL_OWNERS,PI_LANDING_IDENTITYcould each be changed with everything green. Worth one deliberate ticket ("pin the values that are security-relevant") rather than a comment.p1_*guard (the spine exits atvolumes:, and only names are compared).hostPath+DirectoryOrCreateare pinned bymanifest[6], the identity/cisecretNames by H4/H2v3 — so the surviving hole is thetmpemptyDir above andsecretNamerotation of a volume whose secret another section does not name.Verdict of record — ld-reviewer — head
a29e9775c1: APPROVEmodel: deepseek-flash
session: sess-2682-r2-s1-9c41d7
Independent review seat 1 of 2, round 2 (delta). Primary sources only:
git diff 8180f4eb a29e9775,git diff origin/live...a29e9775, my ownkubectl kustomizerenders of the head tree and oforigin/live(5b0d7f43), the harness's own helper functions extracted verbatim from the head blob and driven against my render, and my own harness runs of both trees.The delta is the guard and nothing else
git diff --stat 8180f4eb a29e9775=scripts/gits-sbx-epic-test.sh | 292 ++++…, 1 file, +256/−36, exactly as declared. The head still differs fromorigin/livein three files (statefulset.yaml,README.md, the harness), and the render diff ofcore/manifests/apps/sbx-gits, live vs head, is still exactly two hunks, pure addition, zero removals:- name: TMPDIR/value: /home/ld/work/seat-transcriptsin thesandboxenv, and the seed'smkdir -p … || printf 'SEED-WARN: …'. Nothing else in the render moved.1. The spine now carries every container (and the loss is provably not silent)
origin/livespine 144 lines,diffempty.sandbox,emulator,prepare-work,seed-pi-agent.image:lines present:sbx-fleet:6fa150a@sha256:0ee5e97e…×3 andandroid-emulator:230756a@sha256:66430940….prepare-work's rootsecurityContext(runAsUser: 0,capabilities.add: CHOWN/FOWNER/DAC_OVERRIDE,readOnlyRootFilesystem: true) and thesandbox'simage,readOnlyRootFilesystem,capabilities.drop,requests/limits.p1_spineextracted from8180f4ebover my head render yields 90 lines with only 2 of 4 container names (emulator,seed-pi-agent) and 2 of 4 images — byte-for-byte the failure seat 2 measured — while the delta's new independent census (p1_containers+p1_census_missing, which do not exist at8180f4eb: zero occurrences) flags 6 missing lines against that old spine. So the new assertion is not vacuous: it detects the exact defect it was written for. At this headp1_census_missing(now)andp1_census_missing(live)are both empty, andp1_containerscounts 4 images = 4 spine images, with census sectionscontainers/sandbox,containers/emulator,initContainers/prepare-work,initContainers/seed-pi-agent.2. The mutation table bites — executed against my own render
Baseline first (the ceiling):
p1_lab_sts_bad(live, head, '', 'TMPDIR')= empty (green), and with the allowance removed =env-anadidos: TMPDIR. Rows, each applied inside the named container byp1_mutate(never a hand-patched doc) and then re-judged:sandboxmemory 13Gi → 20Gispine-distintosandboximage digest mutatedspine-distinto contenedores-distintossandboxreadOnlyRootFilesystem: true → falsespine-distinto contenedores-distintossandboxcapabilities.drop: - ALL → - NET_ADMINspine-distinto contenedores-distintosprepare-workcapabilities.addminusCHOWNspine-distinto contenedores-distintosprepare-workrunAsUser: 0 → 1000spine-distinto contenedores-distintosemulatormemory 8Gi → 9Gispine-distintosandbox/emulator/prepare-workenv-anadidos: MUT_ENVExtra rows I added because they are the fields the round-1 strip swallowed:
sandboxrequestsmemory 12Gi → 13Gi,sandboximagePullPolicy,sandboxterminationMessagePolicy,prepare-work's command interpreter, a pod-levelnodeSelectorchange, and deleting theemulatoritem outright — all red (spine-distinto, the last one alsocontenedores-distintos mounts-quitados:/avd env-quitados:…). The env-name addition leaves the spine unchanged by design and is caught by the name set; thep1_list envset now covers every container: head 35 names vs live 34, delta exactlyTMPDIR, which is present in the head set.The harness's own table (§10
H8[8][sbx-gits][censo]+ 11[mut]rows) is PASS on this head, and the allowance is scoped:d_extra_envisTMPDIRforsbx-gitsonly,''forsbx-lab, and the otherp1_lab_sts_badcall sites (10b[R99], 11[H11-render], 17[R159]) aresbx-lab/sbx-sendrastonly, so the exception cannot leak.3. Seat 2's original proof now flags
The
sandboxmemory: 13Gi → 20Gimutation that left the round-1 spine byte-identical now changes the spine and yieldsp1_lab_sts_bad=spine-distinto. That is the precise demonstration the round-1 comment asked for, at this head, on the real render.4. Harness solo — zero new reds, zero fixed
I ran each tree solo (see the note below on why not simultaneously):
a29e9775:RESULT: FAIL (5 of 693 checks failed, 6 skipped, 0 pending)origin/live5b0d7f43:RESULT: FAIL (5 of 670 checks failed, 8 skipped, 0 pending)diffclean) and are the pre-existing ones:manifest[2](memory 6Gi/10Gi),manifest[3]andmanifest[3b](image pins), theT7resource totals row, andT7+R99[10b](the generator's per-file legs). The head adds 12 checks (§28's 8, the census/mutation rows, theT7[diff]re-measure) and adds no red — the guard rewrite did not turn the tree red anywhere.T7[diff]and17[R159] (d)are green with this branch's re-measured set (M README.md,M statefulset.yaml), and all of §24 (27 rows) and §28 (8 rows) pass, including the executedos.tmpdir().Resolutions
a29e9775— four container names and fourimage:lines retained,prepare-work's rootsecurityContextandcapabilities.addretained,p1_census_missingempty on both sides, and thesandboxmemory mutation that used to slip through now goes red (spine-distinto).Blockers
None.
Concerns
TMPDIRthe exposure is closed elsewhere — §28 pins the value structurally through PyYAML — so the disclosure is compensated here; for any other already-present variable the value was unguarded before this delta too, so the delta neither widens nor narrows that./tmpmakesH1b's/tmp/h1-objects-<d>-{before,after}.txt, the20/22[R172*]lock smokes and the "EMPTY pre-existing --dir" probe interfere, producing FAIL rows that belong to neither tree (on the head, a lock-release row; on live, anH1b[sbx-lab]object-set row whose renders I verified are identical). Re-running each tree in its own mount namespace with a private/tmpmade both sets identical. The GITS loop runs it solo, so this is a robustness note for anyone running it twice in parallel — worth its own ticket, not this PR.C-1(disk growth against the 30 GiB cap, no rotation, no volume alert) is unaffected by this delta: it is a property of theTMPDIRchange itself, unchanged between8180f4ebanda29e9775, and I did not re-measure it here.