#!/bin/sh # Claude Code adapter: implements the Mosaic adapter contract for the host's # `claude` CLI, for managed sessions (slice 1 S6). Headless only. The CLI is # MOSAIC_CLAUDE_BIN when set (the bus unit pins it), else `claude` on PATH. # # 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 STREAM="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/stream.mjs" 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_THINKING 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 PROMPT_FILE=$MOSAIC_SYSTEM_PROMPT_FILE case "$PROMPT_FILE" in /*) ;; *) PROMPT_FILE="$(pwd)/$PROMPT_FILE" ;; esac [ "${MOSAIC_INTERACTIVE:-}" != "1" ] || { echo "claude adapter: interactive mode isn't supported" >&2; exit 2; } CLAUDE=claude if [ -n "${MOSAIC_CLAUDE_BIN:-}" ]; then case "$MOSAIC_CLAUDE_BIN" in /*) ;; *) echo "claude adapter: MOSAIC_CLAUDE_BIN must be an absolute path: $MOSAIC_CLAUDE_BIN" >&2; exit 2 ;; esac [ -f "$MOSAIC_CLAUDE_BIN" ] && [ -x "$MOSAIC_CLAUDE_BIN" ] || { echo "claude adapter: MOSAIC_CLAUDE_BIN is not an executable file: $MOSAIC_CLAUDE_BIN" >&2; exit 2; } CLAUDE=$MOSAIC_CLAUDE_BIN fi 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 stream-json private model events; stream.mjs emits only # --verbose --include-partial-messages the final result on stdout and marks # first model activity before long tool work # --system-prompt-file the generated prompt replaces the default, given # as a file so no local user reads it in # /proc//cmdline (row 213); absolute, since # the cd above changed the working directory # --effort the instance's thinking var (row 65; 2.1.302 takes # low, medium, high, xhigh, max) # --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 skill discovery; the role's skills are # text in the system prompt (row 64) ALLOWED=mcp__mosaic [ -z "${MOSAIC_TOOLS:-}" ] || ALLOWED="$MOSAIC_TOOLS,mcp__mosaic" # Completion travels on fd 3, independently of stdout and the CLI exit code. # fd 4 preserves answer stdout; fd 5 preserves CLI stderr while helper/shell # crashes stay private. The CLI never inherits the completion descriptor. HELPER_STATUS=0 { HELPER_COMPLETION=$( { node "$STREAM" "$CLAUDE" -p "$MOSAIC_REQUEST" \ --output-format stream-json --verbose --include-partial-messages \ --system-prompt-file "$PROMPT_FILE" \ --model "$MOSAIC_MODEL" \ --effort "$MOSAIC_THINKING" \ --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" 3>&1 1>&4 4>&-; } 2>/dev/null ) || HELPER_STATUS=$? } 4>&1 5>&2 # The token must name the helper's actual exit status: "complete 0" for # success, "complete N" for a complete CLI error. A helper killed after its # answer was written but before exit leaves a mismatched status. if [ "$HELPER_COMPLETION" != "complete $HELPER_STATUS" ]; then printf 'claude adapter: invalid or incomplete model stream\n' >&2 [ "$HELPER_STATUS" -ne 0 ] || HELPER_STATUS=2 fi [ "$HELPER_STATUS" -eq 0 ] || exit "$HELPER_STATUS" [ "$SESSION_FLAG" = --resume ] || printf '%s\n' "$SESSION_ID" > "$ID_FILE"