#!/usr/bin/env bash # Launch an interactive (TUI) Mosaic agent in its container. # # Usage: # scripts/agent.sh [--mission ] [--workspace ] # [--session ] [--tools ] # # The agent receives the four immutable contracts (constitution, standards, # SOUL, USER) plus its own identity and optional mission directives as its # system prompt, a persistent named session, and - if declared - a # workspace and tool capabilities. The TUI opens clean; you drive. # # This is the Mosaic alternative to launching vanilla pi: same engine, # governed context. set -euo pipefail cd "$(dirname "$0")/.." # shellcheck source=common.sh source scripts/common.sh NAME="" MISSION="" WORKSPACE="" SESSION="" TOOLS="" while [ $# -gt 0 ]; do case "$1" in --mission) MISSION="${2:?}"; shift 2 ;; --workspace) WORKSPACE="${2:?}"; shift 2 ;; --session) SESSION="${2:?}"; shift 2 ;; --tools) TOOLS="${2:?}"; shift 2 ;; --help|-h) sed -n '2,12p' "$0"; exit 0 ;; *) NAME="$1"; shift ;; esac done [ -n "$NAME" ] || { echo "agent: usage: scripts/agent.sh [--mission f] [--workspace ws] [--session s] [--tools list]" >&2; exit 4; } case "$NAME" in *[!A-Za-z0-9._-]*|'') echo "agent: invalid agent name" >&2; exit 4;; esac load_config load_release bootstrap_runtime_dir # Agent seat definition (M15): when agents//agent.json exists it is # strictly validated and its values become defaults (CLI flags override). # The seat's SOUL.md overrides the contract persona; governance contracts # are never overridden. AGENTS_DIR="${MOSAIC_AGENTS_DIR:-agents}" ROLE="" DEFCAPS="" if [ -f "$AGENTS_DIR/$NAME/agent.json" ]; then DEFAULTS_FILE="$(mktemp)" node -e ' const fs = require("fs"); const p = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); if (p.agentVersion !== 1) process.exit(2); const ID = /^[a-z0-9][a-z0-9._-]{0,63}$/; if (typeof p.name !== "string" || !ID.test(p.name)) process.exit(2); if (p.role !== undefined && (typeof p.role !== "string" || !ID.test(p.role))) process.exit(2); let tools = ""; if (p.capabilities !== undefined) { if (typeof p.capabilities !== "object" || p.capabilities === null || Array.isArray(p.capabilities)) process.exit(2); for (const k of Object.keys(p.capabilities)) if (k !== "tools") process.exit(2); if (!Array.isArray(p.capabilities.tools) || p.capabilities.tools.some(t => !/^[a-z]+$/.test(t))) process.exit(2); tools = p.capabilities.tools.join(","); } fs.writeFileSync(process.argv[2], "AGENT_DEF_ROLE=" + (p.role || "") + "\nAGENT_DEF_CAPS=" + tools + "\n"); ' "$AGENTS_DIR/$NAME/agent.json" "$DEFAULTS_FILE" || { rm -f "$DEFAULTS_FILE"; echo "agent: invalid agent definition" >&2; exit 2; } AGENT_DEF_ROLE=""; AGENT_DEF_CAPS="" while IFS= read -r line; do case "$line" in AGENT_DEF_ROLE=*) AGENT_DEF_ROLE="${line#AGENT_DEF_ROLE=}" ;; AGENT_DEF_CAPS=*) AGENT_DEF_CAPS="${line#AGENT_DEF_CAPS=}" ;; esac done < "$DEFAULTS_FILE" rm -f "$DEFAULTS_FILE" ROLE="$AGENT_DEF_ROLE" DEFCAPS="$AGENT_DEF_CAPS" [ -r "$AGENTS_DIR/$NAME/SOUL.md" ] || { echo "agent: definition dir missing SOUL.md: $AGENTS_DIR/$NAME" >&2; exit 4; } mkdir -p "$MOSAIC_DEV_DIR/agents/$NAME" cp "$AGENTS_DIR/$NAME/SOUL.md" "$MOSAIC_DEV_DIR/agents/$NAME/SOUL.md" export MOSAIC_AGENT_SOUL_FILE="/var/lib/mosaic/agents/$NAME/SOUL.md" # Seat record: written once at instantiation. SEAT="$MOSAIC_DEV_DIR/agents/$NAME/seat.json" if [ ! -f "$SEAT" ]; then printf '{"seatVersion":1,"name":"%s","role":"%s","instantiatedAt":"%s"}\n' \ "$NAME" "$ROLE" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$SEAT" fi fi SESSION="${SESSION:-agent-$NAME}" mkdir -p "$MOSAIC_DEV_DIR/sessions/$SESSION" export MOSAIC_SESSION_DIR="/var/lib/mosaic/sessions/$SESSION" export MOSAIC_AGENT_NAME="$NAME" [ -n "$ROLE" ] && export MOSAIC_AGENT_ROLE="$ROLE" export MOSAIC_INTERACTIVE=1 if [ -z "$TOOLS" ] && [ -n "$DEFCAPS" ]; then TOOLS="$DEFCAPS"; fi export MOSAIC_TOOLS="${TOOLS:+$TOOLS}" if [ -n "$MISSION" ]; then [ -r "$MISSION" ] || { echo "agent: mission file not readable: $MISSION" >&2; exit 4; } mkdir -p "$MOSAIC_DEV_DIR/agent-missions" cp "$MISSION" "$MOSAIC_DEV_DIR/agent-missions/$NAME.json" export MOSAIC_MISSION_FILE="/var/lib/mosaic/agent-missions/$NAME.json" fi # Workspace (M13): defaults to a persistent per-agent workspace # (workspaces/) so the agent has a real, host-visible home instead # of the container's neutral /workspace. Override with --workspace . [ -n "$WORKSPACE" ] || WORKSPACE="$NAME" case "$WORKSPACE" in *[!A-Za-z0-9._-]*|'') echo "agent: invalid workspace name" >&2; exit 4;; esac mkdir -p "$MOSAIC_DEV_DIR/workspaces/$WORKSPACE" export MOSAIC_WORKSPACE="/var/lib/mosaic/workspaces/$WORKSPACE" echo "agent: launching TUI agent '$NAME' (session: $SESSION, adapter: $MOSAIC_ADAPTER, model: $MOSAIC_MODEL)" echo "agent: contracts + $([ -n "$MISSION" ] && echo 'mission' || echo 'no mission') loaded; exit the TUI with /quit" # No -T: the TTY is the point. Ctrl+C twice or /quit exits. exec docker compose run --rm mosaic-agent