Clone-per-change under ~/changes and 'allod change run' to sandbox a worker in it #282
Labels
No labels
blocked
bug
decision
duplicate
enhancement
help wanted
invalid
landed?
question
ready-to-merge
stale
wontfix
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
allod/tools#282
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
allod change begin -dmakes a self-contained clone under~/changesinstead of a linked worktree, and a newallod change runruns a worker command inside a bubblewrap sandbox that mounts only that clone, so one dev VM runs many workers that cannot reach each other's trees or the VM's forge and SSH credentials.Part of allod/strategy#69, which holds the design and the mount set. This issue is the
allod/toolshalf; the archetypes and memory halves are their own issues.Primary goals:
begin -dmakes has its own.git, itsoriginset to the parent's origin, and itsagent/<desc>branch cut from the parent'sorigin/<start>; a hardlinked local clone, never an alternates clone, because a parentgit gccan prune what an alternates clone still references..git(path and branch), where a sandboxed worker cannot write.list,record,cleanupandrunresolve a clone to its parent through that ledger, never through the clone's own config, andrecordrefuses to push a clone whoseorigindiffers from its parent's.protected-branchesidentity, sorecordfrom inside one is not a misplaced checkout (exit 8) and the "shared checkout" guard on unnamed changes does not fire for it. The bash twingit-hooks/protected-refs-policyis untouched: inside the sandbox it falls through to the origin match, which blocks only the protected branch and leavesagent/*commits alone.allod change run [<clone>...] -- <command...>— the mount set in allod/strategy#69 under a transient user scope with a memory cap; refuses anything that is not a ledgered clone on anagent/*branch and never runs unsandboxed.list,recordandcleanupkeep handling a linked worktree (--git-dirdiffers from--git-common-dir) until none remain;beginmakes no new ones.Validation
tests/allod-change.shrewritten for clones with the linked-worktree cases kept for the drain window; a probe run throughrunfinds no forge token, no SSH directory, no sibling clone and no shared checkout, with the same probe outside the sandbox finding them;git commitsucceeds inside on the clone's branch and the parent's refs are byte-identical before and after; anix buildof a trivial derivation succeeds inside through the daemon socket; exceeding the memory cap kills the scope and only the scope. Whetherbwrapcan open its nested user namespace inside the nix build sandbox is settled by running it; if not, therunchecks become a by-hand script besidetests/site-preview-by-hand.sh, run in a dev VM with its output in the PR body.Scope
In:
cmd/allod/change.go,internal/protection, the tests,README.mdanddocs/. Out: the archetypes package declaration and the skill text, which are their own child issues; network filtering (allod/strategy#14).Handoff: the branch for this issue is built, reviewed and verified on a dev VM; it relays to the hypervisor as unpushed commits on
agent/worker-sandboxand lands as one PR with this body:On a dev VM,
allod change begin -dnow makes a self-contained clone under~/changesinstead of a linked worktree, the parent repository keeps a ledger of the clones it issued under its own.git, and a newallod change runruns a command inside a bubblewrap sandbox that can reach that clone, the memory checkouts read-only, and the runner's own state, with the forge token, the SSH directory, every shared checkout and every other clone absent. What stays identical:record,submit,listandcleanupkeep their shapes and exit codes, a linked worktree made before this change keeps working in all of them until it drains, the protection package and the git hook are untouched, and nothing changes on the hypervisor. Proof: the shell suite at 332 cases and the sandbox suite at 48, every flake check green, the by-hand script on a real dev VM showing the scope and its memory cap, a sandboxed commit with the parent untouched, the cap killing only the scope, and a build through the daemon socket, plus a live model call inside the sandbox and every cold-review finding re-executed and refused. If it is wrong, revert the branch: the ledger directory in each parent's.gitis inert to older binaries, and any clone already made is an ordinary repository that plain git handles.Refs allod/strategy#69. Closes allod/tools#282.
Risk
R3. A security boundary inside a VM, generated sandbox arguments, a new trusted store (the ledger) and a change to how every
allod changecommand resolves a tree. No secrets, host commands or provisioning; no deployed state. Rollback is a revert. The worst credible failure after validation is a mount-set gap that lets a worker see something the table says is absent; the probes intests/allod-change-run.shand their sabotage halves target that directly.One cold cross-family review (gpt-5.6-sol via pi) returned eight findings; seven were real and fixed in one round: record and cleanup now trust the ledger's branch and origin, not the clone's; the origin check covers
remote.origin.pushurl;begin -drefuses a repository outside the work dir, since the ledger scan would never find its clone; the ledger claim is an O_EXCL create keyed by description; the token exclusion matches hard links and symlinks. Each was re-executed against the rebuilt binary after the fix.Validation
go vet ./...,go test ./...: all packages ok.tests/allod-change.sh: 332 passed.tests/allod-change-run.sh: 48 passed, each credential probe with a sabotage run proving it can fail.nix build .#checks.x86_64-linux.<each>for all eight attributes, one at a time: green;allod-parityruns the 332-case suite inside the build.tests/allod-change-run-by-hand.shon a dev VM: scope named andMemoryMaxset; sandboxed commit lands, parent refs byte-identical; cap exceeded kills the scope only (oom-kill, exit 137);nix-buildinside succeeds through the daemon socket.bwrapopens its nested user namespace inside the nix build sandbox, but that sandbox has no/etc,/run/current-system,/binor/usrto bind, which is why the run suite is by-hand rather than a flake check.allod change runwith aclaude -p --model haikucall inside answered; inside, the forge token,~/.ssh, shared checkouts and other clones were absent, the memory tree read-only, a push refused for lack of credentials.Handoff update for commit
cd9a968on local branchagent/worker-sandbox(17 commits ahead of current master, clean, unpushed). This replaces the validation counts and security claims in the earlier handoff comment.On a dev VM,
allod change begin -dmakes a self-contained clone under~/changes; the parent keeps its ledger outside the worker's reach.allod change runconfines a worker to the named clone or clones, read-only memory and Git rails, and the runner state it needs, under a user scope with a memory cap. A worker can commit inside its clone. The forge token, SSH directory, shared checkouts, and sibling clones are absent. Outside operations on a ledgered clone use isolated Git configuration so worker-controlled hooks, attributes, filters, and inherited helpers do not run with the overseer's authority. Existing linked worktrees still drain through the old paths.The command refuses a trusted Nix user. It becomes usable on the deployed dev VMs after the paired
allod/vm#12andallod/nexuschanges land,allod/archetypes#146declares bubblewrap, and the VM is rebuilt. The runner's provider credential and shared network remain inside the sandbox by design (allod/strategy#13,allod/strategy#14). A clone whose HTTPS remote depends on a global credential helper needs an approved transport, because outside clone Git does not load that helper. If this change is wrong, revert it; its ledger is inert to the previous binary and each clone remains an ordinary Git repository.Refs allod/strategy#69. Closes allod/tools#282.
Risk
R3. This changes the
allod changelifecycle and adds an isolation boundary inside the existing VM. The VM remains the host boundary. The worst credible failure after the checks is a mount or Git execution gap in one worker; the sandbox and host Git gates fail closed on the observed unsafe inputs. No deployed state changes until a pin advance and VM rebuild.Validation
nix flake check -Lpassed.go test ./...andgo vet ./...passed in the dev shell.tests/allod-change.sh441/441;tests/allod-change-run.sh133/133. The focused cases include sabotage probes for the mount set, executable hooks and filters, credential aliases, reflog links, commit graphs, unsafe runner paths, and missing or unparseable Nix configuration.tests/allod-change-run-by-hand.sh6/6 on a dev VM: two writable clones in one scope, a commit with the parent untouched, scope-only OOM kill at the cap, and a trivial build through the Nix daemon socket. It first verifies the real trusted-user refusal, then uses a test-only Nix query mock to exercise the sandbox; a production run as an untrusted user awaits the VM change above.git diff --check origin/master...HEADand the local worktree are clean. The final review findings were resolved in the committed implementation; no new review pass is claimed.Handoff, final: head
1619cbfonagent/worker-sandbox, 18 commits on current masterd3d045f, verified by the integrator on this tree (441 change cases, 133 sandbox cases and six live steps fromnix develop, Go checks green). The last two cold reads' verified findings were already covered bycd9a968; the two commits after it trim comments and document that the sandbox suites run from the devShell. PR body:On a dev VM,
allod change begin -dnow makes a self-contained clone under~/changesinstead of a linked worktree, the parent repository keeps a ledger of the clones it issued under its own.git, and a newallod change runruns a command inside a bubblewrap sandbox that can reach that clone, the memory checkouts read-only, and the runner's session directories, with the forge token, the SSH directory, every shared checkout, every other clone, the overseer's runner configuration and the environment absent. Every command that acts on a clone from outside trusts only the ledger: it refuses a clone whose branch, remote or git config differs from what the ledger and a fixed allowlist permit, and refuses while a worker is still running in it.runrefuses to start on a guest whose login user is still a trusted nix user, which every guest is until allod/vm#12 lands, so this change is usable for a swarm only after that one. What stays identical:record,submit,listandcleanupkeep their shapes, a linked worktree from before keeps working in all of them until it drains, the protection package and the git hook are untouched, and nothing changes on the hypervisor. Proof: the shell suite at 441 cases, the sandbox suite at 133, every flake check green, six live steps on a real dev VM including a two-clone run, a live model call inside the sandbox, and fourteen cold model reads across ten fix rounds whose verified findings were fixed and re-executed. If it is wrong, revert the branch: the ledger directories are inert to older binaries and any clone already made is an ordinary repository.Refs allod/strategy#69. Closes allod/tools#282.
Risk
R4, by the owner's call: a security boundary inside a VM where a mistake exposes the forge credential or the overseer's session to a worker. No secrets or deployed state move and rollback is a revert, but the review budget was the R4 one: a design read, hardening reads with repository access, correctness reads and third-family reads, fourteen in all across OpenAI, Anthropic and open-weight models, rotated until the last pair.s verified findings were all already in the tree. The worst credible failure after validation is a mount-set or gate gap a reviewer did not think of; the probes with sabotage halves in
tests/allod-change-run.shand the config-gate tests intests/allod-change.shpin what was thought of.What the reviews found and the branch now does, in short: trusts the ledger's branch and origin, not the clone's (push URLs included); refuses a clone whose git config could run a command, whose
.gitis a gitfile, or which holds a populated submodule; copies objects instead of hardlinking them; refuses while a run scope for the branch is active; clears the environment to a fixed allowlist with no flag to widen it; binds runner directories read-only except sessions and logs and gives Claude Code a per-run copy of its state file; skips any symlink or hard link that would surface the token; masks memory checkouts' own git directories; starts a new session so the sandbox cannot inject into the invoking terminal; and refuses to start for a trusted nix user or a readable nix netrc.Validation
go vet ./...,go test ./...: all packages ok.tests/allod-change.sh: 441 passed.tests/allod-change-run.sh: 133 passed from the repository devShell, each absence probe with a sabotage run proving it can fail.nix build .#checks.x86_64-linux.<each>for all eight attributes, one at a time: green.tests/allod-change-run-by-hand.shfrom the devShell on a dev VM, six steps: the real trusted-user refusal; scope named withMemoryMax; a two-clone run with both writable; a sandboxed commit with the parent untouched; the cap killing the scope only, with a process started outside surviving; a build inside through the daemon socket.claude -panswering inside; inside, the forge token,~/.ssh, shared checkouts, other clones and the briefs directory absent; runner settings read-only and session directories writable; the outside state file unchanged after a run; memory checkouts readable with their.gitmasked; no controlling terminal; a push refused for lack of credentials.Shipped to relay: head
bf12261, history rewritten into two logical commits (clones, ledger and gates; the run command) with the tree byte-identical to the verified head07ad7d4; the first commit builds on its own. Final verification on that head: Go checks, the flake parity gate (704), 454 change cases, 146 sandbox cases and 8 live steps from the devShell. Twelve fix rounds and sixteen cold reads in all; the last read on the shipped source reproduced nothing.Landed as the two logical commits on master; verified on nix-dev after its rebuild.