From 9b670e27b2681c139ed9cb09cfbc5c8f4ab02f3f Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 9 Oct 2026 19:56:01 -0500 Subject: [PATCH] docs(s6): row 41 slice 1 S6 round 1 candidate packet (filbert) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build.patch dc346027…4454, candidate-manifest 5b3a934d…27f3, base 915e00e5, 41 files, +4302/-55. Gate at 2855e628 in out/: every node suite and every test-*.sh green except test-task's live recall (needs Docker; the base fails the same two). The recorded PM launch waits on the Q14 cutover. Co-Authored-By: Claude Opus 5.5 --- agents/filbert/work/s6/BUILD.md | 169 + agents/filbert/work/s6/base.txt | 1 + agents/filbert/work/s6/build.patch | 4870 +++++++++++++++++ .../filbert/work/s6/candidate-manifest.sha256 | 41 + agents/filbert/work/s6/files.txt | 41 + agents/filbert/work/s6/out/base-test-task.txt | 33 + agents/filbert/work/s6/out/node-bus.txt | 82 + agents/filbert/work/s6/out/node-business.txt | 68 + agents/filbert/work/s6/out/node-cli.txt | 87 + .../work/s6/out/node-control-board.txt | 132 + .../filbert/work/s6/out/node-conversation.txt | 160 + agents/filbert/work/s6/out/node-discord.txt | 186 + agents/filbert/work/s6/out/node-harness.txt | 53 + agents/filbert/work/s6/out/node-ledger.txt | 86 + agents/filbert/work/s6/out/node-mosaic.txt | 77 + agents/filbert/work/s6/out/node-queue.txt | 158 + agents/filbert/work/s6/out/node-seat.txt | 35 + agents/filbert/work/s6/out/node-tasks.txt | 59 + agents/filbert/work/s6/out/node-webui.txt | 23 + agents/filbert/work/s6/out/summary.txt | 23 + agents/filbert/work/s6/out/test-auth.txt | 17 + agents/filbert/work/s6/out/test-conductor.txt | 23 + agents/filbert/work/s6/out/test-config.txt | 26 + agents/filbert/work/s6/out/test-discord.txt | 70 + .../work/s6/out/test-extension-package.txt | 21 + .../filbert/work/s6/out/test-foundation.txt | 53 + agents/filbert/work/s6/out/test-queue.txt | 35 + agents/filbert/work/s6/out/test-release.txt | 7 + agents/filbert/work/s6/out/test-task.txt | 33 + agents/filbert/work/s6/packet-manifest.sha256 | 29 + 30 files changed, 6698 insertions(+) create mode 100644 agents/filbert/work/s6/BUILD.md create mode 100644 agents/filbert/work/s6/base.txt create mode 100644 agents/filbert/work/s6/build.patch create mode 100644 agents/filbert/work/s6/candidate-manifest.sha256 create mode 100644 agents/filbert/work/s6/files.txt create mode 100644 agents/filbert/work/s6/out/base-test-task.txt create mode 100644 agents/filbert/work/s6/out/node-bus.txt create mode 100644 agents/filbert/work/s6/out/node-business.txt create mode 100644 agents/filbert/work/s6/out/node-cli.txt create mode 100644 agents/filbert/work/s6/out/node-control-board.txt create mode 100644 agents/filbert/work/s6/out/node-conversation.txt create mode 100644 agents/filbert/work/s6/out/node-discord.txt create mode 100644 agents/filbert/work/s6/out/node-harness.txt create mode 100644 agents/filbert/work/s6/out/node-ledger.txt create mode 100644 agents/filbert/work/s6/out/node-mosaic.txt create mode 100644 agents/filbert/work/s6/out/node-queue.txt create mode 100644 agents/filbert/work/s6/out/node-seat.txt create mode 100644 agents/filbert/work/s6/out/node-tasks.txt create mode 100644 agents/filbert/work/s6/out/node-webui.txt create mode 100644 agents/filbert/work/s6/out/summary.txt create mode 100644 agents/filbert/work/s6/out/test-auth.txt create mode 100644 agents/filbert/work/s6/out/test-conductor.txt create mode 100644 agents/filbert/work/s6/out/test-config.txt create mode 100644 agents/filbert/work/s6/out/test-discord.txt create mode 100644 agents/filbert/work/s6/out/test-extension-package.txt create mode 100644 agents/filbert/work/s6/out/test-foundation.txt create mode 100644 agents/filbert/work/s6/out/test-queue.txt create mode 100644 agents/filbert/work/s6/out/test-release.txt create mode 100644 agents/filbert/work/s6/out/test-task.txt create mode 100644 agents/filbert/work/s6/packet-manifest.sha256 diff --git a/agents/filbert/work/s6/BUILD.md b/agents/filbert/work/s6/BUILD.md new file mode 100644 index 00000000..c0201af8 --- /dev/null +++ b/agents/filbert/work/s6/BUILD.md @@ -0,0 +1,169 @@ +# Row 41 (#1523): slice 1 S6, candidate packet, round 1 + +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. Base: `915e00e5` (`base.txt`). None of +the 41 files changed between the base and `2855e628`, where the 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/`. + +## Files (`files.txt`, 41) + +`build.patch` is `git diff --cached --binary 915e00e5` over those files, ++4302/−55. It applies cleanly to `2855e628` with `git apply --index`, 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 || exit 2` with hook timeout + 20; `--bare` is never passed. `--restricted` is defence in depth only. + 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 + ` 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 --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 `**, 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 `2855e628` with `build.patch` applied, in a detached worktree, +sequentially, each output in `out/` (`out/summary.txt`). `TMPDIR` on the +scratch disk; `DOCKER_HOST=unix:///nonexistent.sock`, so nothing reaches +Docker. + +| Suite | Pass | Fail | +|---|---|---| +| node: bus, business, cli, control-board | 74, 60, 77, 124 | 0 | +| node: conversation, discord, harness, ledger | 152, 178, 45, 78 | 0 | +| node: mosaic, queue, seat, tasks, webui | 69, 148, 27, 51, 14 | 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 | + +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 fails the same two (`out/base-test-task.txt`, identical +PASS/FAIL lines), so they are the environment, not this candidate. + +An earlier gate over this candidate without the `--no-approve` edits had +`test-queue` fail F1 once ("a plain `commit -e` whose guard ran before +update-ref fails at its own HEAD update": only the stderr match). It passed +in two patched reruns, in the base run, and in this gate. See the +follow-ups below. + +## 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. diff --git a/agents/filbert/work/s6/base.txt b/agents/filbert/work/s6/base.txt new file mode 100644 index 00000000..71bfd382 --- /dev/null +++ b/agents/filbert/work/s6/base.txt @@ -0,0 +1 @@ +915e00e548439140efc838e2b75aba3ef971cbf4 diff --git a/agents/filbert/work/s6/build.patch b/agents/filbert/work/s6/build.patch new file mode 100644 index 00000000..48402e9a --- /dev/null +++ b/agents/filbert/work/s6/build.patch @@ -0,0 +1,4870 @@ +diff --git a/adapters/README.md b/adapters/README.md +index 33ed6ded..a9460c3f 100644 +--- a/adapters/README.md ++++ b/adapters/README.md +@@ -43,8 +43,11 @@ Optional, adapter-specific (documented per adapter): + 1. Adapters print ONLY the response on stdout. Status lines go to stderr. + 2. Adapters never read configuration files; the resolved settings arrive via environment. + 3. Adapters never write outside `/var/lib/mosaic`. +-4. Adding an adapter requires: a new directory, the contract implementation, and +- adding the name to the allowlist in `scripts/mosaic-config.mjs`. ++4. Adding a worker adapter requires: a new directory, the contract ++ implementation, and adding the name to the allowlist in ++ `scripts/mosaic-config.mjs`. A managed-session adapter (below) is selected ++ by the launch bundle, not by `execution.adapter`, and is not on that ++ allowlist unless it also works as a worker adapter. + + ## Included adapters + +@@ -52,3 +55,27 @@ Optional, adapter-specific (documented per adapter): + print mode (`-p`), ambient discovery disabled, stdin detached. + - `mock` — deterministic echo of `MOSAIC_MOCK_RESPONSE`. Test-only: never use + it where a real model response is required. ++- `claude` — the host's `claude` CLI, for managed sessions only (below). ++ Not a worker adapter: the image has no `claude`, and the adapter refuses ++ without the bundle's hook and MCP files. ++ ++## Managed sessions (slice 1 S6) ++ ++The session runner (`packages/harness/src/runner.mjs`) runs one adapter ++call per turn on the host, as `/bin/sh /adapter.sh>` (the ++repository keeps adapters 0644; only the image sets the mode), in the ++session's workspace and its own process group. Same contract, plus: ++ ++| Variable | Meaning | ++|---|---| ++| `MOSAIC_WORKSPACE` | The session's workspace; the adapter runs there. | ++| `MOSAIC_SESSION_DIR` | The session's persistent directory; later turns resume the session kept there. | ++| `MOSAIC_TOOLS` | The built-in tool limit (pi names for `pi`, Claude Code names for `claude`), comma-separated. | ++| `MOSAIC_POLICY_FILE`, `MOSAIC_TOOLS_FILE` | The bundle's gate policy and typed tools. | ++| `MOSAIC_TOOL_SOCKET` | The runner's tool socket, which the typed tools call. | ++| `MOSAIC_TURN_MARKER` | `pi`: the extension writes it at `agent_end`; a pi turn that exits 0 without it failed. | ++| `MOSAIC_EXTENSIONS` | `pi` only: extension files loaded with `-e`; a missing one exits 2. | ++| `MOSAIC_CLAUDE_SETTINGS`, `MOSAIC_CLAUDE_MCP_CONFIG` | `claude` only: the bundle's gate hook and MCP server; required. | ++ ++The layers each adapter relies on, and what they don't cover, are in ++`packages/harness/README.md`. +diff --git a/adapters/claude/adapter.sh b/adapters/claude/adapter.sh +new file mode 100644 +index 00000000..2e29fe25 +--- /dev/null ++++ b/adapters/claude/adapter.sh +@@ -0,0 +1,77 @@ ++#!/bin/sh ++# Claude Code adapter: implements the Mosaic adapter contract for the host's ++# `claude` CLI, for managed sessions (slice 1 S6). Headless only. ++# ++# Contract: see adapters/README.md. ++# stdout = the turn's answer only; stderr = diagnostics; exit 0 on success. ++# ++# The layers this relies on are row S0 lines 1-5 (docs/plans/2026-10-04_slice-1.md, ++# "What S6 can rely on"): the PreToolUse command hook in MOSAIC_CLAUDE_SETTINGS, ++# wrapped as `timeout -k 2 10 || exit 2`, and the tool limit (--tools). ++# --bare is never passed: it turns settings hooks off. ++set -eu ++ ++need() { ++ eval "v=\${$1:-}" ++ [ -n "$v" ] || { echo "claude adapter: $1 is required" >&2; exit 2; } ++} ++need MOSAIC_SYSTEM_PROMPT_FILE ++need MOSAIC_REQUEST ++need MOSAIC_WORKSPACE ++need MOSAIC_SESSION_DIR ++need MOSAIC_MODEL ++need MOSAIC_CLAUDE_SETTINGS ++need MOSAIC_CLAUDE_MCP_CONFIG ++for f in "$MOSAIC_SYSTEM_PROMPT_FILE" "$MOSAIC_CLAUDE_SETTINGS" "$MOSAIC_CLAUDE_MCP_CONFIG"; do ++ [ -r "$f" ] || { echo "claude adapter: not readable: $f" >&2; exit 2; } ++done ++[ "${MOSAIC_INTERACTIVE:-}" != "1" ] || { echo "claude adapter: interactive mode isn't supported" >&2; exit 2; } ++ ++mkdir -p "$MOSAIC_WORKSPACE" "$MOSAIC_SESSION_DIR" ++cd "$MOSAIC_WORKSPACE" ++ ++# Session: the first turn names a new session id, later turns resume it. ++# The id is kept only after a turn succeeds, so a failed first turn never ++# leaves a --resume of a session that doesn't exist. ++ID_FILE="$MOSAIC_SESSION_DIR/claude-session-id" ++if [ -s "$ID_FILE" ]; then ++ SESSION_FLAG=--resume ++ SESSION_ID=$(cat "$ID_FILE") ++else ++ SESSION_FLAG=--session-id ++ SESSION_ID=$(cat /proc/sys/kernel/random/uuid) ++fi ++ ++# Flags: ++# -p one-shot: print the answer and exit ++# --output-format text stdout is the answer only ++# --system-prompt the generated prompt replaces the default ++# --restricted no user/project/local settings files; --settings ++# (the gate hook) still applies; no code-running ++# tool unless --tools names it; file tools confined ++# to the working directory. Defence in depth: the ++# S0 lines above don't depend on it. ++# --tools the built-in tool limit (S0 line 5); empty = none ++# --allowedTools the same tools plus the mosaic MCP server, so ++# --permission-mode dontAsk nothing waits on a prompt and anything else is denied ++# --settings the gate hook (S0 lines 1-4) ++# --strict-mcp-config only the bundle's MCP server, which serves the ++# --mcp-config typed bus tools ++# --disable-slash-commands no skills (the bundle lists none) ++ALLOWED=mcp__mosaic ++[ -z "${MOSAIC_TOOLS:-}" ] || ALLOWED="$MOSAIC_TOOLS,mcp__mosaic" ++PROMPT_CONTENT="$(cat "$MOSAIC_SYSTEM_PROMPT_FILE")" ++claude -p "$MOSAIC_REQUEST" \ ++ --output-format text \ ++ --system-prompt "$PROMPT_CONTENT" \ ++ --model "$MOSAIC_MODEL" \ ++ --restricted \ ++ --tools "${MOSAIC_TOOLS:-}" \ ++ --allowedTools "$ALLOWED" \ ++ --permission-mode dontAsk \ ++ --settings "$MOSAIC_CLAUDE_SETTINGS" \ ++ --strict-mcp-config \ ++ --mcp-config "$MOSAIC_CLAUDE_MCP_CONFIG" \ ++ --disable-slash-commands \ ++ "$SESSION_FLAG" "$SESSION_ID" || exit $? ++[ "$SESSION_FLAG" = --resume ] || printf '%s\n' "$SESSION_ID" > "$ID_FILE" +diff --git a/adapters/pi/adapter.sh b/adapters/pi/adapter.sh +index 13c9da51..3ebb1efe 100644 +--- a/adapters/pi/adapter.sh ++++ b/adapters/pi/adapter.sh +@@ -60,6 +60,19 @@ if [ -n "${MOSAIC_SKILLS:-}" ]; then + IFS=$OLDIFS + fi + ++# Extensions (S6): explicitly provided extension files, loaded with -e ++# after --no-extensions turns discovery off. A missing file refuses here; ++# pi itself also refuses to start on a missing or broken -e (row S0 line 3). ++EXT_FLAGS="" ++if [ -n "${MOSAIC_EXTENSIONS:-}" ]; then ++ OLDIFS=$IFS; IFS=',' ++ for e in $MOSAIC_EXTENSIONS; do ++ [ -f "$e" ] || { echo "pi adapter: extension missing: $e" >&2; exit 2; } ++ EXT_FLAGS="$EXT_FLAGS -e $e" ++ done ++ IFS=$OLDIFS ++fi ++ + # Mode (M13): interactive TUI or one-shot print. + PRINT_MODE="-p" + REQUEST_ARG="" +@@ -74,13 +87,18 @@ fi + # interactive TUI mode) + # --system-prompt replace the default prompt with the generated one + # --no-* no ambient context/skills/extensions/templates/themes ++# EXT_FLAGS the explicitly provided extensions only (-e) + # SESSION_FLAGS ephemeral | persistent | forked (per env) + # TOOLS_FLAG per capabilities + # --offline no startup network operations (update checks/telemetry) ++# --no-approve ignore project-local .pi/ files (settings, SYSTEM.md) ++# whatever trust is saved for the workspace + PROMPT_CONTENT="$(cat "$MOSAIC_SYSTEM_PROMPT_FILE")" + set -- \ + --offline \ ++ --no-approve \ + --no-extensions \ ++ $EXT_FLAGS \ + $SKILLS_FLAG \ + --no-prompt-templates \ + --no-themes \ +diff --git a/docs/TOOLS.md b/docs/TOOLS.md +index e07eabfa..b96e9012 100644 +--- a/docs/TOOLS.md ++++ b/docs/TOOLS.md +@@ -196,7 +196,11 @@ config problem · `4` usage or a required file missing. Details: + scripts/mosaic inbox | tasks | agents [--business ] [--json] + scripts/mosaic decide