Skip to content

fix(smoke): enforce recorded sandbox identity before smoke-suite teardown - #913

Merged
davidfarah2003 merged 13 commits into
mainfrom
up/884-sandbox-guard
Aug 28, 2026
Merged

fix(smoke): enforce recorded sandbox identity before smoke-suite teardown#913
davidfarah2003 merged 13 commits into
mainfrom
up/884-sandbox-guard

Conversation

@davidfarah2003

@davidfarah2003 davidfarah2003 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

The issue this refers to is about an assumption that was remembered rather than enforced: live smokes call a destructive down inside a sandbox, and nothing checked that the sandbox actually held. Every attempt to enumerate the affected set reached for a naming convention and produced a different wrong answer, including the enumeration in the issue itself. Filename matching and script-name matching each miss a different file, and one file is reachable by neither. So this guards the destructive call rather than the convention that is supposed to keep it away, which is what makes a suite added next month guarded on the day it is written by someone who never read the issue.

  • add a source-only smoke-kit anchor that records the sandbox root, its .cotal ownership marker, COTAL_HOME, and XDG_CONFIG_HOME before any CLI invocation
  • refuse folder-rooted teardown unless the exact guarded spawn options retain those recorded identities
  • require target-addressed teardown to name its space explicitly and verify the canonical sandbox registry record points to the recorded root
  • route smoke CLI teardown calls through the shared guard and add a static source scan that rejects newly introduced unguarded calls

The issue named nine files, while this change guards 21 current suites across five directories. The set was derived by call shape rather than by filename or script name because every naming-based classifier missed a different path. Filename matching missed the two auth suites, script-name matching missed up-tls-routes-live.smoke.ts, and _ps-arm2.smoke.ts is reached by no package.json script at all.

The static scan is deliberately described at its actual boundary: it covers every current file edited here plus a regex over *.smoke.ts. It can still miss a computed verb, a non-.smoke.ts harness or child, and anything placed on its semantic-literal allowlist. It is a ratchet over today's call shapes, not proof that an unguarded teardown is impossible to write. The generic guard separately refuses down web so a target-addressed teardown cannot silently use folder-root identity checks.

smoke:sandbox-guard is in package.json's check chain, positioned immediately after smoke:core-boundary and before the first live entry, and the suite asserts both of those facts about itself so a later edit cannot quietly undo either. It is not inserted into bin/smoke/ci-suites.txt because that file is frozen by position until PR #880, and its round-robin shard walk would re-shard every later suite.

CI does not currently execute this suite, and that is an open item rather than something this change resolves. No workflow invokes pnpm check: grepping .github/workflows/ for it returns nothing, while the same grep for pnpm check:docsbundle matches ci.yml:47, so the absence is real rather than a bad pattern. check is a local developer aggregate. The proof is correct, is exercised by the mutations below, and is reachable by anyone running pnpm check or the script directly, but nothing in CI runs it today. The remedy is one step in ci.yml beside pnpm typecheck, where a broker-free, network-free, roughly one second safety proof belongs, and it is deliberately not in this change because modifying a workflow file needs a credential scope this change's author does not have.

Worth being exact about, because presence in something named like a gate is not evidence that anything runs it, and a proof that never executes is indistinguishable from a passing one in every report. #912 measures the scale of the same problem: 88 of 385 gated suites never execute at main's tip, because a shard stops at its first red.

Verification

  • pnpm smoke:sandbox-guard
  • pnpm smoke:core-boundary
  • pnpm smoke:gate-inventory
  • pnpm typecheck
  • pnpm build
  • pnpm changeset status

Mutation evidence is a committed fixture rather than a description of edits somebody once made by hand: bin/smoke/mutations/sandbox-guard.json holds ten mutations, each naming the assertion it must redden, each pinned to the broker-free pnpm smoke:sandbox-guard. Run it with node scripts/mutation-proof.mjs --config bin/smoke/mutations/sandbox-guard.json. All ten report KILLED on their named cell. Reverting the emptiness check reddens exactly the two empty-space cells and the roll-up that names them, so the fixture is targeting the behaviour rather than catching collateral from a broadly failing suite.

The eighth mutation exists because reviewing the first seven found a gap in them. The guard records four identities and refuses unless all four hold, but only three had a named cell: hardcoding the ownership marker check to true left the suite fully green and the whole registry still reported every mutation killed. The marker guarantee was recorded rather than checked, which is the same fault this change exists to fix, one level up in the evidence. That check now has a cell, and the mutation that used to pass silently reddens it alone. The ninth mutation closes the narrower replacement gap: a marker created after recording has the expected path but foreign identity, and accepting that foreign marker reddens only the named replacement cell. The tenth mutation closes the remaining target-record split: a canonical filename whose document names another space used to satisfy the guard while loadMeshes selected a legacy record for the requested space at a foreign root, and ignoring that document space field reddens only the named cell.

Worth stating as a limit on the method: a full registry pass is evidence about the registry. It says every enumerated mutation is caught, not that the surface is enumerated, so the standing question for a fixture like this one is what no entry in it constructs.

That proof has a boundary worth stating: a killed mutation shows the suite depends on the mutated code, not that a real entry point reaches it.

No live suite was run. The destructive CLI call sites were graded by reading because executing the applicable live suites was prohibited. This leaves the real subprocess teardown acceptance path as a named verification gap rather than implying it was exercised.

Reviewing the guard found three defects in it

Each was a correct fix that left a narrower version of the same gap, and the shape is worth recording because all three lived in the seam between a guard and the parser it was imitating.

The first was position: the guard looked for a subcommand at a fixed argv index while the CLI reads positionals. The second was parser semantics: the guard resolved --space with indexOf, taking the first occurrence, while the CLI's parser is last-wins. That one is the serious member of the set, because a repeated flag could have the guard approve one mesh while down acted on another. The third was truthiness: typeof "" === "string" satisfied a type check, but down treats an empty space as absent and falls through to the current mesh, so --space "" passed a guard whose whole purpose was to require an explicit target.

The fix is to stop imitating the parser and call it. The guard now runs node:util's parseArgs with the same configuration the CLI uses, so last-wins and --space= forms are decided by the same code that decides them at the call site. Delegating to the resolver proper is not available here, because smoke-kit carries a zero-dependency rail.

The comment on the predicate carries the rule that makes it maintainable, and it is an asymmetry rather than an equality: the guard may refuse more values than the CLI honours, but must never accept one the consumer treats as absent. A future edit can then be checked against a stated direction instead of against a reader's memory of which values matter.

Refs #884, and deliberately does not close it. This lands the smoke-side enforcement: the sandbox assumption becomes a checked identity at the destructive call, so a suite that has lost its sandbox refuses rather than reaching the operator's mesh. The product-side ask in that issue remains open, and it is the larger half: a later comment there retracts caller-side marking as sufficient, because a freshly created root has no .cotal marker at the moment up resolves, so the walk climbs to the nearest marked ancestor. Marking protects a test suite, which can create its marker before its first CLI call, and cannot protect up, which is the verb that creates the marker. Removing fail-open from the verbs themselves, up first and then down, is not attempted here.

{ cwd: root, env: { ...env, [hook]: "1" }, encoding: "utf8", timeout: 240_000 });
const crashOptions = { cwd: root, env: { ...env, [hook]: "1" }, encoding: "utf8" as const, timeout: 240_000 };
assertSmokeSandboxDown(sandbox, ["down", "--preserve-state"], crashOptions);
const crashed = spawnSync(tsx, [cliPath, "down", "--preserve-state"], crashOptions);
@davidfarah2003

Copy link
Copy Markdown
Contributor Author

Verdict: APPROVE WITH NITS at head dc9b38b.

I reviewed this without prior context: read issue #884 in its own terms, then the PR body and the full diff, then verified the load-bearing claims against the tree.

What I checked:

  • The head resolves independently: origin/up/884-sandbox-guard is dc9b38b, verified as a commit with a positive control on origin/main and a negative control on an invented sha.
  • pnpm smoke:sandbox-guard passes on the head and pnpm typecheck is clean (exit 0).
  • The guard is fail-closed where it matters: a missing anchor, a missing cwd, a missing or foreign COTAL_HOME or XDG_CONFIG_HOME, or a deleted .cotal marker each refuse the down before the spawn. The two auth suites the issue flagged as under-sandboxed now set XDG_CONFIG_HOME, and the guard makes an absent value an error rather than a skip, which is what the issue's constraints demanded.
  • The target guard reads the canonical registry record: meshes/space.<hex>.json is exactly what the workspace registry writes (spaceSegment/spaceKey hex), the record carries a root field, and down web --space X is real CLI semantics in down.ts. Only the web component is rootedAt: "target", so routing down manager through the folder-rooted checks is correct.
  • The static scan's allowlist is genuinely semantic (a mesh event "down" in mesh-wait, a lang free("down") in engine), not a CLI call. A repo-wide hunt for quoted "down" in smoke files found no unguarded site outside those two.
  • All down call sites in the suites the issue called out (down-manifest-usermode, user-auth-launch, _ps-arm2) route through the guarded helper, and the 21-suite / five-directory count matches.
  • The CI claims are accurate: pnpm check appears in no workflow (only check:docsbundle at ci.yml:47), the suite is absent from ci-suites.txt, and the changeset is a valid empty changeset (changeset status reports no bumps).

Nits:

  1. The generic guard refuses down web only when web is args[1]. Node's parseArgs collects positionals regardless of flag order, so down --space X web parses the same as down web --space X and would take the folder-rooted path, skipping the registry verification the target guard exists to do. The folder checks still pin COTAL_HOME so the sandbox home is not escaped, but it weakens the exact defense the web refusal was added for. Consider checking the component name anywhere in the positionals, not just args[1].
  2. The guard suite itself is not executed by CI (disclosed in the body). The primary guarantee still holds, because the guarded live suites call the guard whenever CI runs them, but the ratchet against a newly written unguarded down is local-only until the ci.yml step lands. Worth a tracked follow-up so it does not evaporate.

Neither rises to a block: the change solves the issue as stated (the remembered sandbox assumption becomes an enforced one at the destructive call), covers the full derived set rather than the issue's undercount, and is honest about its own boundaries, including the subprocess teardown path it could not execute.

@davidfarah2003

Copy link
Copy Markdown
Contributor Author

APPROVE WITH NITS at head dc9b38b50a7d84315ce3197c0d3a7fae05e02a8a (resolved from up/884-sandbox-guard).

I read this as a fresh reviewer, without the issue's conclusions in front of me, and traced the guard against the actual resolution code. What holds up:

  • The identity model is the right shape. recordSmokeSandbox captures physical path, dev and ino of the root, its .cotal marker, COTAL_HOME and XDG_CONFIG_HOME, and creates the marker before any CLI call. That marker is what terminates findCotalRoot inside the sandbox, so containment no longer depends on where TMPDIR points or on the walk's fail-open fallback.
  • The guard's assumptions match the product code. down web --space <name> is the documented form in down.ts, the registry record is meshes/space.<hex>.json under COTAL_HOME (exactly what meshFileName/spaceKey produce), and MeshEntry.root is absolute. I checked each suite's recorded anchor against its actual spawn options: cwd and env line up, no down fires before up, no teardown runs from a foreign cwd, and no assertion depends on .cotal being absent.
  • It runs. pnpm smoke:sandbox-guard passes on this tree, so the static scan is green over the real repo and the negative cases (foreign root, foreign COTAL_HOME, missing env, missing anchor, down web without --space) are asserted directly. pnpm smoke:core-boundary and pnpm typecheck pass, and pnpm changeset status is clean.
  • The body is honest about its own boundaries. That is rare and valuable.

Nits, none blocking:

  1. The static scan runs nowhere automated. CI never runs pnpm check, the suite is not in ci-suites.txt, and no workflow step invokes it, so the ratchet that makes a future unguarded down unmergeable executes only when someone runs the local check chain. The remedy the body itself names, one pnpm smoke:sandbox-guard step beside pnpm typecheck in ci.yml, is a normal PR edit; the credential-scope reason for leaving it out is not a real blocker, a workflow change ships in the same PR like any other change. The identity guard is not dead weight meanwhile: CI runs up-stack, up-manifest, ext and dogfood live, so the guarded teardown paths do execute there.
  2. "Closes live smokes call down on an unenforced sandbox assumption: a broken sandbox tears down the operator's mesh and still reports a pass #884" is broader than what this delivers. The issue's last comment retracts the caller-side marking approach as a mitigation and asks for the verbs themselves to refuse a root they were not given, with up's teardown half first. This change is exactly the caller-side mitigation, done well, and the smoke scope is defensible: the title and original body are about the smokes. But closing the issue on it loses the product-side finding unless the issue is amended to the narrower scope first.
  3. The refusal message prints the verdicts but not the one belief the failure will have been built on: that COTAL_HOME governs folder-rooted down. One sentence in the throw would save the next debugger a false lead.

What I checked that would have caught a problem: the registry filename and --space form behind the target guard, per-suite recorded-versus-spawn identity, teardown ordering, .cotal-presence assertions, the two safe suites actually executed, typecheck, changeset status, and the CI claims in the body. All verified against head dc9b38b.

@davidfarah2003

Copy link
Copy Markdown
Contributor Author

BLOCK on dc9b38b50a7d84315ce3197c0d3a7fae05e02a8a (resolved from origin/up/884-sandbox-guard; git cat-file -t accepts this commit and a known ancestor, and rejects an invented sha).

This change hardens smoke-suite teardown. It does not close #884 as that issue now stands.

The later comments on #884 replaced the original helper-shaped request. They locate the defect in one resolver serving both a creating verb and a destroying verb, and failing open:

if (parent === dir) return resolve(start);

down still builds folder context from cotalRoot() / findCotalRoot(cwd). up still pins through the same walk: ensureRootForSpace only creates a local .cotal when the ancestor hosts a different space, so a nested unmarked directory of the same space stays captured. The manager still defaults an omitted spawn cwd to workspaceRoot. None of those paths are in this diff.

Concrete sequence that still takes a live stack down after this lands:

  1. A process is started with cwd inside a live mesh tree and without its own .cotal marker. The default private-state layout is exactly that: <mesh>/.cotal/jcode/<id>/ walks up through .cotal and stops on the mesh marker. A spawn with no --cwd is even simpler: cwd is the mesh root.
  2. That process runs bare cotal down, or cotal up from a fresh nested directory that does not yet own a marker (the creating verb is the one that cannot be protected by marking, because the marker does not exist at resolve time).
  3. findCotalRoot returns the live mesh root. Folder-rooted down reads that root's pidfiles and SIGTERMs broker, manager, delivery, and web. The smoke-kit guard never runs, because it is not on the CLI path.

The suite work is real and, on its own terms, carefully fail-closed: recorded root / marker / COTAL_HOME / XDG_CONFIG_HOME identities, no absent-env pass, down web cannot silently take the folder check, cleanup paths in the edited files now go through the helper, and the set was derived from call shape rather than from a filename convention. I ran pnpm smoke:sandbox-guard (PASS) and pnpm smoke:core-boundary (PASS). The static scan is a ratchet over *.smoke.ts literals, which the PR already bounds honestly.

Closing #884 with a harness-only change would treat the demonstrated product failure as solved. Keep the smoke guard. Do not close the issue until up/down refuse a root they were not given, or until a follow-up is filed for that remaining sequence and #884 is retargeted.

Named gap: live subprocess teardown was not executed (prohibited here). CI still does not run smoke:sandbox-guard; that is disclosed and is not the block.

@davidfarah2003

Copy link
Copy Markdown
Contributor Author

BLOCK at head dc9b38b50a7d84315ce3197c0d3a7fae05e02a8a.

The target-addressed guard does not parse the command the way the CLI does, so it can approve one mesh while down acts on another.

Concrete failure sequence:

  1. A guarded smoke helper receives ['down', 'web', '--space', 'sandbox', '--space', 'operator'] with a valid sandbox anchor and a canonical sandbox registry record pointing at the recorded root.
  2. assertSmokeSandboxTargetDown uses args.indexOf('--space'), sees the first sandbox, verifies that record, and returns successfully.
  3. The real CLI uses parseCommandArgs, which delegates to Node parseArgs. For a non-multiple string flag, repeated --space is last-wins here, so values.space is operator.
  4. down.ts sees the web component as rootedAt: 'target', calls resolveMeshTarget(..., { space: 'operator' }), and uses that registry entry's root for the web pidfile and stop.
  5. The destructive subprocess therefore targets the operator record even though the guard passed the sandbox record.

I reproduced the mismatch broker-free against the PR implementation and the exact parser: the guard returned, while the parsed CLI space was operator. There is a second form of the same contract gap: flags may precede positionals, so down --space operator web parses as target-addressed, but both the shared guard and dogfood-live classify target addressing only when args[1] === 'web' and apply folder-root checks instead. Valid --space=sandbox syntax is also rejected by the target guard even though the CLI accepts it.

The proof suite checks only down web --space value, so none of these parser-equivalent forms is behind the claimed target-addressed guarantee. The fix should derive effective positionals and flags with the command parser's semantics, reject ambiguous duplicate flags if that is the intended CLI contract, and test alternate flag order, equals syntax, and duplicates.

Other requested checks held: bare down is correctly treated as folder-rooted, smoke-kit remains private and source-only with no dist, no shipped src/** imports it, pnpm smoke:sandbox-guard, pnpm smoke:core-boundary, pnpm typecheck, and pnpm changeset status passed. The PR body's disclosure that CI does not execute smoke:sandbox-guard is accurate. The real subprocess teardown path remains the stated verification gap.

BLOCK

@davidfarah2003

Copy link
Copy Markdown
Contributor Author

Follow-up on dc9b38b50a7d84315ce3197c0d3a7fae05e02a8a. Verdict is now APPROVE WITH NITS.

I blocked because this PR closed #884 while leaving the product-side failure sequence in place. Re-fetched just now:

That is the second half of the condition I named. The product walk (findCotalRoot fail-open, ensureRootForSpace only pinning when the ancestor hosts a different space, omitted spawn cwd defaulting to the mesh root) is still real and still unfixed. It is now correctly left on #884 rather than treated as solved by a harness change. That was the block. It no longer holds.

Keep the smoke-side work. It is fail-closed on recorded identities, covers the call-shape set rather than a filename convention, and pnpm smoke:sandbox-guard plus pnpm smoke:core-boundary both passed here.

Nits, neither of which reopens the block:

  • Target-addressed routing still keys on args[1] === "web". The CLI takes web from positionals and --space from flags, so down --space X web is a target-addressed stop that this head classifies as folder-rooted. No current suite uses that argv order. dogfood-live uses ["down", "web", "--space", SPACE], which matches. The hole is in the new guard, not in today's call sites.
  • The opening sentence still says "The issue this closes" after the close was removed. Cosmetic, but it is the same class of stale-body problem.

Named gap unchanged: live subprocess teardown was not executed. CI still does not run smoke:sandbox-guard. Both were already disclosed.

@davidfarah2003

Copy link
Copy Markdown
Contributor Author

Follow-up, still dc9b38b50a7d84315ce3197c0d3a7fae05e02a8a. Still APPROVE WITH NITS.

Re-fetched the PR just now: body still Refs #884 and does not close it, closingIssuesReferences is empty, #884 is OPEN. That remains what lifted the block.

I independently reproduced the duplicate --space split on this head, without running the CLI:

parseArgs({ args: ["web", "--space", "sandbox", "--space", "operator"],
            options: { space: { type: "string" } }, allowPositionals: true, strict: true })

Node returns { values: { space: "operator" }, positionals: ["web"] }. The guard uses args.indexOf("--space"), which is the first occurrence. So ["down", "web", "--space", "sandbox", "--space", "operator"] can pass the sandbox record check and still have down act on the last --space.

That is a real hole in the new guard, and it is the same class as the original issue (a check that can be green while the destructive call aims elsewhere). It is not a current call site: the one down web smoke uses a single --space. I am not raising the verdict back to BLOCK for an argv no suite writes today. It does make the earlier args[1] === "web" nit stronger: this helper should not re-parse argv by index. FlagSpec even claims a repeated non-multiple flag is a usage error and that last-wins is never silent. Node does not do that; last-wins is silent today. Using the real parser, or refusing a second --space outright, is the fix.

Product fail-open (findCotalRoot, same-space unmarked nested dirs, omitted spawn cwd) stays on #884, which this PR no longer claims to close.

@davidfarah2003

Copy link
Copy Markdown
Contributor Author

Delta re-grade of cc56726e43b7e9ba85630eff8134269cf8d281b7 (resolved from origin/up/884-sandbox-guard; git cat-file -t accepts this commit and the prior head, and rejects an invented sha). Still APPROVE WITH NITS.

git diff dc9b38b50a7d84315ce3197c0d3a7fae05e02a8a..cc56726e43b7e9ba85630eff8134269cf8d281b7 is the parser change I asked for. Target routing no longer keys on args[1]. Space selection no longer keys on indexOf("--space"). Both now come from node:util parseArgs with the same flag table as down, allowPositionals: true, strict: true.

That closes both nits from the previous head:

  • ["down", "--space", X, "web"] is classified as target-addressed. The suite now asserts the generic guard refuses it and the target guard accepts it.
  • Duplicate --space is last-wins, matching Node rather than the documented FlagSpec contract. ["down", "web", "--space", sandbox, "--space", operator] now fails the target guard because values.space is operator. I reproduced that against the extracted file, without running the CLI.

Unrecognized flags fail closed (cannot classify arguments), which is the right direction if the copied flag table drifts: a missing flag refuses the spawn rather than letting it through unclassified.

Remaining nit, not a block: downOptions is a second copy of the CLI flag list. Drift fails closed today. It is still a copy. The suite covers flag-before-target and unknown flags; I exercised duplicate --space last-wins against the extracted file, not as a named cell in the suite.

#884 remains OPEN and this PR still does not close it. Product fail-open is unchanged by the delta.

Named gap: this tree is still at dc9b38b5, so pnpm smoke:sandbox-guard was not re-run against the new head. Classification was exercised on the extracted cc56726e file instead.

The prior proof covered only one canonical argv form, leaving parser-equivalent forms outside the claimed guarantee.
@davidfarah2003

Copy link
Copy Markdown
Contributor Author

APPROVE WITH NITS at head 981120072b9fc5877fb7c830d80af4787bdee8ec.

Delta re-grade from dc9b38b50a7d84315ce3197c0d3a7fae05e02a8a:

The parser fix closes my block. parseSmokeDown uses Node parseArgs with the same six flags, short -f, strict: true, and allowPositionals: true as the registered down command. assertSmokeSandboxTargetDown now derives the effective space from parsed.values.space, so there is no separate caller-supplied space that can disagree with the subprocess argv. The dogfood helper passes the same args array to the guard and spawn.

I exercised the canonical form, flags before web, --space=value, repeated --space, unknown flags, and mixed target/folder component forms against the canonical CLI parser and the current guard. Repeated space follows the CLI's actual last-wins behavior and verifies that resulting registry record. Unknown syntax fails closed. Mixed component calls are refused by the target helper.

The regression proof is load-bearing now. The committed suite has named cells for flag order, equals syntax, and last-wins duplicates. Restoring first-occurrence target binding while preserving equals support makes the duplicate cell fail on target guard uses the CLI parser's last space value; the unmodified head passes.

Package boundaries still hold: smoke-kit remains private, source-only, no-emit, and has no dist; no shipped src/** imports it. The three-file delta passes direct typecheck and git diff --check, and adds no fallback.

Nits and named gaps:

  • The copied flag table can drift if the CLI adds or changes a down flag. Today it matches exactly, and strict parsing fails closed rather than degrading.
  • CI still does not execute smoke:sandbox-guard, as the PR body discloses.
  • The real subprocess teardown acceptance path was not run because live stack execution is prohibited here.

APPROVE WITH NITS

@davidfarah2003

Copy link
Copy Markdown
Contributor Author

Delta re-grade of 981120072b9fc5877fb7c830d80af4787bdee8ec (resolved from origin/up/884-sandbox-guard; git cat-file -t accepts this commit and cc56726e, and rejects an invented sha). APPROVE.

git diff cc56726e43b7e9ba85630eff8134269cf8d281b7..981120072b9fc5877fb7c830d80af4787bdee8ec drops the caller-declared space argument. The target guard now takes S only from parsed.values.space. That widens the accepted set to calls where the old declared label would have disagreed with the parser.

Does the newly accepted set contain a call that acts on a root other than the anchor's? No.

assertRecordedSandboxDown still runs first. It refuses unless spawn cwd, marker, COTAL_HOME, and XDG_CONFIG_HOME match the recorded identities. Only then does the guard read join(expected.cotalHome.path, "meshes", space.<hex(S)>.json) and require that record's root to be the recorded sandbox root. A foreign COTAL_HOME therefore cannot supply the record: I reproduced a mismatched home throwing the identity error, not the record-read error.

So a newly accepted call is one where S is whatever parseArgs last-wins to, and that S's record under the already-proven sandbox home still points at the sandbox root. The CLI, on the same argv, also last-wins to that S. The discarded check was a label comparison, not a second root check.

The duplicate --space cell is now in the suite, with the first space's record pointed at a foreign checkout and the last space's record pointed at the sandbox. Equals-form --space= is covered too.

downOptions remains a copy of the six-flag static literal. Drift still fails closed. Not a block.

#884 remains OPEN. Product fail-open is unchanged by the delta.

Named gap: this tree is still at dc9b38b5, so pnpm smoke:sandbox-guard was not re-run against 98112007. Classification and identity order were exercised on the extracted file.

@davidfarah2003

Copy link
Copy Markdown
Contributor Author

Verdict: BLOCK at 981120072b9fc5877fb7c830d80af4787bdee8ec

Resolved first-hand after git fetch origin: origin/up/884-sandbox-guard is 981120072b9fc5877fb7c830d80af4787bdee8ec (git cat-file -t = commit). Positive control dc9b38b50a7d84315ce3197c0d3a7fae05e02a8a is a commit. An invented sha is refused. Tree HEAD matches that remote sha. Two hops from dc9b38b5: cc56726e then 98112007.

The claim under test: the widened accepted set of assertSmokeSandboxTargetDown contains no call that acts on a root other than the anchor's.

old: declared == parsed == S  AND  record(S).root == anchor.root
new: parsed is a string       AND  record(S).root == anchor.root

That implication is false. A member of the new set can still make cotal down web act on a different root.

Failure sequence (measured, no cotal down spawn)

down.ts still treats an empty --space as absent:

resolveMeshTarget(process.cwd(), values.space ? { space: values.space } : {})

"" is a string, so the target guard now accepts it. It is also falsy, so the CLI drops it and follows current-mesh.

Reproduced against the functions at this sha (temp dirs only, no broker, no CLI verb):

  1. Anchored COTAL_HOME/meshes/space..json (UTF-8 hex of "") with { root: <sandbox> }.
  2. A second record target whose root is a foreign checkout, and current-mesh set to target.
  3. assertSmokeSandboxTargetDown(anchor, ["down", "web", "--space", ""], options) returns.
  4. The equals form ["down", "web", "--space="] returns too. parseArgs and parseCommandArgs both yield space: "".
  5. The same flags down.ts would pass ({}, because values.space is falsy) go to resolveMeshTarget. That returns { root: <foreign checkout>, space: "target", source: "current" }.

So the guard's S is "" and record("").root is the sandbox, which is why the call is in the new accepted set. The process that actually runs then stops target-addressed web under the current mesh, which is not the anchor.

Before 98112007, the caller had to pass a declared label and the guard required parsed.values.space === declared. A live caller declaring SPACE = "dogfood-custom" could not accept "". Removing that parameter is what admits this call.

pnpm smoke:sandbox-guard still passes. It never plants space..json, never uses --space="", and never checks this falsy/current split.

Routes that did not break the root claim

  • Hex encoding vs traversal. Empty, ., .., ../x, /etc/passwd, NUL, newlines, long ASCII, and unpaired surrogates all encode to [0-9a-f] and stay under the anchored COTAL_HOME/meshes/ as space.<hex>.json. Empty becomes space..json (still in that directory). Hex does neutralize path escape.
  • Parser disagreement. Guard parseArgs and CLI parseCommandArgs agreed on every argv tried, including flag-before-target, --space=value, and last-wins on a repeated non-multiple --space.
  • Identity before the record read. A swapped COTAL_HOME is refused by assertRecordedSandboxDown before readFileSync of the space record.

Unpaired surrogates collapse through Buffer.from(..., "utf8") to U+FFFD, so the guard can accept "\uD800" by reading the U+FFFD file. The CLI lookup then misses. That is a false accept that errors, not a foreign teardown.

What would close it

Keep the parsed-string check, but refuse a space the CLI will not honor as an explicit mesh name. Empty string is the one that is a string for parseArgs and absent for resolveMeshTarget. A non-empty typeof space === "string" is not enough.

@davidfarah2003

Copy link
Copy Markdown
Contributor Author

BLOCK at head 981120072b9fc5877fb7c830d80af4787bdee8ec, independently resolved from origin/up/884-sandbox-guard and verified as a commit with positive and negative object controls.

The newly accepted set contains a call that acts on a root other than the anchor's: an empty parsed space.

Concrete failure sequence:

  1. The guarded argv is ["down", "web", "--space=operator", "--space="], and the caller's removed declared-space argument is operator.
  2. Node parseArgs is last-wins, so parsed.values.space === "". The old predicate rejects because parsed does not equal declared. The new typeof space === "string" predicate accepts the empty string.
  3. If space..json under the anchored COTAL_HOME contains the anchor root, the guard reads that file and returns. This malformed record is enough for the guard even though the workspace registry rejects empty space names and cannot produce or load it canonically.
  4. The actual down path does not treat "" as an explicit space. Both if (values.space) and values.space ? { space: values.space } : {} take the absent-space branch, so resolveMeshTarget may select current.
  5. With current naming an operator mesh, down web resolves that mesh's foreign root and addresses its web pidfile. The guard passed while the command targets a root other than the anchor's.

I reproduced this broker-free against the current guard, Node parser, and workspace resolver: the guard accepted --space=, while the resolver selected source: "current" and returned a foreign root. pnpm smoke:sandbox-guard passes, so the committed coverage does not catch this case.

assertRecordedSandboxDown running first is still load-bearing and correct: it anchors the record read to the recorded COTAL_HOME. It does not fix this mismatch because the guard and down disagree about whether the parsed empty string names a space at all.

Require the effective space to satisfy the same predicate down uses, at minimum a non-empty string, before building the record path. Add a regression cell for a repeated flag whose last value is empty.

@davidfarah2003

Copy link
Copy Markdown
Contributor Author

Follow-up evidence at the same head 981120072b9fc5877fb7c830d80af4787bdee8ec. Verdict remains BLOCK.

I reran the counterexample through the actual registered command surface rather than a copied parser configuration:

  • Imported the built core package entry, self-registered the CLI commands, resolved the registered down command, and called public parseCommandArgs on ["web", "--space=operator", "--space="].
  • Observed positionals: ["web"] and values.space: "". This is the effective value the real dispatcher hands to down.
  • The current target guard accepted the same argv when the anchored home contained space..json with the anchor root.
  • The exact production selection branch in down.ts, values.space ? { space: values.space } : {}, therefore produced {}. The built workspace resolveMeshTarget selected source: "current", space operator, and the foreign operator root.

A broader matrix through the same registered parser, current guard, and production resolver confirmed canonical order, flag-before-target, equals syntax, and repeated last-wins to a non-empty sandbox space stay pinned. A repeated last foreign space is refused. Missing and unknown flags are refused. Only repeated last-wins to empty produced guard: accepted plus resolverSource: current and a foreign root.

Observed project checks:

  • pnpm typecheck: pass across the repository build and recursive typechecks.
  • pnpm smoke:cli-kernel: pass through the public command parser suite.
  • pnpm smoke:sandbox-guard: pass, confirming the committed focused coverage misses this edge.
  • git diff --check: pass, and the worktree remained clean.

The actual destructive down process-stop path was not executed because this review explicitly prohibits every down invocation. That final acceptance step is blocked by the environment rule. The parser, guard, truthiness branch, and target resolver were exercised through production package boundaries, and they already establish the foreign-root selection before any stop occurs.

The result is sound for non-empty parsed spaces, but it is not sound for every string. The guard must require the same effective-space predicate as down, at minimum a non-empty string, and cover a repeated flag whose last value is empty.

@davidfarah2003

Copy link
Copy Markdown
Contributor Author

Verdict: APPROVE at 9d7446764f90198b4614ad2e9b87d33f70d5a51f

Resolved first-hand after git fetch origin: origin/up/884-sandbox-guard is 9d7446764f90198b4614ad2e9b87d33f70d5a51f (git cat-file -t = commit). Positive control 981120072b9fc5877fb7c830d80af4787bdee8ec is a commit. An invented sha is refused. Delta from that previous head is one commit: fix: refuse empty sandbox target space. Graded from the object. The worktree was not switched.

The claim under test: the target guard may refuse more values than cotal down honours, never fewer, and the new mutation fixture's expectRed cells redden for the reason they name.

Emptiness vs honoured-ness

down.ts still does values.space ? { space: values.space } : {}. On Node's parseArgs domain for a non-multiple string flag, values.space is string | undefined. The only falsy string is "". So:

typeof space !== "string" || space === ""

is the same predicate as !space for every value that parser can produce.

Measured against the functions at this sha (temp dirs, no broker, no CLI verb):

argv parsed CLI honours guard
missing --space undefined no, follows current refuse
--space "" "" no, follows current refuse
--space= "" no refuse
--space=operator --space= "" no refuse
--space= --space=operator "operator" yes accept (sandbox record)
" ", tab, newline, ZWSP, "0", ".", "..", NUL truthy string yes accept when that record's root is the sandbox

Zero cases where the guard accepted a value the CLI would treat as absent. The previous teardown ("" accepted, CLI dropped the flag, current-mesh selected a foreign root) is closed on both --space "" and last-wins --space=.

Whitespace-only is truthy on both sides. That is not a remaining gap: the CLI looks up a mesh actually named " ", it does not fall through to current. If that record is missing, the guard is stricter (cannot establish identity) rather than looser.

Lower bound

The one extra refusal seen was a truthy --space target whose planted record pointed at a foreign checkout. The guard refused on root identity. The CLI would have honoured the name and resolved that foreign root. That is the guard doing its job, not a looser acceptance test.

Mutation fixture

Each of M1, M2, M3, M5, M6, M7 has a unique find string at this sha. pnpm smoke:sandbox-guard loads @cotal-ai/smoke-kit from src/, so those source mutations are the copy the suite runs.

Applied on extracted copies of this sha, not by mutating the parked worktree:

  • M1 named cell printed ✗ foreign sandbox root is refused by identity: Missing expected exception.
  • M2 named cell printed Missing expected exception: generic guard refuses flag-before-target down web
  • M3 named cell printed ✗ target guard uses the CLI parser's last space value: Got unwanted exception. (equals-form also went red, because first---space scanning cannot see --space=value; the named last-wins cell still changed state)
  • M5 named cell printed check reaches smoke:sandbox-guard
  • M6 named cell printed check reaches smoke:sandbox-guard before its first environment-dependent live suite (M6_FIND_EXACT holds in package.json's check script; first live step remains pnpm smoke:spawn-from-anywhere:live)
  • M7 named cell printed ✗ target guard refuses both empty-space argv forms because both child empty-argv cells missed their expected exception. That is the mutated emptiness check, not an earlier unrelated failure.

M4 was not applied to the parked tree. The suite's own scan regex was run against the M4 replacement in dogfood-live.smoke.ts. After the rewrite, spawnSync(..., "down", ...) in the finally block is a raw down spawn whose preceding five lines do not contain assertSmokeSandboxDown, which is exactly the raw down spawn is not immediately guarded row.

M2, M4, M5, and M6 still fail via assert.throws / assert.ok / assert.notEqual messages rather than the new refuses/permits printers, so those labels do not appear on a green run. That is throw-only evidence. It is enough for a named red here. It is weaker instrumentation than the empty-space aggregate, which prints on pass and on fail.

What was tried that did not break it

Hex encoding still keeps empty, .., absolute, and NUL names inside the anchored meshes/ directory. Parser agreement still holds, including last-wins. Identity still runs before the record read. The empty-name record space..json is now refused rather than verified.

No teardown path remains in this delta where a parsed space the CLI treats as absent is accepted by the guard.

@davidfarah2003

Copy link
Copy Markdown
Contributor Author

BLOCK (tests and mutation evidence)

Head graded: 9d74467 (resolved myself from origin/up/884-sandbox-guard after fetch; cat-file reports commit; a one-character-mutated sha fails cat-file as the negative control).

What I re-ran by hand, in a clean tree at that head, each restored byte-exact afterward (diff -q against a copy taken before mutating) with the suite green again before the next one:

  1. M1 identity comparison replaced with a constant true. Red, named: "foreign sandbox root is refused by identity". The foreign root directory exists and is a real directory, so this red comes from identity differing, not from absence. Killed.
  2. M2 web-target routing switched from parsed positionals to args[1]. Red, named: "generic guard refuses flag-before-target down web". Killed.
  3. M3 space taken from the first separate --space token instead of parser last-wins. Red, named: "target guard uses the CLI parser's last space value" (equals-form cell reddens too). Killed.
  4. M4 dogfood finally-block teardown reverted to a raw unguarded spawnSync down. Red, the coverage scan names the exact call site. Killed.
  5. M5 proof removed from the check chain. Red, named: "check reaches smoke:sandbox-guard". Killed.
  6. M6 proof kept in check but moved behind the first live entry. Red, named: "check reaches smoke:sandbox-guard before its first environment-dependent live suite". Killed.
  7. M7 empty-string space accepted (the CLI treats an empty parsed space as absent). Red, both named empty-space cells plus the aggregate. True exit code 1 (checked without pipe truncation). Killed.

I also ran the shipped registry end to end (node scripts/mutation-proof.mjs --config bin/smoke/mutations/sandbox-guard.json) and it reports all 7 KILLED, exit 0, with the same named reds.

Why BLOCK: one surviving mutation, in the exact class this campaign has hit before (a guarantee that reads as verified because it is written down, with no check behind it).

The guard records and checks a fourth identity: the sandbox root's .cotal ownership marker. The code's own doc comment calls this directory load-bearing, because it is the thing that stops a bare down from walking upward into an operator checkout, and markerHeld participates in the same combined refusal as the root, COTAL_HOME, and XDG_CONFIG_HOME identities. But no named cell and no registry mutation covers it. Concrete failure sequence: replace the line

const markerHeld = markerIdentity === "same";

with

const markerHeld = true;

and run pnpm smoke:sandbox-guard. Exit 0, "sandbox guard smoke: PASS", every cell green. Run the full shipped mutation registry too: still all KILLED, exit 0. The marker identity guarantee is silently disabled and nothing notices, even though the other three identities each have a dedicated refusal cell and the root identity has its own registry mutation. A later refactor that deletes the marker check as dead code would ship green the same way.

The ask is small and in the suite's existing style: one cell that removes or replaces the marker directory between anchor and guard call and asserts the refusal by name (the error message already carries marker=missing or marker=foreign), plus one registry mutation (markerHeld hardcoded true) expecting that cell red. Root, COTAL_HOME, and XDG_CONFIG_HOME each have exactly this shape; the marker is the only recorded identity without it.

What I checked that would have caught a problem elsewhere: coverage and correctness are proven separately (the static scan for call-site coverage, the refusal cells for semantics, and the check-chain assertions for gating are distinct, and mutations 4, 5, and 6 each redden only their own layer); the guard's parseArgs mirror matches the CLI's declared down options including the -f short and strict mode, so last-wins and equals-form agreement is real rather than asserted; the two semanticDownOnly allowlist entries use "down" only as an event name and never spawn it; the changeset is empty-form and pnpm changeset status reports no packages to bump, which matches the fact that every changed file is private test infrastructure.

The live subprocess teardown acceptance path remains a named gap, as the description already states; this block is not about that.

@davidfarah2003

Copy link
Copy Markdown
Contributor Author

APPROVE at head 9d7446764f90198b4614ad2e9b87d33f70d5a51f, independently resolved from origin/up/884-sandbox-guard and verified as a commit with positive and negative object controls.

The empty-space regression is fixed. I drove the real public dispatcher with the registered down grammar, then the published guard and production target resolver:

  • Separate empty and repeated-last-empty forms both produce values.space === "". The guard now refuses both before reading an empty-space record. Without the guard, the unchanged consumer truthiness branch produces {} and resolves current, which in the probe was a foreign root.
  • A canonical non-empty value and an empty-first/non-empty-last value are accepted, passed as explicit { space }, and resolve the sandbox record.
  • A whitespace-only value is also accepted and passed explicitly. That is correct for this lower bound because down treats it as truthy. The guard is matching what the consumer honors, not applying an accidental trim-based rule.
  • A repeated-last foreign non-empty value is still refused by the record-root comparison.

The lower-bound framing holds: the guard rejects the consumer-absent string and may remain stricter elsewhere, but it does not accept a parsed value that this consumer drops.

I independently ran the committed seven-mutation fixture against an exact archive of this head. All seven were KILLED on their named cells. I also reapplied each mutation separately and compared its transcript with the green baseline:

  • M1 prints foreign sandbox root is refused by identity: Missing expected exception.
  • M2 prints Missing expected exception: generic guard refuses flag-before-target down web.
  • M3 directly reddens target guard uses the CLI parser's last space value. It also breaks equals syntax, but the named repeated-value cell independently fails.
  • M4 reports the introduced dogfood line as raw down spawn is not immediately guarded.
  • M5 and M6 directly fail the absent and late check-gate assertions they name.
  • M7 makes both direct empty-space cells fail, then reddens the named aggregate target guard refuses both empty-space argv forms.

So the expectRed values are causal, not incidental text from unrelated failures.

Observed checks on the exact published content:

  • focused sandbox guard smoke: pass with all seven named progress cells
  • smoke-kit source typecheck: pass
  • seven-mutation fixture: 7 of 7 KILLED
  • every scratch-mutated file restored byte-for-byte to its published Git blob
  • git diff --check: pass, original worktree clean
  • PR object and remote branch still pin this exact head

Named gap: the actual destructive down stop was not executed because this review prohibits every down invocation. Public dispatch, guard behavior, and foreign-versus-sandbox target resolution were exercised through production boundaries before the stop point.

APPROVE

@davidfarah2003

Copy link
Copy Markdown
Contributor Author

Follow-up: executed at 9d7446764f90198b4614ad2e9b87d33f70d5a51f in a private worktree

The previous comment graded the object from a checkout still at 98112007. Extracted copies of the new files were run there. That is not the same as executing the suite and fixture with @cotal-ai/smoke-kit resolved from this sha.

This follow-up is the executable grade. A private detached worktree was added at 9d7446764f90198b4614ad2e9b87d33f70d5a51f, pnpm install --frozen-lockfile was run there, then:

pnpm smoke:sandbox-guard
# PASS, including:
# ✓ target guard refuses a separate empty --space value
# ✓ target guard refuses a repeated flag whose last space value is empty
# ✓ target guard refuses both empty-space argv forms

node scripts/mutation-proof.mjs --config bin/smoke/mutations/sandbox-guard.json
# baseline green (7 progress marks)
# M1 KILLED named: foreign sandbox root is refused by identity
# M2 KILLED named: generic guard refuses flag-before-target down web
# M3 KILLED named: target guard uses the CLI parser's last space value
# M4 KILLED named: raw down spawn is not immediately guarded
# M5 KILLED named: check reaches smoke:sandbox-guard
# M6 KILLED named: check reaches smoke:sandbox-guard before its first environment-dependent live suite
# M7 KILLED named: target guard refuses both empty-space argv forms
# All 7 mutation(s) killed.

After the runner restored, HEAD was still 9d744676 and git status --porcelain was empty. The worktree has been removed. The parked review checkout remains at 98112007.

M3 also reddened the equals-form cell (first---space scanning cannot see --space=value). The named last-wins cell still changed from its green line, so the kill is the claimed one, not an earlier unrelated failure. M7 dropped to 4 marks because both empty-argv child cells missed their expected exception, which is the mutated emptiness check.

Verdict stands: APPROVE at 9d7446764f90198b4614ad2e9b87d33f70d5a51f.

The guard records four identities and refuses unless all four hold, but only
three had a named refusal cell. Hardcoding markerHeld to true left the suite
fully green, so the marker guarantee was recorded rather than checked.

Adds the missing cell and the registry mutation that must redden it.
@davidfarah2003

Copy link
Copy Markdown
Contributor Author

APPROVE at 8a580b7 (resolved myself from origin/up/884-sandbox-guard after fetch; cat-file reports commit; a one-character-mutated sha fails cat-file as the negative control).

The delta from the previously blocked head is exactly one suite cell and one registry entry. I materialized both changed files at this sha, verified the blob hashes, and confirmed baseline green before touching anything.

What I re-ran against the new cell, each restored byte-exact afterward with the suite green again:

  1. The shipped mutation (markerHeld hardcoded true): exit 1 with exactly one failed cell, and it is the marker cell by name. Not zero, not two, one.
  2. Forcing rootMatches to false instead: the marker cell fires zero times. The suite still exits 1, but from the correct earlier cells, so the new cell does not collect a kill it did not earn.
  3. Two refactor shapes beyond the registry entry: hardcoding markerIdentity to "same" (check dead, message would lie), and dropping markerHeld from the decision gate entirely. Both are caught by the same named cell. So the cell is genuinely specific rather than merely narrow: any implementation that stops refusing a missing marker fails it, whether the check dies by hardcode, by removal from the gate, or by short-circuit.
  4. The full registry through the shipped runner: all 8 killed, exit 0, named reds only.

On whether the cell regex could be satisfied while the marker check is dead: the regex demands a refusal message reading root=same, COTAL_HOME=same, XDG_CONFIG_HOME=same, marker=missing. A dead marker check permits instead of refusing, so no message exists to match; and a refactor that hardcoded the whole message while checking nothing would immediately redden the earlier permit cells that pin correct acceptance. I could not construct a passing shape.

On the verdict-string instrument (the message reports markerIdentity but the gate uses markerHeld): I probed the reachable states directly. With the marker actually missing, the message reads marker=missing, which is both what the gate acted on and what the cell pins, so in every state the suite can produce the string is a faithful witness. The divergent shape (gate refusing while the string says same) requires markerHeld to disagree with markerIdentity, which only arises under the exact mutation the registry now kills. My verdict: an acceptable instrument note, not a defect worth filing. Printing the gate value instead would erase the missing/foreign distinction the message currently carries, which would make it less faithful in the real states. If anything, a one-line comment in the guard noting the ternary's intent would be a nice-to-have, not a blocker.

The named gaps in the description remain accurately stated (CI does not run this suite; the live subprocess teardown path was graded by reading). Nothing in this delta changes either.

@davidfarah2003

Copy link
Copy Markdown
Contributor Author

Verdict: APPROVE WITH NITS at 8a580b759168001e80624aa52d4b702d9f52d405

Resolved first-hand after git fetch origin: origin/up/884-sandbox-guard is 8a580b759168001e80624aa52d4b702d9f52d405 (git cat-file -t = commit). Positive control 9d7446764f90198b4614ad2e9b87d33f70d5a51f is a commit and a true ancestor. An invented sha is refused. Delta from that ancestor is one commit: test: cover the sandbox ownership marker identity (+17/-1 across the suite and the mutation registry).

Executed, not read: a private detached worktree was added at this sha, pnpm install --frozen-lockfile was run there, then the suite and mutations were run there. The worktree has been removed. The parked review checkout remains at 98112007.

What the new cell does (re-measured)

The cell is:

refuses("missing sandbox ownership marker is refused by identity",
  () => assertSmokeSandboxDown(anchor, ["down"], { cwd: root, env }),
  /identity verdicts root=same, COTAL_HOME=same, XDG_CONFIG_HOME=same, marker=missing/)

after rmSync(join(root, ".cotal")).

pnpm smoke:sandbox-guard at this sha: PASS (8 named printers, including this cell).

Registered fixture (node scripts/mutation-proof.mjs --config bin/smoke/mutations/sandbox-guard.json): 8/8 KILLED. M8 named missing sandbox ownership marker is refused by identity and dropped to 7 marks against a baseline of 8.

M8 applied by hand and the suite captured: exactly one , that named cell, Missing expected exception. No other named printer failed. After restore, HEAD was still this sha and the tree was clean.

rootMatches = false (unregistered U1): the suite dies on the first doesNotThrow of a matching root, zero / printers, and never prints the marker cell. Confirmed: that mutation does not fire the marker cell.

Specific, or just narrow?

The regex pins the other three identities to same and requires marker=missing. That is specific to the missing-marker path, not a generic identity refusal:

  • U1 (rootMatches = false) reddens elsewhere and does not print this cell.
  • U5 (always print marker=held in the throw) still refuses the missing marker, but the cell goes red because the string no longer matches. The check is alive; the printer is what died.
  • U4 (drop markerHeld from the combined AND) accepts the missing marker and the cell goes red for Missing expected exception. Removing the check, not just the string, is visible.

So a refactor that keeps the verdict string while the check is dead still has to actually throw that string on this input. Dropping the AND without throwing fails the cell. Lying in the unused throw also fails it. That part of the claim holds.

What the cell does not see is a marker whose identity is foreign rather than missing. Unregistered U3:

const markerHeld = markerIdentity === "same" || markerIdentity === "foreign";

pnpm smoke:sandbox-guard PASS, 8/8 printers green, mutation-proof SURVIVED with a positive control in the same file. Missing still refuses (marker=missing is not held). A replaced-but-present .cotal would now be treated as held, and nothing in the suite plants that.

That is the eighth entry's remaining hole: M8 proves markerHeld = true cannot swallow a missing marker. It does not prove markerHeld is identity === "same" rather than identity !== "missing".

Unregistered mutations (not in the registry; not proposed as new cells)

id change result vs marker cell
U1 rootMatches = false WRONG-RED / early unwanted exception. Cell not printed.
U2 sameDirectory miss reports foreign KILLED. Missing marker now prints marker=foreign, regex misses.
U3 markerHeld true for foreign as well as same SURVIVED. Finding, not a request to weaken the cell.
U4 drop markerHeld from the AND KILLED, missing expected exception.
U5 throw always says marker=held KILLED, regex mismatch.
U7 skip sameDirectory for the marker (markerIdentity = "same") red, missing expected exception (same class as M8).
U9 marker identity compared against the root directory red, missing expected exception (root still exists after rmSync(.cotal)).

U3 is the one the registry still does not cover. I am not adding it to the fixture from this seat.

Verdict: APPROVE WITH NITS at 8a580b759168001e80624aa52d4b702d9f52d405. The new cell is specific to missing-marker with the other three identities pinned, and M8 kills the mutation it names. The remaining gap is foreign vs missing on the marker, which this round's registry still treats as out of universe.

@davidfarah2003

Copy link
Copy Markdown
Contributor Author

Follow-up at 8a580b7: the recording side is covered, indirectly but genuinely, and I verified it by execution rather than by reading.

The question was whether the registry attacks only the check while the anchor could record a fiction. A malformed version of that mutation dies in setup and proves nothing, so I built the well-formed shapes first:

  1. Record the marker identity from a different real directory (COTAL_HOME) while leaving all directory creation intact. The suite exits 1 with exactly one failed cell: "missing sandbox ownership marker is refused by identity", by name. The same-directory self-compare in the guard now compares the real marker against a recorded fiction, reports marker=foreign, and refuses the permit that the first cell requires.
  2. Record the root identity from the wrong directory. Exit 1; the very first permit cell reddens with root=foreign in the message.
  3. Falsify only the recorded dev/ino of the marker. Exit 1, refusal on the first permit, marker=foreign.

Then I ran shape 1 through the shipped runner as a trial registry entry (temporary config in /tmp, not committed anywhere): KILLED, red and named, and all 8 existing entries stay killed alongside it. A mutation tool entry for the recording side is therefore well-formed and cheap if the maintainers want the belt-and-braces entry. My honest view: it is optional, not owed. The reason is direction. Every entry in the registry today breaks the guard's decision logic, and the suite catches each by refusing or permitting wrongly at the guard call. The anchor mutations are a different species: they corrupt the fixture the suite hands itself, and the suite catches them because the guard is strict enough that a poisoned fixture cannot produce a consistent pass. That is real coverage, but it is a property of the whole suite-plus-guard system rather than of a check the registry names. A registry of guard mutations that happens to also survive poisoned anchors is honest; a registry entry that mutates the fixture to prove the fixture was honest is weaker evidence than it looks, because the same entry cannot distinguish a guard that catches fiction from a guard that merely disagrees with it.

On the reporting mismatch raised alongside this (the message reports markerIdentity while the gate uses markerHeld): I enumerated the reachable states directly. Marker actually missing: message says marker=missing, gate refuses, cell pins exactly that. Marker recreated (different dev/ino, same path): message says marker=foreign, gate refuses. Marker intact: both agree. The only state where the string could say one thing and the gate do another is a disagreement between markerIdentity and markerHeld, and I verified by probing that this state exists only under the exact hardcode mutation M8 already kills. So the string is a faithful witness of the decision in every state the code can reach unmutated. My ruling stands: an instrument note, not a defect. If someone wants to harden it anyway, gating on and printing the same value, or a one-line comment on the ternary, would do; neither is required for correctness.

Nothing here changes my verdict. The previously stated gaps (CI does not run this suite; the live subprocess teardown path was graded by reading) remain the accurate open items.

@davidfarah2003

Copy link
Copy Markdown
Contributor Author

APPROVE WITH NITS at a4c4cca

Resolved the head independently: rev-parse in a detached tree, ls-remote refs/pull/913/head, gh headRefOid, cat-file positive and negative controls all agree on this sha.

This solves the smoke-side half of issue 884 in the shape the issue itself demands: enforcement at the destructive call, with coverage derived from call shape rather than a maintained list. I exercised the real guard module directly (17/17 cells): foreign cwd refused, missing or foreign COTAL_HOME and XDG_CONFIG_HOME refused, marker missing or replaced refused, the generic guard refuses down web without the target guard, the target guard requires an explicit non-empty --space and verifies the registry record root, repeated --space is last-wins through the same node parseArgs the CLI uses, and unknown down flags fail closed. The repo-wide static scan replicates clean at this sha with zero unguarded files, and the mutation fixture's nine find strings all match the source verbatim.

Verified the guard agrees with the CLI's actual semantics: the declared down flags match, down.ts treats an empty parsed space as absent, findCotalRoot terminates at the suite-owned marker, and the registry record path meshes/space..json matches the workspace layout. The issue's enumeration claims hold against package.json, including the suite reachable only by a non-live-named script and the suite reachable by no script at all. The head commit also fixes a real defect found while wiring: the bind-fence teardown was passing down --server, which is not a declared down flag, so that teardown errored out and leaked the fixture broker.

I could not run pnpm smoke:sandbox-guard, pnpm typecheck, or pnpm changeset status in my tree because node_modules is absent, so those are inspected rather than exercised. The guard behavior itself was exercised against the module source with a self-contained driver.

Nits, non-blocking. CI never invokes pnpm check, so the ratchet proof fires only for local check runs; the PR discloses this and names the one-step ci.yml remedy. Manifest-form down is guarded on folder identity only, not on the manifest's owned root. The guard's downOptions is a copied mirror of the CLI's declared flags, so drift fails closed but would not be flagged by the static scan. The check chain was reordered to seat the guard before the first live entry; behavior-neutral but broader than the minimum diff.

Would ship while on call.

@davidfarah2003

Copy link
Copy Markdown
Contributor Author

BLOCK a4c4ccae140fa63028c010439550953c14d23b44

The folder-rooted identity guard is strong, but the target-addressed guard can approve a different registry entry from the one the CLI uses.

Concrete failure sequence:

  1. Record sandbox root S under COTAL_HOME=H.
  2. For requested space victim, put { "space": "other", "root": "S" } in the canonical file H/meshes/space.76696374696d.json.
  3. Put { "space": "victim", "root": "O" } in a legacy registry file, where O is an operator root.
  4. Guard cotal down web --space victim.
  5. assertSmokeSandboxTargetDown reads only the canonical filename, does not validate the document's space, sees root S, and passes.
  6. The CLI resolves victim through loadMeshes and findMesh, which key records by the document's space, so it selects the legacy entry at O.
  7. Teardown acts on O after the guard approved S.

The target guard must validate the same selected record the CLI honors, including its requested space, before comparing the root.

Exercised: pnpm smoke:sandbox-guard, pnpm smoke:core-boundary, pnpm changeset status, pnpm typecheck, all nine sandbox mutations, and a focused file-selection reproduction. Inspected: parser-equivalent argv handling, empty space, missing and replaced markers, guarded current call sites, smoke-kit privacy and source-only packaging, PR body scope, and issue 884 remaining open for the product-side resolver work. No live suite was run.

@davidfarah2003

Copy link
Copy Markdown
Contributor Author

Security review verdict: APPROVE WITH NITS at a4c4cca.

Head was resolved independently (ls-remote, gh, rev-parse, cat-file with positive and negative controls); all instruments agree.

The sandbox teardown guard survives the full adversarial lens: last-wins --space, empty space in both argv forms, flag-before-target, equals-form, down web without an explicit space, foreign or replaced .cotal markers, COTAL_HOME and XDG_CONFIG_HOME identity swaps, symlink aliases, registry split-brain (the guard reads the canonical space..json file and loadMeshes ranks that same file as winner), and static-scan bypass attempts (the suite scan fails red on unguarded call sites, raw spawns, and the allowlist is two audited non-CLI entries).

Exercised broker-free in the review tree: smoke:sandbox-guard PASS, smoke:core-boundary PASS (smoke-kit private, zero deps, 395 shipped files scanned, 0 imports), plus a parser-agreement reproduction running the guard against the real parseCommandArgs down spec: every CLI-accepted form is guard-accepted, unknown flags throw on both sides.

Nit N1 (non-blocking): smoke:sandbox-guard runs in the local check gate but no CI workflow invokes check or smoke:sandbox-guard. Adding it to the ci.yml unit job suite list would close the gap. Nit N2: the guard's downOptions table duplicates the CLI down flag spec; a cross-reference comment or shared constant would keep them in sync. Nit N3: duplicate web positionals dedupe harmlessly on both sides.

Named gaps: live broker teardown behavior was not exercised (banned for this review); mutation-proof was reviewed statically, not executed.

The target-addressed sandbox guard keyed only on the canonical filename
and the record root. loadMeshes and findMesh key by the document space
field, so a canonical file whose document named another space could pass
while a legacy record for the requested space selected a foreign root.

Require the canonical document space to match the requested space before
comparing roots, and pin that refusal with a named cell and mutation.
@davidfarah2003

Copy link
Copy Markdown
Contributor Author

Revised security verdict (supersedes the earlier comment): APPROVE WITH NITS at 2e9c64c.

Correction of record: the previously graded head a4c4cca had a real registry split-brain, and the earlier APPROVE WITH NITS at that sha is retracted. Reproduced against the real guard module: a canonical space.<hex(victim)>.json holding {space:"other", root:S} together with a legacy-named file holding {space:"victim", root:O} made assertSmokeSandboxTargetDown pass, because it keyed the record by filename while findMesh keys by the document space field. A down web teardown for space victim would then resolve the legacy entry and act at the operator root.

The new head 2e9c64c (fix(smoke): refuse mismatched target registry space) closes it: the guard now requires the canonical record's space field to equal the requested space, with a new suite cell and mutation M10. Re-verified at that sha: smoke:sandbox-guard PASS, smoke:core-boundary PASS (smoke-kit private, zero deps, 395 shipped files, 0 imports), and the split-brain reproduction now refuses with observed space "other", expected space "victim" while the CLI-side selection loop still resolves victim at the operator root, proving guard and consumer now key records identically.

Remaining nit (unchanged): smoke:sandbox-guard runs in the local check gate but no CI workflow invokes check or smoke:sandbox-guard.

@davidfarah2003

Copy link
Copy Markdown
Contributor Author

APPROVE WITH NITS 2e9c64c725c13c354a99a621a749be78ec8244a2

This supersedes my BLOCK at a4c4ccae140fa63028c010439550953c14d23b44.

The updated head requires the canonical registry document's space field to equal the requested space before accepting its root. I exercised the real updated guard with the real built workspace loadMeshes implementation:

  • malformed canonical { space: "other", root: S } plus legacy { space: "victim", root: O }: the guard refuses the mismatch
  • aligned canonical { space: "victim", root: S } plus a conflicting legacy duplicate at O: the guard passes and loadMeshes selects S

Observed: mismatchRefused: true, alignedGuardPassed: true, canonicalWon: true.

Also exercised successfully: pnpm smoke:sandbox-guard, pnpm smoke:core-boundary, pnpm typecheck, pnpm changeset status, and all ten mutations in the sandbox fixture.

Nits:

  1. The PR body still says the mutation fixture contains nine mutations and all nine are killed. The exact-head diff contains ten, including M10 for target document space identity.
  2. Current GitHub workflows still do not invoke smoke:sandbox-guard.

Issue 884 correctly remains open for the product-side resolver work. No live suite was run.

@davidfarah2003

Copy link
Copy Markdown
Contributor Author

APPROVE WITH NITS on 2e9c64c

Independent review of this change against issue 884.

Does it solve the issue? The issue's final form has two halves. The smoke-side ask is to enforce the sandbox assumption at the destructive call so a suite that has lost its sandbox refuses instead of tearing down the operator's mesh. The product-side ask is to remove fail-open from the verbs themselves, up first. This change lands the first half in full and wider than the issue's own file list: 21 suites across five directories, derived by call shape rather than by filename or script name. It deliberately does not close the issue because the product-side half remains open, and the PR body says so plainly. That matches the issue's own final state.

Checks I ran on this head, with the tree clean before and after:

  • pnpm smoke:sandbox-guard: PASS
  • pnpm smoke:core-boundary: PASS
  • pnpm typecheck: PASS
  • pnpm changeset status: clean, no bumps
  • node scripts/mutation-proof.mjs --config bin/smoke/mutations/sandbox-guard.json: all ten mutations KILLED on their named cells, restore verified, suite re-passes
  • Independent enumeration: 24 smoke files contain the literal down verb, the two allowlisted files use it semantically, the other 22 all route through the guard, zero unguarded call sites

Things I inspected by reading rather than running: the guard's parseArgs configuration exactly matches the CLI's declared down flags (file/-f, run, space, dry-run, preserve-state, store-dir; strict; positionals allowed; last-wins through the same node:util parser the dispatcher uses), and the target-guard record path matches the workspace registry layout (spaceKey hex naming, document space field authoritative, realpath root comparison). Every edited suite records its anchor before the first CLI call, every down uses the recorded root as cwd, and no scenario deletes the root or its marker before a down. The 21 edited suites are live and were not run, so the real subprocess teardown path stays a named, unexercised acceptance gap.

Nits:

  • The PR body says the fixture holds nine mutations; the committed fixture holds ten (M1 through M10). All ten kill, so this is a prose count, not an evidence gap.
  • CI does not execute the suite. I verified that the workflows run typecheck and the docs bundle and a live chain, but no pnpm check and no smoke:sandbox-guard, and ci-suites.txt does not list it. The proof is real but only runs when a developer runs pnpm check. The PR names this as an open item; wiring it beside pnpm typecheck in ci.yml is a one-line follow-up that would make the gate actually gate.
  • The static scan is a ratchet over today's call shapes, as the PR states: literal down only, a five-line lookback, and a two-file allowlist. It cannot catch a computed verb or a non-smoke.ts harness.

On-call judgment: ship. The change is fail-closed in every direction a sandbox can be lost, agrees with the real parser and registry, is mutation-proven, and leaves zero unguarded down call sites. Do not treat it as closing 884; the product-side half stays open and tracked.

@davidfarah2003
davidfarah2003 merged commit 96edee1 into main Aug 28, 2026
13 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant