feat(harness): meta-harness and PM launching off T3 (#1523, row 41)

Slice 1 S6. packages/harness runs a role session under the pi or claude
adapter behind a tool gate, and packages/seat holds the session and its
process. The bus host gains a launch socket: `mosaic bus start
<business> --pm <harness>:<model>` runs the PM without a window, the PM
launches role sessions through the broker within the business file's
`launch.max`, and every launch, refusal and end is a launch-log line and
a broker event. `mosaic talk`, `mosaic launches off|on|list` and
`mosaic stop <run>` are new CLI verbs. packages/bus gains the IPC ops
identity, authorize, refuse, endLaunch and credentialStatus, plus
Broker.endLaunch (decision 77); no socket verb, event kind or schema
changed. The session environment is an allowlist, and the runner exits
before claiming if a founder credential reaches it.

Filbert built it (agents/filbert/work/s6). Darkwing reviewed on #1523:
round 1 changes (26991/26995), round 2 changes (27004/27010), round 3
changes (27012/27032), round 4 approve (27033/27034) on manifest
08a78972, 42 paths.

Sage's gate on 64b0f9f1 (12:32Z), sequential and teed: node suites
business 60, bus 74, cli 83, control-board 124, conversation 182,
discord 178, harness 56, ledger 78, mosaic 69, queue 149, runs 41,
seat 27, tasks 51, webui 39, every one 0 failed; test-auth 15,
test-conductor 17, test-config 24, test-discord 66,
test-extension-package 18, test-foundation 44, test-queue 27,
test-release 14, test-task 98 and 26 without Docker, all 0 failed.
64b0f9f1..ee7f909b touches none of the 42 paths. It landed after the
Q14 hold ended at 2026-10-11T17:19:23.857Z. The recorded PM-to-coder
run on a scratch data root is still owed before row 41 moves to done.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-11 12:22:50 -05:00
co-authored by Claude Opus 5.5
parent ee7f909bd3
commit fe5c68fa03
42 changed files with 4998 additions and 63 deletions
+29 -2
View File
@@ -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 <adapters/<name>/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`.
+79
View File
@@ -0,0 +1,79 @@
#!/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 <gate> || 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; no CLAUDE.md file or
# auto-memory in the prompt. The S0 lines above
# don't depend on it, but it is what keeps founder
# and repository memory out; a test fails without 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"
+18
View File
@@ -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 \