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..8a259540 --- /dev/null +++ b/adapters/claude/adapter.sh @@ -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 || 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" 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