Files
stack/agents/filbert/work/s6/BUILD.md
T
jason.woltjeandClaude Opus 5.5 55bff3b274 docs(s6): row 41 round 4 candidate packet, R5 fix (filbert)
Candidate manifest 08a78972 (42 files). Gate at 0a4c8f13.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 22:30:16 -05:00

571 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Row 41 (#1523): slice 1 S6, candidate packet, round 4
Author: Filbert. Reviewer: Darkwing. Brief: `docs/plans/2026-10-04_slice-1.md`,
section "Slice 1 S6", blob `72d11de2`. Plan: `PLAN.md` here (b6d2fe2b).
Rulings: lead decisions 77 and 78, Sage's round 2 ruling (fix R1, R2
and R3), round 3 ruling (fix R4) and round 4 ruling (fix R5). Base:
`915e00e5` (`base.txt`). None of the 42 files changed between the base and
`0a4c8f13`, where the round 4 gate ran. The candidate source is uncommitted. There are no pushes. No token, private
binding or tracker host was read or written, and nothing touched the live
`mosaic-bus@mosaic-stack` unit or `~/.mosaic-dev/bus/`.
## Round 4
Darkwing's round 3 asked for changes (#1523 comment 27032, rev 271, packet
`agents/darkwing/work/s6-review/review-r3.md` and `r3/`, 974da6ed). R4 is
verified fixed. Sage ruled: fix R5 in this round; leave R1 to R4 alone, no
other scope. Four files changed since round 3, and none is new:
- `packages/harness/src/gate.mjs`, `README.md`
- `packages/harness/tests/gate.test.mjs`, `pi-session.test.mjs`
No source outside `insideWorkspace` in the gate changed.
### R5: a relative path is resolved from the real path of Pi's cwd
Both adapters `cd "$MOSAIC_WORKSPACE"` before they start the harness, and
a process's cwd is the real path (`getcwd`). Pi 0.85.1 resolves a relative
path lexically against `process.cwd()` (`resolveToCwd`), so `..` climbs the
real path's parents. Round 3's `insideWorkspace` resolved a relative path
against the workspace as given. With the workspace or the dataRoot behind
a symlink, the two climb different directories. In Darkwing's layout
(`$R/data -> $R/deep/store`, workspace `$R/data/ws`), `../../data/ws/X` is
`$R/data/ws/X` to the gate and `$R/deep/data/ws/X` to Pi. A real Pi
session read a file there and wrote a new one.
The fix: a relative path must resolve inside from both the workspace's
real path, as Pi resolves it, and the path as given. An absolute path is
unchanged. `spellings()` already built from both bases (R4), so `read`'s
other-spelling check needed no change.
The given-path check is a choice, and it is stricter than Pi. A path that
is inside for Pi but climbs out of the given path is refused, for example
`../../store/ws/x` when the workspace is `$R/a/ws -> $R/deep/store/ws`. I
kept it so the gate stays fail-closed whatever directory it runs in, and
doesn't rest on the adapter's `cd` alone. The cost is that such a path is
refused even though it is safe. A session has no need for it: the plain
relative path or the absolute one works.
### Claude Code isn't affected
Claude Code makes a `file_path` absolute against its own cwd before the
`PreToolUse` hook runs. Its cwd is also the real path, so `claude-gate.mjs`
gets the path Claude Code will open, and the absolute branch checks it.
Darkwing's `claude-cwd-dotdot.sh` shows this: the hook's refusal names
`<R>/deep/data/ws/secret.txt`, not the relative name. My rerun against
round 4 gives the same (below). The new check covers a relative path from
Claude Code too, if a later version passes one through.
### Tests
- `gate.test.mjs`, "a relative path climbs from the workspace's real path,
in both harnesses". Two layouts, each in Pi and Claude Code:
- workspace behind a symlink: `<dir>/a/ws -> <dir>/deep/store/ws`;
- dataRoot behind a symlink: `<dir>/data -> <dir>/deep/store`, workspace
`<dir>/data/workspaces/b/i`, the launcher's shape.
In each, the `../..` escape is inside as given and lands on a planted
file outside for Pi (the test asserts both). Read and write of the escape
are refused. Read and write of `../ws/...` or `../i/...`, through the
symlinked root and back in, are allowed. A path that is inside for Pi
but outside as given is refused.
- `pi-session.test.mjs`, "pi: a relative path climbs from the real path of
a workspace behind a symlink", and the same for a dataRoot. A real Pi
0.85.1 session runs through the adapter and the mock API, in the
symlinked workspace:
- the escape read and write come back as gate errors;
- the outside file's text never reaches stdout;
- the outside directory holds only the planted secret, and no
`planted.txt` appears in the workspace;
- the inside read returns its content, and the inside write lands.
`session()` in that file gains an optional workspace placement for this;
the other tests keep `<dir>/ws`.
### Mutants
Each was made on `gate.mjs`, restored from a copy and checked with `cmp`.
The base passes `gate.test.mjs` and `pi-session.test.mjs`, 19/19.
| Mutant | Change | Result |
|---|---|---|
| given | the given path only (round 3) | 16/3: the gate test and both Pi sessions |
| real | the real path only | 18/1: the gate test's stricter case |
The first draft of the gate test let `real` survive. The stricter case was
added for it.
### Darkwing's probes against round 4
`r3/probe/pi-cwd-dotdot.sh` and `claude-cwd-dotdot.sh`, run unchanged with
`WT` set to the round 4 build, both modes each. Outputs are in
`out/probe-pi-cwd-dotdot.txt` and `out/probe-claude-cwd-dotdot.txt`.
| Probe | Mode | Result |
|---|---|---|
| Pi | read | gate refuses `../../data/ws/secret.txt`; the tool result is the refusal; the secret isn't read |
| Pi | write | gate refuses `../../data/ws/planted.txt`; nothing is written outside or in the workspace |
| Claude Code | read | the hook refuses `<R>/deep/data/ws/secret.txt` (absolute) |
| Claude Code | write | the hook refuses `<R>/deep/data/ws/planted.txt`; nothing is written |
### Darkwing's other round 3 notes
Per Sage's ruling, nothing else changed this round. These go to
follow-ups:
- Mw, Mx, My and Mz survive the spelling tests. Each flips some of
Darkwing's spell-edge cases from refused to allowed: lowercase `am`,
two apostrophes, a path below a respelled self-loop, and the
normalised forms (`@`, NBSP before AM, `file://`, `%27`).
- Darkwing's fix 2: realpath the workspace in the seat's `workspaceDir`
(`packages/seat/src/session.mjs:46`), so a symlinked dataRoot never
reaches the harness as a given path.
- The Ma leftover (`fake-adapter.mjs`) reproduced, and stays a follow-up.
## Round 3
Darkwing's round 2 asked for changes (#1523 comment 27010, rev 259, packet
`agents/darkwing/work/s6-review/review-r2.md` and `r2/`). Sage ruled: fix
R4 in this round; R1 to R3 hold and stay closed. Six files changed since
round 2, and none is new:
- `packages/harness/src/gate.mjs`, `README.md`
- `packages/harness/tests/gate.test.mjs`, `pi-session.test.mjs`
- `packages/cli/tests/host.test.mjs`, `launcher.test.mjs`
No source outside the harness gate changed.
### R4: a read is checked under every spelling Pi's read would open
When the name it is given doesn't exist, Pi 0.85.1's `read` opens another
spelling. `resolveReadPathAsync` (`dist/core/tools/path-utils.js`) tries
these in order, and opens the first that exists:
1. ` AM.` or ` PM.` with U+202F before the `AM`/`PM`;
2. the NFD form;
3. `'` changed to U+2019;
4. both 2 and 3.
The `pi` binary runs the same code from `dist/bundle/`. In 0.85.1 only
`read` and the CLI's file arguments call it. `write`, `edit`, `grep`,
`find` and `ls` take the name as given. The gate checked only the given
name, so asking for `notes's.txt` could read a link `notes’s.txt` that
points outside.
The fix is `otherSpelling` in `gate.mjs`. It covers Pi's `read` and
Claude Code's `Read` (Darkwing's note 3):
- It builds every spelling from the workspace path and from its real
path. The adapter starts Pi with `cd "$MOSAIC_WORKSPACE"`, so Pi resolves
a relative name against the real path.
- A spelling that exists and resolves outside the workspace refuses the
read. So does one that goes through a dangling link.
- A spelling that doesn't exist is skipped, and so is ENOTDIR. Any other
`lstat` error refuses the read.
It doesn't depend on which spelling Pi would open. So a read is refused
even when the exact name exists inside and some spelling points outside.
The README's "Limits" says so, with the example of a sibling `jo’s/ws`
beside a workspace in `jo's/ws`. Its pin note lists what to recheck on a
Pi upgrade.
Tests:
- `gate.test.mjs`, "read is checked under every spelling pi's read would
open, in both harnesses". It asks for six names, relative and absolute,
in both harnesses, covering each family and their mixes:
- straight quote
- AM/PM
- NFD
- NFD with a quote
- NFD with a straight quote kept
- a quote with composed accents
Each is refused when its other spelling links outside. It is allowed
when that spelling links to a file inside. A `write` of the asked name is
allowed.
- `gate.test.mjs`, "other spellings cover directories, dangling links and
pi's cwd". These are refused:
- a directory spelling;
- a dangling spelling;
- a workspace given through a link, whose real path has a sibling
`jo’s/ws`.
A name with no other spelling is allowed, and so is ENOTDIR.
- `pi-session.test.mjs`, "pi: a read is refused when pi would open another
spelling outside". A real Pi 0.85.1 session runs through the adapter and
the mock API. It reads the six names, plus one whose other spelling is
inside:
- each of the six comes back as an error naming the other spelling;
- the outside file's text never reaches stdout;
- the inside one returns its content.
Darkwing's `r2/probe/pi-variant.sh` against this round refuses all four
modes: plain, quote, ampm and nfd. Plain is refused by the existing check.
Mutants: each was made on `gate.mjs`, restored from a copy and checked
with `cmp`. The base passes `gate.test.mjs` and `pi-session.test.mjs`,
16/16.
| Mutant | Change | Result |
|---|---|---|
| none | no other-spelling check | 13/3 |
| pibase | spellings from the given workspace path only | 15/1: Pi's cwd case |
| ampm | no AM/PM spelling | 14/2 |
| nfd | no NFD spelling | 14/2 |
| curly | no U+2019 spelling | 14/2 |
| nfdcurly | no NFD-with-U+2019 spelling | 14/2 |
| pionly | Pi's `read` only, not Claude's `Read` | 15/1 |
| danglingok | a dangling spelling is skipped | 15/1 |
| enotdir | ENOTDIR refuses | 15/1 |
| existsonly | a spelling that is a link is skipped | 13/3 |
The first draft of the tests let `nfd` and `curly` survive. Their names
had both features, so the combined spelling alone caught them. The two
single-feature names were added for that.
### Darkwing's other round 2 notes
1. **Mr.** `host.test.mjs` has a new test, "a broker reply with no
request waiting breaks the channel and the host exits 1". It binds a
coder, then has the broker child send an `identity` reply with an id
that no request waits for. The host logs "broker channel broken (reply
with no request waiting)" and exits 1, and a later op refuses with
`broker-channel-broken`. Under Mr (such a reply is ignored), the test
fails at its 5 s limit. No code changed.
2. **Mv.** Every `launcher.test.mjs` test now has a 30 s timeout. The
longest real run is 3.7 s. Under Mv the file ends in 62 s with 6
failed, 2 cancelled and 1 passed; round 2's run hung.
3. **Claude's `Read`.** It gets the same check (R4 above).
4. **The leftover after Ma.** Not traced this round. It goes to
follow-ups.
5. **Mq** still fails only at the 30 s test timeout. That is a failure,
not a hang, so I left it.
A related leftover, not from this candidate: a `conversation/src/shim.mjs`
from my round 2 gate's conversation suite was still running about an hour
later. It belonged to a removed worktree, under my scratch `TMPDIR`. It
ignored SIGTERM, and I killed it by PID with SIGKILL. The conversation
package is row 40's, so it goes to follow-ups for its owner.
## Round 2
Darkwing's round 1 asked for changes (#1523 comment 26995, rev 247,
packet `agents/darkwing/work/s6-review/` at `f19787ee`). Sage ruled that all three
findings are fixed in this round. R1 sits inside decision 77: it's trusted
IPC, with no socket verb, event kind or schema change. This round adds one
file, `packages/cli/tests/host.test.mjs`, which round 1 didn't touch.
### R1: host↔broker replies carry the request's id
- `host.mjs`: every request gets `id = ++seq`, and the broker process echoes
it on every launch-op, bind and startup reply (`tag()` in
`packages/bus/src/process.mjs`). A reply is taken only if its id is the
waiting request's.
- Any of these breaks the channel (`breakChannel`): no reply within
`REQUEST_TIMEOUT_MS` (10 s), a reply with another id, a reply with no id,
or a reply with no request waiting.
- Once the channel is broken:
- the waiting request refuses with `broker-channel-broken`, and so does
every later one, including those already queued;
- the host logs it and closes with exit 1, for the unit to restart;
- a launch waiting on the channel is refused, never allowed.
- Tests:
- `host.test.mjs` "a broker reply that misses the wait breaks the
channel …". The broker is SIGSTOPped with one request waiting and a
bind queued behind it. Both refuse, the broker is resumed and its late
reply answers nothing, the host exits 1, and a later request refuses.
- "a broker reply with another request's id, or none, breaks the
channel …" changes the id on the way out, so the broker echoes the
wrong one, or drops it.
- `end-launch.test.mjs` checks the echo, refusals included.
- The cli and bus READMEs say this.
Darkwing's `probe/desync.mjs` against this round: after the stall, p1 to p4
all refuse with `broker-channel-broken`, and the host logs "broker channel
broken (no reply within 10 s); stopping the host". The probe's second half
(two binds during a stall) then fails with `kill ESRCH`, because the host
has already stopped the broker. Round 1 cross-wired the replies there
(`b2 bind run-y (reviewer) got: {"run":"run-x"}`).
### R3: the gate refuses dangling symlinks
`gate.mjs` `real()` walks up with `lstat`, not `exists`, so a dangling
symlink counts as an existing name. It then takes the realpath of the
nearest existing ancestor, and an `ENOENT` from `realpathSync` means the
path reaches a dangling link. `decide()` refuses that as "path goes through
a dangling symlink", at any depth, even when the link points inside the
workspace. A write through the link would create its target wherever the
link names, and Pi's `write` creates missing parent directories first. A
link whose target exists is checked as before, so one pointing outside the
workspace is refused as outside it.
- `gate.test.mjs` "a dangling symlink is refused at any depth, in both
harnesses" covers Pi `write` and Claude Code `Write`, by relative and
absolute path. `notes.md` points outside, `dangling` points nowhere and
`inner` points to a missing name inside the workspace. All of
`notes.md`, `dangling/x`, `dangling/deep/x` and `inner` are refused. Once
the targets exist, `notes.md` is refused as outside and `inner` is
allowed. A path under a regular file is refused as `ENOTDIR`.
- `pi-session.test.mjs` "pi: a write through a dangling symlink is blocked,
and nothing appears outside" runs a real Pi session's `write` through the
extension. `notes.md` and `gone/x.md` come back as tool errors, a plain
`fine.md` lands in the workspace, and the outside directory stays empty.
- Claude Code: `--restricted` stays defence in depth. The gate refuses the
path whether or not the flag is set.
Darkwing's probes against this round:
- `pi-dangling.sh`: the dangling link gives "mosaic gate: write path goes
through a dangling symlink: notes.md", and the outside file stays absent.
With the target present, the write is refused as outside the workspace
and the file keeps its content.
- `claude-dangling.sh`: the hook refuses with the same reason, and the
outside file stays absent.
- `gate-edges.mjs`: the output matches round 1 except one line, `write
dangling/x`, which went from ALLOW to the dangling-symlink refusal.
The README's "Limits" gains a TOCTOU line: the gate checks a path when the
call is made, so a link that `bash` or another process of the same user
changes between the check and the open isn't seen. That reach is the same
as `bash` itself.
### R2: a half-open `launch.sock` client can't hold `close()`
The launcher keeps its `launch.sock` connections in a set. `close()` calls
`server.close` and then destroys each connection, before it waits for the
server to close. `launcher.test.mjs` "a launch client that never closes its
side doesn't hold the host's close" opens one client that was answered and
keeps its side open, and one that never sends. `close()` must finish within
5 s. Darkwing's `close-hang.mjs`, in both modes (`silent`, `line`), now
gives `closed 0` about 15 ms after `host.close(0)`. In round 1, the close
waited until the probe destroyed its client at 25 s. The cli README says
so.
### Notes 1 to 5
1. **`--restricted`.** The harness README and the `adapters/claude/adapter.sh`
comment now say that the flag keeps `CLAUDE.md` files (user, parent,
workspace) and auto-memory out of the prompt, and that the gate doesn't
rely on it for paths. Two tests cover it:
- "claude adapter: --restricted is always passed" runs the adapter
against a stand-in `claude` and checks argv. It runs without Claude
Code installed.
- "claude: CLAUDE.md files and auto-memory don't reach the model;
without --restricted they do" plants the four markers and runs the
real adapter against the mock API: none reaches the request. A copy of
the adapter without the flag sends all four.
Darkwing's `claude-memory.sh` gives the same result for this round: 0 of
4 with the candidate, 4 of 4 without the flag.
2. **`launchPm` skips `authorizeLaunch`.** The cli README now says so:
`launches off` doesn't stop `bus start --pm`, because the human launches
the PM, and the other launcher checks still apply.
3. **Policy parse.** `runner.mjs` reads and parses the policy inside the
`try`, so a missing or malformed policy exits 2 (usage) before the claim.
Test: "a missing or malformed policy exits 2 before the claim".
4. **System prompt in argv.** This is now a harness README "Limits" line.
No code change.
5. **Mutants.** Each new test was run against its mutant and fails. Each
source file was restored from a copy and checked with `cmp`.
| Mutant | Change | Round 1 | Round 2 |
|---|---|---|---|
| R1a | the host takes any reply while a request waits | (finding) | "another request's id, or none" fails |
| R1b | no reply timer | (finding) | the stall test fails at its 30 s test timeout instead of hanging |
| R2 | `close()` doesn't destroy connections | (finding) | the half-open test fails within its 5 s bound |
| R3 | round 1's `existsSync` walk in `real()` | (finding) | the gate and pi-session dangling tests fail |
| N1 | the adapter without `--restricted` | (note) | both claude-session tests fail |
| Mg | `founderCheck` returns null | runner test hangs | the unit test fails; the runner test fails at its 60 s clock |
| Mh | no SIGKILL after the stop timeout in `close()` | survives | "a runner that ignores SIGTERM is killed when the host closes" fails |
| Mi | no SIGKILL in `recover()` | survives | the died-hard "kills them" case fails |
| Mj | no 4096-byte stdin cap | survives | "no capability, or a malformed one, on stdin" fails |
| Mk | no `LINE_MAX` on `launch.sock` | survives | "an over-long launch request is refused at once …" fails |
How the new tests separate the mutants:
- **Mh and Mi** rely on a SIGSTOPped process, which ignores SIGTERM and
dies only on SIGKILL.
- **Mi:** in the died-hard test, the leftover session and its runner are
stopped before the next host starts. Without the stop, the runner exits
on its own once the broker is gone, which is why Mi survived round 1.
- **Mh** uses a test-only `stopTimeoutMs` (default 30000), set to 1 s.
- **Mg and Mj:** `startRunner` in `runner.test.mjs` now SIGKILLs a runner
still alive after 60 s. A regression fails there instead of hanging the
file.
- **The stall test** registers its SIGCONT hook before `host.close`. Hooks
run in order, so a stopped broker can't hold the cleanup.
## Files (`files.txt`, 42)
`build.patch` is `git diff --cached --binary 915e00e5` over those files,
+4998/−63. It applies cleanly to `0a4c8f13` with `git apply`, and
`sha256sum -c candidate-manifest.sha256` passes in that tree.
| Area | Files | What |
|---|---|---|
| Harness (new) | `packages/harness/` | bundle, typed tools, gate, Pi extension, Claude Code gate and MCP server, runner, tests, README |
| Adapters | `adapters/claude/adapter.sh` (new), `adapters/pi/adapter.sh`, `adapters/README.md` | Claude Code adapter; Pi takes `MOSAIC_EXTENSIONS` as `-e`, and `--no-approve` |
| Sessions | `packages/seat/src/session.mjs`, `proc.mjs` (new), tests, README | spawn under `unshare`, registry, launch log, `stop` |
| Launcher and verbs | `packages/cli/src/launcher.mjs` (new), `host.mjs`, `cli.mjs`, tests, README | the launch socket in the trusted host; `mosaic talk`, `stop`, `launches off\|on\|list`; `bus start --pm` |
| Broker | `packages/bus/src/{broker,process,runtime}.mjs`, `tests/end-launch.test.mjs`, README | trusted IPC launch ops; `Broker.endLaunch` |
| Other | `docs/TOOLS.md`, `scripts/mosaic`, `scripts/agent-host-dev.sh` | verb reference; host seats pass `--no-approve` (the DEFERRED entry) |
## Against the brief
- **Bundles, Pi then Claude Code** (`packages/harness/README.md`, "The
bundle"): prompt, policy, typed tools and a manifest from one resolved
instance. Each manifest names the S0 lines it relies on (`reliesOn`).
Skills: resolution runs without a skills source, so bundles list none
(README "Limits").
- **S0 lines** (README table): Pi blocks in a `tool_call` handler, a throw
blocks, a missing `-e` refuses to start, and a hang is bounded by the
runner's wall clock plus the `agent_end` turn marker. Claude Code uses a
command hook run as `timeout -k 2 10 <gate> || exit 2` with hook timeout
20; `--bare` is never passed. `--restricted` is defence in depth for the
gate, and it keeps `CLAUDE.md` files and auto-memory out of the prompt.
Line 5 (`bash`) is the tool limit and is written as a limit.
- **The PM launches through the broker, within `launch`**: the host's
launch socket checks the instance list, that the instance isn't already
running, the model family against `launch.max` (every running session
counts, the PM's too), then the broker's own `role.launch` authorization.
Every launch, refusal and end is a launch-log line and a broker event.
`mosaic launches off` is Jason's one word (`launch.revoke`). `mosaic stop
<run>` ends a session.
- **Founder credentials (REQ-CRED-2)**: the session environment is an
allowlist (`ENV_ALLOW`). The runner exits 20 before claiming if a known
founder variable reached it, or if a service the role needs has no usable
role token (the broker's credential status, metadata only).
- **The PM moves off T3**: `mosaic bus start <business> --pm` launches the
business's `launch.by` instance without a window, and `mosaic talk`
reaches it. The handover of Sage's T3 thread is the acceptance run below.
## Changes from the plan
1. **The capability goes to the runner as one stdin line**, not a 0600
file. It is written after the bind and is never on disk, in argv or in
the environment.
2. **Verbs live in `packages/cli`**, not `packages/seat` (lead decision
77). `packages/seat` keeps the session module and the moved `/proc`
helpers (`proc.mjs`).
3. **`--pm` takes no argument.** The PM is the business file's `launch.by`
instance; its harness and model come from resolution.
4. **Runner exit 23** covers a `role.claim` with no answer as well as
`brokerRetries` failed polls.
5. **The adapter runs as `/bin/sh <adapter.sh>`**, because the repository
keeps adapters 0644 and only the image sets the mode.
## Broker surface
No socket verb, no event kind and no schema change. The new launch ops are
trusted IPC from the host to the broker process: `identity`,
`authorizeLaunch`, `refuse`, `endLaunch`, `credentialStatus`, beside the
existing `bindLaunch`. `bindLaunch` records the run only after its checks,
and a rebind of an ended run refuses with `run-ended`, also across a broker
restart. The host gains `op(message)` and `beforeClose(fn)`.
## Process control
- **Early signals.** The runner installs its SIGTERM and SIGINT handlers
before anything else, because pid 1 of a PID namespace ignores a signal
with no handler. Node's own startup still leaves a window, so the
launcher and `mosaic stop` resend SIGTERM every second.
- **`isRunner(pid)`** is `cmdline[1] === RUNNER`, which skips `unshare`'s
forked child before its exec (its argv still names the runner).
- **Host lost.** A starting host reads the last host's `sessions.json`.
Before the launch socket opens, for each entry of this business it
SIGKILLs the session if it still runs, rebinds the run with the recorded
identity and ends it as `host-lost`, which releases the role. A rebind
that refuses with `run-ended` logs "was already ended". An unreadable or
malformed `sessions.json` refuses the host start with exit 3.
- **PID namespace limits** are in `packages/harness/README.md`, "Limits":
same UID, shared broker socket and `/tmp`, network not confined, and a
same-UID process can still ask something outside its tree (a systemd user
manager, an existing tmux server) to run a command. The README no longer
says the namespace blocks `ptrace` in general; it hides other sessions by
pid.
## `--no-approve` (DEFERRED: "Host seats launch Pi with `--approve`")
`scripts/agent-host-dev.sh` and `adapters/pi/adapter.sh` now pass
`--no-approve`. Pi 0.85.1 (`trust-manager.js`) gates project `.pi/`
resources on trust: `settings.json`, extensions, skills, prompts, themes,
`SYSTEM.md`, `APPEND_SYSTEM.md`. `-e` paths load in the "temporary" scope,
which trust doesn't gate. A scratch probe against a workspace saved as
trusted, with a mock Messages API, showed:
| argv | project `SYSTEM.md` | explicit `--skill` | generated prompt | `-e` extension |
|---|---|---|---|---|
| host-seat, `--approve` | loaded | loaded | loaded | loaded |
| host-seat, `--no-approve` | ignored | loaded | loaded | loaded |
| `adapters/pi/adapter.sh` | ignored | loaded | loaded | loaded |
The skill appears only when the session has a tool to read it; the probe's
first run used `--no-tools` and showed no skill in any case, the
`--approve` control included.
`agent-host-dev.sh` keeps `--append-system-prompt`, not the entry's
"explicit system prompt", because its comment preserves Pi's built-in coding
prompt on purpose, and `--no-approve` already closes the project
`SYSTEM.md`. I didn't edit `docs/plans/DEFERRED.md`: it holds another
seat's uncommitted changes. The entry can close when this lands.
`scripts/test-goal-native.py` still passes `--approve` on purpose (it tests
project extensions) and is untouched.
## Gate
Run at `0a4c8f13` with this round's patch applied, in a detached worktree.
`sha256sum -c candidate-manifest.sha256` passed there (42 OK). Suites ran
sequentially, each output in `out/` (`out/summary.txt`). `TMPDIR` was on
the scratch disk and `DOCKER_HOST=unix:///nonexistent.sock`, so nothing
reached Docker. Round 1's outputs stay in this directory at `9b670e27`,
round 2's at `779e9780` and round 3's at `0f38236f`. Darkwing's R5 probe
outputs from round 4 are `out/probe-pi-cwd-dotdot.txt` and
`out/probe-claude-cwd-dotdot.txt`.
| Suite | Pass | Fail |
|---|---|---|
| node: bus, business, cli, control-board | 74, 60, 83, 124 | 0 |
| node: conversation, discord, harness, ledger | 161, 178, 56, 78 | 0 |
| node: mosaic, queue, seat, tasks, webui | 69, 148, 27, 51, 22 | 0 |
| test-auth, conductor, config, discord | 15, 17, 24, 66 | 0 |
| test-extension-package, foundation, queue, release | 18, 44, 27, 4 | 0 |
| test-task | 26 | 2 |
Against round 3, harness gains 3 tests (R5). Conversation (161) and webui
(22) grew because row 40 landed (08b428ec); none of their files is in this
candidate.
The two `test-task` failures are "user recall run succeeds (exit 1)" and
"recalled user name". That check runs a live worker and needs Docker. The
unpatched base at `0a4c8f13` fails the same two (`out/base-test-task.txt`,
identical PASS/FAIL lines), so they come from the environment, not this
candidate. No test process was left running after the gate.
## Acceptance run: waiting
The recorded run (the host started with `--pm`, `mosaic talk` asks the PM
to launch a coder, `mosaic agents` shows both claims) waits on the Vikunja
move to tasks.woltje.com (Q14), as Sage ruled: the integration commit and
the acceptance run wait for that cutover; build and review continue. It
will not run on Astra (R26) or against the live unit. The tracker host
comes only from `vars.tracker.baseUrl`; no code, test or launcher config
names one, and fixtures use 127.0.0.1 and example.test.
## Follow-ups (not in this row)
- `packages/queue/tests/commit.test.mjs`, `pausedCommit`, awaits the
child's `exit` rather than `close`, so its stderr can be unread when
`test-queue` F1 matches it.
- `scripts/test-task.sh` (:514-518): the live user-recall check needs
Docker and isn't skipped when Docker is unavailable.
- Darkwing's round 2 note 4: after the Ma mutant's harness run, a
`fake-adapter.mjs` was left running. Which test spawned it, and whether
that test cleans up when it fails, isn't traced.
- `packages/conversation` (row 40): a `shim.mjs` from a gate's
conversation suite outlived the run, and it ignored SIGTERM.
- Darkwing's round 3 survivors Mw, Mx, My and Mz in the gate's spelling
check (lowercase `am`, two apostrophes, a path below a respelled
self-loop, the normalised forms). Each needs a test; Sage kept them out
of round 4.
- Realpath the workspace in the seat's `workspaceDir`
(`packages/seat/src/session.mjs:46`), Darkwing's round 3 fix 2.