# Mosaic runtime adapters An adapter is the entire harness-specific surface of the system. Everything upstream of an adapter — configuration, contracts, missions, tasks, run records — is harness-agnostic; everything inside an adapter may assume one specific agent runtime. ## Contract An adapter lives at: ```text /opt/mosaic/adapters//adapter.sh ``` and must be executable. The dispatcher (`/opt/mosaic/src/run-agent.sh`) selects it via `MOSAIC_ADAPTER` (default: `pi`) and execs it after the system prompt has been generated. **Inputs (environment):** | Variable | Meaning | |---|---| | `MOSAIC_SYSTEM_PROMPT_FILE` | Absolute path to the generated per-container system prompt (contracts + optional mission section). Read it; do not modify it. | | `MOSAIC_REQUEST` | The exact user request text (may contain newlines). | | `MOSAIC_PROVIDER` | Configured provider name. | | `MOSAIC_MODEL` | Configured model id. | Optional, adapter-specific (documented per adapter): | Variable | Meaning | |---|---| | `MOSAIC_MOCK_RESPONSE` | mock only: the verbatim response to emit | **Outputs:** - `stdout`: the model response text — the only channel the orchestrator captures - `stderr`: diagnostics (never credentials) - exit `0`: success; nonzero: failure ## Rules 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 write only in the selected workspace/session mounts or private per-container state; input mounts are read-only and the data-root root is absent. 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 - `pi` — the pinned `@earendil-works/pi-coding-agent` CLI in noninteractive 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): `MOSAIC_CLAUDE_BIN` when set, else `claude` on `PATH`. 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_MODEL_ACTIVITY_DIR` | Managed runner's existing writable run directory. Trusted model streaming events publish the fixed `output\n` token to `proxy-first-output` (0600, exclusive create). No event contents or credentials enter it. Claude's private stream reader and Pi's extension signal thinking, tool-use and text activity before final stdout; init/status, tool results and workspace writes do not count. | | `MOSAIC_MODEL_ACTIVITY_SOURCE` | The harness module may set `adapter` in its bundle environment to declare model events authoritative and disable the runner's stdout activity fallback. Claude declares this because final stdout can be a synthetic error. Pi and mock retain the fallback. The runner reads the declaration from module metadata, ignoring any ambient environment copy. | | `MOSAIC_EXTENSIONS` | `pi` only: extension files loaded with `-e`; a missing one exits 2. The pi module names two: the gate and typed tools extension, then the unslop check's `tools/unslop-hook/extension.ts` (row 66). | | `MOSAIC_UNSLOP_LOG` | The unslop check's JSONL log, `/unslop.jsonl`, which the runner sets for both adapters and reads after each turn (row 66). | | `MOSAIC_CLAUDE_SETTINGS`, `MOSAIC_CLAUDE_MCP_CONFIG` | `claude` only: the bundle's gate hook and MCP server; required. | | `MOSAIC_THINKING` | The route's thinking level (row 65), which the harness module's `levels()` already checked. `pi` passes `--thinking` (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`) and omits it when unset, as for `run-task.sh` workers. `claude` passes `--effort` (`low`, `medium`, `high`, `xhigh`, `max`) and requires it; unset exits 2. | | `MOSAIC_CLAUDE_BIN` | `claude` only: absolute path of the CLI to run, from the host's environment (the bus unit pins it). Unset or empty runs `claude` from `PATH`; a relative path or a non-executable file exits 2. | The layers each adapter relies on, and what they don't cover, are in `packages/harness/README.md`. Claude uses `--output-format stream-json --verbose --include-partial-messages`. The adapter's `stream.mjs` consumes those events and prints a complete final result, including error results, with the same newline as the CLI's text format. CLI failures retain their nonzero exit status. Claude's model events alone mark activity; its final stdout may be a synthetic error. The runner retains stdout activity marking for Pi and mock adapters. Malformed or incomplete streams emit only the fixed diagnostic and fail the turn; a failed or interrupted first turn does not persist a reusable session ID. The helper is mounted read-only and its digest is recorded in the launch bundle. A private completion pipe separates handled CLI exits from helper launch/runtime failures, even if their exit codes collide. The helper acknowledges only after its answer has been handed to stdout, and the token names its exit status (`complete `). The adapter emits the fixed diagnostic if completion is absent, the stream is invalid, or the helper's actual exit differs from the acknowledged status (the helper died while its answer was draining), suppressing helper stacks and shell crash text. CLI stderr keeps its existing channel; the CLI does not inherit the completion pipe. No extra writable mount or completion file is needed. Pi retains print mode and signals activity through its existing extension. Mock remains a test-only echo. ### A role's skills (row 64) A managed session's skills reach both adapters as text in the bundle's `prompt.md`, which they pass as a file: `pi` with `--system-prompt ` and `claude` with `--system-prompt-file ` (row 213). Neither adapter loads a skill of its own: `pi` keeps `--no-skills`, since role sessions never get `MOSAIC_SKILLS`, and `claude` keeps `--disable-slash-commands` and `--restricted`. Sage chose this on 2026-10-19 from a probe, recorded in `agents/sage/work/queue-64/probe.md`: - Pi's `--skill` puts only a skill's name and description in the prompt, and the model has to `read` the SKILL.md itself. The gate refuses every file-tool path outside the workspace, so the session couldn't open its own skills. Opening the gate to the bundle would change a security boundary. - Claude Code loads a bundle-local `--plugin-dir` skill under `--restricted` only with its `Skill` tool on, and then it also offers its own bundled skills. Which ones it offers changes with the Claude Code version, and the `Skill` tool would pass through the gate hook. The cost is prompt size: a role's skill text is in every turn, where the other routes load a skill when it's used. The bundle caps a role's skills at 64 KiB together. The cap dates from when `claude` passed `prompt.md` as one argument, which Linux caps at 128 KiB; row 213 passes the file instead, so no local user reads the prompt in `/proc//cmdline`. Pi 0.85.1 uses the path string itself as the prompt when the file is missing or can't be read, so the pi adapter exits 2 unless the path is a readable regular file. ### The unslop check (row 66) Both adapters run the mechanical writing check from `tools/unslop-hook`, driven by one `lists.json`. `pi` loads it as its second `-e` extension. `claude` runs `claude-hook.js` from the bundle's `settings.json`: the Stop hook checks the turn's replies, and the UserPromptSubmit hook adds the correction notice as context on the next turn when the last reply had tells. Each hook command ends `; exit 0`, so neither blocks. Both log to `MOSAIC_UNSLOP_LOG`, and the runner adds a note for the operator to the next RESULT it sends (`packages/harness/README.md`, the runner). The harness modules mount the three files read-only at their own paths, and the manifest records their digests. `MOSAIC_UNSLOP_HOOK` and `UNSLOP_LISTS` aren't in the session environment's allowlist, so a managed session can't switch the check off or swap its lists.