121 lines
5.6 KiB
Bash
121 lines
5.6 KiB
Bash
#!/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 <gate> || 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/<pid>/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"
|