feat(permission): confine what we execute, and stop asking what the fence already answers - #39
Merged
Merged
Conversation
…t costs The permission spike asked how routine work can need fewer approvals while authority stays explicit. It compared five harnesses and modelled the policy. It never audited where this harness decides, and the answer is not where the corpus says it is. ## Three findings, each verified in this checkout A delegated child's floor — whether it may write files or run commands at all — is a `matches!` on `ToolCall.name` in plexmaton-runtime, gating admission before any capability is read. CHB-1 states the outcome and never the mechanism; `NativeToolProfile` appears nowhere in .agents/. The governing sentence in tools.rs says a name is a label the model chose. At the highest -stakes boundary in the product, it is the decision. There is no DenyAndRemember. Deny wins every precedence race and is the only rule shape the interface cannot produce; it exists solely for someone who hand-edits configuration, and once they do, no view shows it back to them. The capability-based Forbidden and Ask tiers are documented, unit-tested and unreachable: both non-test-file calls to ApprovalPolicy::new fall after #[cfg(test)], so production runs the default with both sets empty. ## What the comparison adds Re-read against composition rather than precedence, Claude's "sandbox auto-allow" is not an exception anywhere — it is the shared rule, named in code in three independent implementations. The converse is ours: the two sources without containment are the two where turning the questions off leaves nothing underneath. ## Cost, measured instead of deferred sandbox-exec on macOS adds a constant ~6 ms per invocation — 2.6x on `true`, 1.8x on a pipeline, under a percent for anything doing real work. Cost is no longer a reason to defer containment on this platform. Running it exposed a constraint reading could not: a subpath parameter must be resolved, because the unresolved form is accepted as a valid profile and grants nothing — a write fence that looks applied and denies the roots it was told to allow. ## Two rules this change installs next-decisions.md carries the four axes a design is judged on, the hypothesis on the table, and the four open forks — because criteria agreed in conversation do not survive a session, and the last attempt spent three agent sweeps re-deriving what the spike and sandbox-boundary already held. .agents/README.md now says a budget tightens a corpus that code already documents, so a spike may exceed it while its work is unbuilt. This change leaves .agents/README.md itself 307 bytes over, which its own escape hatch (split the rules from the budget table) pays when something else touches it.
The containment question had one unknown left: the command contract requires group signalling to reach the shell and its descendants, drains to terminate, and a launch failure to stay distinguishable from a command exit. Wrapping the spawn in sandbox-exec looked like it put another process in that path. It does not. The probe records launched == shell: sandbox-exec applies the profile to itself and execs the target, so the process tree is structurally unchanged and every existing cancellation invariant holds untouched. 24/24, each arm run bare and wrapped, because a bare failure means the probe is wrong rather than the wrapper. The first two runs failed exactly that way. It also splits launch failure in two, which the earlier probe recorded as one. A malformed profile is loud: exit 65 and a located parse error. A well-formed profile with an unresolved subpath is silent: accepted, exit 0, command runs, fence grants nothing. Only the second needs defending against, by resolving every path before it enters a profile and verifying the fence after launch.
The spike asked how routine work can need fewer approvals while authority stays explicit. Three sessions answered it, and the answer is smaller than the machinery built for it. None of it was written down, which by this repository's own rule means it had not happened. ## The position Plexmaton has one user, on their own machine, on a project they chose, who picked the provider and installed every connected tool. Comparable harnesses prompt and warn because they cannot know any of those four things; read their code for its mechanisms, not for features to match. This harness is best-effort damage control and says so. Inside the blast zone everything is allowed and nothing is asked. Operations mean what they mean: no undo layer, because insurance inside a zone contradicts calling the zone acceptable, and losing uncommitted work already has an answer. Command inspection is out, because `python -c` or a build script defeats any parser. Network egress is out, because the model API call is already the largest egress channel and blocking `curl` guards an empty room. The rule that remains is one sentence: what we execute ourselves, we confine; what we hand to a subprocess, we do not pretend to. The file tools honour it today. The shell is the half that does not, and closing it is what lets the inspection that can never be complete stay unwritten. ## What measurement settled sandbox-exec applies its profile to itself and execs the target, so it adds no process: `launched == shell`, and every cancellation invariant holds untouched. 24/24, each arm bare and wrapped. Cost was already a constant ~6 ms. Failure splits in two. A malformed profile is loud. A well-formed profile with an unresolved subpath is silent — accepted, exit 0, command runs, fence grants nothing — and macOS hands you that by default through /var. Canonicalise before a path enters a profile; reject any path not equal to its resolved form. nono lost on evidence, not preference: it confines the calling process, which a TUI that must keep writing its journal cannot be, and reaching per-command through it returns to first-party unsafe in a multi-threaded process. Linux runs unconfined by decision until there is a host that can test Landlock or bwrap. ## Corrections this makes to its own prior claims The "sandbox auto-allow is the shared rule" finding was recorded with the audience folded into the problem. The composition rule survives; the warnings and tiers around it do not cross the audience boundary. sandbox-boundary.md is 665 bytes over its budget and README.md 41. Per .agents/README.md a spike may exceed while its work is unbuilt and pay the debt once the code documents itself, which is the next change.
The shell was the one place where a policy sentence was not also a capability. command-tool.md said so honestly — the root is a starting directory, not a sandbox, and this implementation claims no containment — and that honesty is what the owner did not want to keep. The alternative to a fence is reading the command before running it, and that can never be finished: `python -c`, `node -e`, a written-then-run script or a build script all reach the same effects under a spelling no parser has seen. So nothing here inspects a command. CMD-7 bounds writes at the kernel and leaves the command opaque, which is what lets the inspection stay unwritten. ## Why this is a small change sandbox-exec applies its profile to itself and execs the target, so the spawned process *is* the shell. CMD-2's process group, CMD-5's signalling and CMD-3's drains are untouched: below the spawn site, executor.rs is unchanged, and the cancellation and reaping tests pass unmodified, which is the guard that says so. environment.rs is unchanged too — CMD-2 already keeps real HOME and toolchain configuration, which is exactly what granting the standard cache directories requires. ## The failure this defends against A malformed profile is loud. A well-formed profile with an unresolved subpath is silent: accepted, exit 0, command runs, fence grants nothing — and macOS hands that over by default, reporting its temp directory under /var while the kernel matches /private/var. Every root is therefore canonicalised before it enters a profile or is dropped, and a root that does not exist is never granted. A second silent case appeared while running the suite: inside an outer sandbox that forbids nesting, every command exited 71 having run nothing, which reads as the command's own failure. That refusal belongs to the process tree, so it is probed once and reported as Unconfined::ApplyRefused rather than arriving as an exit code from everything the owner runs. ## What it does not claim Reads, network and process operations are untouched: confining reads breaks toolchains one missing sysroot at a time, and the model request is already the widest path off the machine. The setsid escape CMD-5 admits is unaffected — neither worsened nor repaired. Off macOS there is no fence, by decision: Landlock and bubblewrap exist, neither can be exercised on this host, and a fence that is never run reports confinement it may not deliver. Confinement is carried on the typed result so that difference is visible per command rather than inferred. ## Verified 53/53 in plexmaton-command and 258 in plexmaton-runtime, run both inside an outer sandbox (fence unavailable) and outside it (fence applied). A real `cargo build` compiles fresh dependencies under the generated profile, and the negative control denies a write one directory outside it.
The fence shipped one commit ago stopped git, python and curl from starting. Each reported `could not open '/dev/null': Operation not permitted`, which names the symptom and nothing a reader would connect to a write fence. A global `deny file-write*` covers /dev like any other path, and /dev/null is a write. Denying it confines nothing — the device holds no state — while removing it from every program that opens one for itself, which is most of them. git needs it to run at all: add, commit and log each failed. The tests missed it for a reason worth keeping: CMD-2 opens /dev/null for stdin in the *parent*, so the child inherits a descriptor and never opens one. Every test that redirected into a file passed, and a real `cargo build` passed, because neither opens a device itself. cmd_7 now runs a command that does, and fails with the exact message above when the grant is removed — checked by reverting the fix and re-running. Granted by name rather than as a subtree: /dev holds raw disk devices. /dev/fd is the one subpath, for process substitution. Verified: 54/54 in plexmaton-command, and a full git init/add/commit/log workflow under the generated profile, with the out-of-zone write still denied.
The injected-supervision helper gave a command 25 ms to install a SIGTERM trap and exec into a sleep, then relied on the deadline arriving while it was still alive. That was always a race against process startup; CMD-7's launcher puts real milliseconds in front of the shell, and the race started losing — cmd_5's kill-failure arm reported TimedOut with no SIGKILL sent, because the group was gone before termination could fail the way the test injects. The rest of this module already solved this: write a marker, wait for it, then act. Elapsed time is not readiness. The trigger becomes cancellation, which reaches the same terminate_and_reap path, fired once the command is provably in its final state. Selected rather than joined, because the injections divide: one on the wait path fails before any marker can be written and needs no trigger at all, and joining on a marker that never arrives hung that arm for its full bound. Verified fenced, 20 consecutive runs of the full crate suite: no failure in this family. The one remaining intermittent failure is cmd_5_cancellation_gracefully_terms_reaps_and_joins_drains at ~5%, and it is not this change — an unfenced control, whose spawn is byte-identical to the one before CMD-7, flakes at the same rate over the same 20 runs.
Unfinished and committed early: this work was overwritten once in the working tree today and recovered from dangling blobs. It compiles; it does not yet pass. Both halves of "a call the system bounds does not need the question the bound replaces" become seeded Session presets rather than a policy default, so each is visible in /permissions and revocable, and neither depends on the ambient host in a way a test cannot control: - files: the existing PER-3 native file-change preset, granted at Session start. Its exclusions stand, so .git and agent-control paths keep asking. - commands: a new ConfinedCommands matcher, granted only where CMD-7 reports it can fence one. The check stays inside plexmaton-command so no caller can assert a fence that crate did not find. Outstanding, and why this is WIP: nine runtime permission tests encode the old default and need deciding one at a time, and owned_scheduling::user_control::attention_decision_routes_only_to_the_exact_live_child_generation hangs. Bisecting showed the hang survives removing the confinement probe, so the cause is not the blocking spawn it looked like; it has not been isolated to this change or to the rebase onto 1a3f652.
Still unfinished. Sixteen runtime fixtures broke on one root cause and ten remain; this lands the part that is decided so the next pass starts from a position rather than a bisect. The cause is single: these fixtures use a command as the call that waits, and the confined-command preset removes the wait. One of them — owned_scheduling::user_control::attention_decision_routes_only_to_the_exact_live_child_generation — has no timeout around that wait, so it hung rather than failed, which is what sent an earlier bisect after the confinement probe. The probe was innocent: removing it left the hang in place, and the test passes untouched at 8a84d7c. ask_about_commands revokes the preset the way an owner does from /permissions, so each fixture keeps its own subject — the commit boundary, attention routing, what a remembered scope covers — instead of testing a default it never meant to assert. On a host with no fence there is nothing to revoke and commands already ask, so the fixture reads the same either way. Applied at the shared builders rather than at sixteen call sites, and after any use_coding_session, because a Session replacement carries its own preset. The ten that remain are a second class: they assert grant counts and revisions that two seeded presets shift. Those want relative assertions, not new numbers. Still owed beyond them: tests for the new default itself, so the relaxed path is not the untested one, and the documents.
Sixteen fixtures broke on one cause and they divided in two. Most use a command, or an edit, as the call that waits, and the seeded presets remove the wait. ask_about_commands and ask_about_file_changes revoke a preset the way an owner does from /permissions, so each fixture keeps its own subject — the commit boundary, attention routing, what a remembered scope covers — and reads the same on a host with no fence, where there is nothing to revoke and the call already waits. Applied at the shared builders and the two `owner` helpers rather than at every call site, and after any use_coding_session, because a Session replacement carries its own presets. The rest asserted totals. `grants().len() == 1` was a proxy for "the approval applied one grant" and stopped meaning that the moment a Session starts with presets; `grants()[0]` reached for that grant by position and found a preset instead. Both now name the origin, which says what was meant and keeps saying it whatever else the Session carries. PER-7 and the session picker's PER-7 are the two that needed the opposite: they prove that *enabling* the setting releases what waited, so they clear the seeds first and watch the flow put one back. 1282 tests pass across the workspace; clippy is clean. Still owed: tests for the new default itself, so the relaxed path is not the untested one, and the documents.
PER-11. A call the system bounds does not need the question the bound replaces. An edit reaches only what WFS-1 and MUT-2 pin; a command reaches only what CMD-7 fences. So a coding Session seeds the native file-change preset, and the confined-command preset where a fence exists to hold it. Presets rather than a policy default, and that is the whole design. PER-3's exclusions live on the preset, so relaxing the fallback would have granted the control plane along with everything else — `.git`, `.plexmaton`, `.agents`, `.codex` and `agents.md` at any depth keep asking precisely because the preset is what carries them. And a grant is a /permissions row: revocable, visible, gone with the process. An invisible default is none of those, and could not have given the fixtures a host-independent way to get a call that waits. Nothing is granted that precedence cannot refuse: PER-2 consults a Deny rule, an explicit Ask rule and then grants, so an owner who asked to be asked still is, and a command on a host with no fence still waits because no preset was seeded there. Proven on the production path rather than through the test helper that revokes: a fenced command runs with no pending approval and no decision-created grant; revoking the preset brings the question back, which is what makes the silence the preset's; and a seeded Session writes note.txt while .agents/note.md waits. 1285 tests pass across the workspace, clippy is clean, six gates pass.
…ready bounds PER-3 excluded `.git`, `.plexmaton`, `.agents`, `.codex` and `agents.md` at any depth from the native file-change preset. With PER-11 seeding that preset, the two halves of one Session grant answered the same question differently: CMD-7's fence grants every path beneath the workspace root to every shell command, which is the route that actually writes `.git`, while the exclusion asked about the route a model does not need. It also reinstated in-zone approvals on the path this repository writes most. The bound is what WFS-1 and MUT-2 pin. Inside it nothing is carved back out. The `/permissions` row and its confirmation now say what the grant covers rather than what it excludes; three widths regenerated.
`cmd_5_cancellation_gracefully_terms_reaps_and_joins_drains` asserted that a cooperative command is never escalated to SIGKILL, and measured it against the product's one-second termination grace while nine sibling tests spawned processes beside it. It lost about one run in six on this branch, and passed 6/6 serially — contention, not logic. The grace now comes through the `ProcessOperations` seam the supervision fault tests already use. Production answers with the product constant; this one test answers with sixty seconds, so a SIGKILL means a real hang rather than a slow scheduler. Widening strengthens the assertion: the run time is unchanged at 2.03s, so the group still becomes quiescent immediately. The fixture's readiness marker is renamed into place. One created by `>` and filled afterwards exists before it holds a pid, and hands back a truncated one. Verified: 12/12 clean where it was 1/6 failing, and the assertion still fails when the executor's early return is mutated away.
… back PER-11 seeds the confined-command preset, so a command runs without asking. Three journeys drive a command approval and were never told: `smoke-permissions`, `smoke-model` and `smoke-tree` all timed out waiting for "Approval required" on this branch, which CI would have reported and no local gate did. `Terminal.restore_command_approvals` takes that row back through `/permissions` before the part of a journey whose subject is the admission itself. Where no fence exists the grant was never seeded and the question is already there, so the helper changes nothing and the journeys read the same on every host — the seeded row is what the `/permissions` list shows, not a flag a test can set. The two journeys that expected a command to run unasked now prove the grant they name: `smoke-permissions` revokes in both processes, so its configured Allow rule and its Project prefix grant each carry their own turn rather than riding the seeded one. The row marker is short on purpose: the full scope wraps at 60 columns, and a wrapped marker silently matched nothing, which skipped the revoke and left `smoke-model` failing exactly as before. Verified: all seven PTY smokes pass on this branch, and `scripts/tests` 43 pass.
…t the card applies Two reviews, eight findings, all verified against the code. The offer card lied. `remember_offer` still read "native create/edit; no controls/Git" after PER-3 dropped the exclusion, so Allow-and-remember described a narrower grant than the one it installs — the reading PER-10 exists to prevent, and nobody had to edit the card for it to happen. The card now takes its scope off the matcher it will apply, and a test pins them equal so the two cannot drift again. GOPATH was granted whole. It names a workspace holding `src` beside `pkg/mod`, so every other Go project under it became writable — source outside the admitted workspace, which is what the fence exists to deny. Caches are now declared with the variables that relocate them and the tail that reaches the cache inside the value; a set variable replaces its default rather than joining it, which is what the old comment claimed and the old loop did not do. `/tmp` was denied. macOS answers `TMPDIR` with a per-user directory under `/private/var/folders`, so a command hardcoding `/tmp` — which shell one-liners and build scripts routinely do — reached no granted root. Both are granted; one is dropped by dedup where they are the same path. A green gate proved nothing about the fence. Every fence test steps aside where the host has none, which is right for a developer inside an outer sandbox and would let a runner refusing nested profiles take the whole mechanism with it behind a passing run. The macOS gate sets `PLEXMATON_FENCE_REQUIRED`; where that is set, an unavailable fence fails instead of skipping. `a_binding_carries_…` also asserts both arms rather than falling off the end unfenced. PER-3 claimed a confined-command proof it did not have: the test it named only checks Seatbelt argv shape. A matcher test now proves membership is the catalog's definition and revision, and refuses a lookalike. Revoking the confined-command preset holds for the life of the process, and PER-11 says so rather than a comment claiming the owner cannot un-make it while the UI offers Revoke. Rejected: a typed re-enable twin, which buys a rarely-walked path back for a second control and its confirmation copy. Both spike Status rows still read "unbuilt" while the same file's body named CMD-7 and PER-11. That header is the first sentence the next agent is routed to. Verified: 1592 pass with the fence gate on, clippy and fmt clean, all seven PTY smokes pass, 43 script tests, citations/frames/file-length clean. The two new assertions were each confirmed by mutating the implementation and watching them fail. `GOPATH/src` is denied and `GOPATH/pkg/mod` and `/tmp` allowed, checked against a real `sandbox-exec` launch.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Tool permission had one rule the code did not follow, and a policy nobody had written down. This
branch settles the position, builds the fence it calls for, and stops asking the questions that
fence replaces.
The position
.agents/spikes/permission-policy/next-decisions.mdrecords it, because a position agreed inconversation does not survive a session — the previous attempt spent three sweeps re-deriving what
the spike already held.
Plexmaton has one user: a solo developer, on their own machine, on a project they chose, who picked
the provider and installed every tool connected to it. That decides which mechanisms in comparable
harnesses are load-bearing here and which exist for an audience we do not have. A prompt saying
"this tool needs network access" tells Claude's user something they did not know; told to this user,
about a server they installed for that purpose, it is noise.
Four axes were fixed with the owner before any of the decisions below, and the decisions were judged
against them: rate (approvals per turn), generalisation (how many future questions one
answer prevents), answerability ("Allow Bash?" cannot be answered — Bash does anything), and
interruption (a long autonomous run stopped for something trivial; a delegated child asking
while the user reads the parent). Hands-off inside the blast zone scores full marks on all four.
That is why the remaining work is a fence rather than a dialogue.
What that rejected, each with its reason recorded beside the rule: approval prompts for in-zone
work, checkpointing the worktree, command inspection of any kind, network egress as a category, and
capability warnings.
CMD-7 — the fence
On macOS the shell launches through a Seatbelt profile denying writes outside a resolved root set —
the admitted workspace root, both temporary directories, and the toolchain caches the owner's
environment names — leaving reads, network and process operations untouched. The command itself is
never inspected: any interpreter defeats a matcher under a different spelling, and matching a
command cannot establish its effects.
Three things the implementation had to learn the hard way, each now a sentence in the spec:
exits zero, and grants nothing. macOS hands that case over by default: it reports its temporary
directory under
/varwhile the kernel matches/private/var. A fence that looks applied anddenies the roots it was told to allow, with no diagnostic.
/dev/nullstopsgit,pythonandcurlfrom starting, and confines nothing. The executor supplies stdin from the parent, so aninherited descriptor satisfies any fixture that only redirects into a file — the test must open
its own device or the regression passes unseen.
exited 71 having run nothing, which reads as the command's own failure.
The launcher applies the profile to itself and
execs the shell, so the spawned process is theshell: CMD-2's process group, CMD-5's signalling and CMD-3's drains are structurally unchanged.
Measured at ~6 ms per invocation, and 24/24 cancellation arms pass bare and wrapped. Off
macOS the spawn is byte-identical to an unconfined one and the typed result carries which case
applied — a recorded fact, not a platform accident.
PER-11 — a Session starts granting what it already bounds
A coding Session seeds the native file-change preset, and the confined-command preset where PER-3
offers one. A call the system bounds does not need the question the bound replaces: an edit reaches
only what WFS-1 and MUT-2 pin, a command only what CMD-7 fences.
Both are ordinary
/permissionsrows — revocable, gone with the process — so revoking one restoresits question. Precedence is untouched: Deny and explicit Ask are consulted before any grant.
Rejected: dropping
FileWriteandProcessSpawnfrom the policy's fallback. It reaches the samesilence and loses what makes it answerable — a fallback has no
/permissionsrow, so it cannot beseen, revoked, or turned off for one Session.
PER-3 — nothing is carved back out of the zone
The file-change preset excluded
.git,.plexmaton,.agents,.codexandagents.mdat anydepth. Once PER-11 seeded that preset, the two halves of one Session grant answered the same
question differently: CMD-7's fence grants every path beneath the workspace root to every shell
command — which is the route that actually writes
.git— while the exclusion asked about theroute a model does not need. It also reinstated in-zone approvals on the path this repository writes
most, which is the reflex the position exists to prevent.
The bound is what WFS-1 and MUT-2 pin. Inside it nothing is carved back out. The
/permissionsrowand its confirmation now say what the grant covers rather than what it excludes; three widths
regenerated and reviewed.
Two defects this found in its own evidence
A 1-in-6 flake.
cmd_5_cancellation_gracefully_terms_reaps_and_joins_drainsasserted that acooperative command is never escalated to SIGKILL, measured against the product's one-second grace
while nine sibling tests spawned processes beside it. Parallel: 1/6 failing. Serial: 6/6 clean.
main: 8/8 clean. Contention, not logic.The grace now comes through the
ProcessOperationsseam the supervision fault tests already use.Production answers with the product constant; that one test answers with sixty seconds, so a SIGKILL
means a real hang rather than a slow scheduler. Widening strengthens the assertion — and the
run time is unchanged at 2.03 s, so the group still becomes quiescent immediately. Verified 12/12
where it was 1/6 failing, and the assertion still fails when the executor's early return is mutated
away. The fixture's readiness marker is also renamed into place; one created by
>and filledafterwards exists before it holds a pid.
Three red smokes.
smoke-permissions,smoke-modelandsmoke-treeeach drive a commandapproval and were never told PER-11 had seeded it away. All three timed out waiting for
"Approval required".
Terminal.restore_command_approvalstakes that row back through/permissionsfirst — the same route the owner has, not a flag a test can set — and where no fenceexists the grant was never seeded, so the journeys read the same on every host. The two journeys
that expected a command to run unasked now prove the grant they name rather than riding the
seeded one.
What two independent reviews found
Eight findings, each verified against the code before it was acted on.
The offer card lied.
remember_offerstill read"native create/edit; no controls/Git"afterPER-3 dropped the exclusion, so Allow-and-remember described a narrower grant than the one it
installs. That is the reading PER-10 exists to prevent, and nobody had to edit the card for it to
happen — a second copy of the sentence was enough. The card now takes its scope off the matcher it
will apply, with a test pinning them equal.
GOPATHwas granted whole. It names a workspace holdingsrcbesidepkg/mod, so every otherGo project under it was writable — source outside the admitted workspace, which is precisely what
the fence exists to deny. Caches are now declared with the variables that relocate them and the
tail that reaches the cache inside the value, and a set variable replaces its default instead of
joining it, which the old comment claimed and the old loop did not do. Checked against a real
sandbox-execlaunch:GOPATH/srcdenied,GOPATH/pkg/modallowed./tmpwas denied. macOS answersTMPDIRwith a per-user directory under/private/var/folders,so a command hardcoding
/tmp— which shell one-liners and build scripts routinely do — reached nogranted root and failed with a denial nothing in the profile explained. Both scratch directories are
granted; dedup drops one where they are the same path.
A green gate proved nothing about the fence. Every fence test steps aside where the host has
none — right for a developer inside an outer sandbox, and wrong for the gate, where a runner
refusing nested profiles would take the whole mechanism with it behind a passing run. Nobody could
see which of the two reasons it passed for, which is the difference the position says must never be
implicit. The macOS gate now sets
PLEXMATON_FENCE_REQUIRED, and where that is set an unavailablefence fails instead of skipping. This PR's run is the first that proves the fence applied on the
runner.
a_binding_carries_…likewise asserts both arms rather than falling off the end unfenced.PER-3 claimed a proof it did not have. The test it named only checks Seatbelt argv shape. A
matcher test now proves confined-command membership is the catalog's definition and revision, and
refuses a lookalike.
Revoking the confined-command preset is one-way within a process, and PER-11 now says so,
rather than a comment claiming it is not the owner's to un-make while the UI offers Revoke.
Rejected: a typed re-enable twin, which buys a rarely-walked path back for a second control and its
confirmation copy; restarting is the boundary the Session already has.
Both spike Status rows still read "unbuilt" while the same file's body named CMD-7 and PER-11 as
built. That header is the first sentence the next agent is routed to.
Verification
Run outside the sandbox: 17 runtime tests bind loopback fixture servers, which a sandbox refuses.
cargo test --workspace --locked --no-fail-fastPLEXMATON_FENCE_REQUIRED=1cargo test -p plexmaton-command --libcargo clippy --workspace --all-targets --locked -- -D warningscargo fmt --all --checkpython3 -m unittest discover -s scripts/testsNo document crosses a budget it was not already over;
next-decisions.mdgrew 40 bytes.Still open, and deliberately not in this branch
existing phase or a phase of its own — a roadmap decision.
disclosing beneath it, so removing the question left the record intact. A per-turn status-line
summary sits on top of that floor; it is contract text, so
ui-ux.mdowns it and it needs arendered frame the owner has seen.
prefix. Its tree-sitter parse of every command, the 20 ms budget another branch hasbeen fighting, and the capability engine that never runs are all dead once a command on a fenced
host never reaches a prefix offer. PER-10 has 40 citations across code and documents; a mechanism
goes with its spec, tests and citations together, so that is its own change.