151 lines
9.1 KiB
Markdown
151 lines
9.1 KiB
Markdown
# 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/<name>/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 <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_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, `<run>/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 <status>`). 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 <path>`
|
|
and `claude` with `--system-prompt-file <path>` (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/<pid>/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.
|