Compare commits

..
Author SHA1 Message Date
jason.woltje 34e06e7de7 Merge M15: agent seats - per-agent SOUL and role contracts
Closes #36
2026-09-03 11:56:38 -05:00
jason.woltje 9bd4f1c405 feat(agents): agent seats - per-agent SOUL, role, definitions dir (#36)
- agents/<name>/ holds agent.json (strictly validated: version, name,
  role?, capabilities?, workspace?, session?) + SOUL.md (persona prose)
- agent.sh: definition loading (quote-safe node defaults file), runtime
  SOUL copy to dataRoot/agents/<name>/, MOSAIC_AGENT_SOUL_FILE ->
  loader fills the SOUL slot from the seat's persona (contract SOUL =
  default persona; governance never overridden)
- seat.json written once at instantiation (seatVersion, name, role, at)
- identity section gains agent role; compose passthrough for role+SOUL
- live user context (M14) + seat SOUL compose the full persona:
  governance -> persona -> identity -> user -> mission
- RELEASE -> 0.0.10; packaged and health-gated activated
- example seat committed: agents/researcher

Closes #36
2026-09-03 11:56:38 -05:00
jason.woltje a7b612435b docs: BUILD-LOG Phase 15, CURRENT.md - M13 shipped 2026-09-03 11:25:41 -05:00
11 changed files with 158 additions and 12 deletions
+24
View File
@@ -337,4 +337,28 @@ Conductor loop proven end-to-end on the stack itself. `main` merged with M8; rel
Session forking verified. `main` merged with M11, tagged `session-fork-v1`; release 0.0.7 active.
---
## Phase 15: Interactive TUI agent + TOOLS.md (M13)
### Entry 15.1 — before
- Timestamp: 2026-09-03
- Intended action: Add scripts/agent.sh — an interactive TUI launcher (contracts + optional mission + agent identity + named session + optional workspace/tools) — and the pi-adapter interactive branch; remove the fixed compose command; add docs/TOOLS.md as the on-demand reference AGENTS.md routes to; RELEASE -> 0.0.8 (Gitea #35).
- Reason: The owner's bootstrap model is vanilla pi sessions directed by AGENTS.md, graduating to governed TUI agents — the first the system itself launches.
- Expected result: TUI agent launches with contracts+identity context; headless paths unchanged; TOOLS.md consolidates the reference.
### Entry 15.2 — after
- Observed: mock plumbing asserts agent name/session/workspace/mission delivery; identity section asserted in generated prompt; headless hello + suites green (24/58/17/14 + verify); 0.0.8 packaged and health-gated activated.
- Failure or correction:
1. Regression: pi adapter rewrite made MOSAIC_AGENT_NAME unconditionally required, breaking headless paths — caught by task suite (empty-stderr exit-nonzero), fixed (optional in headless; identity section simply omitted).
2. Regression: unquoted $REQUEST_ARG word-split the request into positional args — fixed with positional-argument building (set -- ... "$@").
3. Mission fixture wording (objective named the agent) invited the model to append its name after the marker, tripping the strict gate — fixture tightened; strict gate kept by design.
- Conductor session env hygiene: sandbox config exports now scoped per-command after a leak broke cross-suite runs.
## Result (M13)
Interactive TUI agent launched and verified; TOOLS.md reference shipped. `main` merged with M13, tagged `interactive-agent-v1`; release 0.0.8 active.
+1 -1
View File
@@ -1 +1 @@
0.0.8
0.0.9
+5
View File
@@ -0,0 +1,5 @@
# SOUL - researcher
You are the researcher seat of the Mosaic fleet. You are curious, methodical,
and precise. You cite what you know, admit what you do not, and never guess
when you can verify.
+6
View File
@@ -0,0 +1,6 @@
{
"agentVersion": 1,
"name": "researcher",
"role": "researcher",
"capabilities": { "tools": ["read", "bash"] }
}
+2
View File
@@ -24,6 +24,8 @@ services:
# Interactive TUI mode + agent identity (M13, set by scripts/agent.sh)
MOSAIC_INTERACTIVE: ${MOSAIC_INTERACTIVE:-}
MOSAIC_AGENT_NAME: ${MOSAIC_AGENT_NAME:-}
MOSAIC_AGENT_ROLE: ${MOSAIC_AGENT_ROLE:-}
MOSAIC_AGENT_SOUL_FILE: ${MOSAIC_AGENT_SOUL_FILE:-}
# mock adapter only: verbatim response for deterministic seam tests
MOSAIC_MOCK_RESPONSE: ${MOSAIC_MOCK_RESPONSE:-}
# Documented container auth alternative: provider API key via
+5 -2
View File
@@ -7,12 +7,12 @@ update this file to the next action). No ambiguity, no re-planning.
## Next action
Owner review of M12 (conductor auto-apply policy) — then name the next target.
Owner review of M13 (interactive TUI agent + TOOLS.md) — then name the next target.
## Queue (ordered, not started)
1. Second real adapter (parked — owner focused on Pi)
2. Session forking from a common ancestor — SHIPPED in M11; exercise via session demos
2. Auto-apply policy for worker patches — SHIPPED in M12
3. UX/DX backlog #31 (grep highlight vs status colors — likely closed by owner's unalias)
4. Push policy decision: auto-apply commits locally; push remains explicit (documented in CONDUCTOR.md)
@@ -28,6 +28,9 @@ Owner review of M12 (conductor auto-apply policy) — then name the next target.
## Completed log
- 2026-09-03 — M12 conductor auto-apply policy (#34) — committed on main, 17/24/58/14 + verify green; push stays explicit
- 2026-09-03 — M13 interactive TUI agent + TOOLS.md (#35) — merged, agent.sh TUI launcher + identity injection, 24/58+/17/14 + verify green; release 0.0.8 activated
- 2026-09-03 — M9 mission capability policy (#30) — merged, least-privilege intersection
- 2026-09-03 — test UX: green OK/red FAIL status colors (#31 adjacent) — terminal-only, pipe-safe
- 2026-09-03 — M10 run-record retention (#32) — merged, prune keep-N/dry-run/receipt, 49/24/14 + verify green
+56 -5
View File
@@ -41,11 +41,60 @@ load_config
load_release
bootstrap_runtime_dir
# Agent seat definition (M15): when agents/<name>/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
@@ -55,11 +104,13 @@ if [ -n "$MISSION" ]; then
export MOSAIC_MISSION_FILE="/var/lib/mosaic/agent-missions/$NAME.json"
fi
if [ -n "$WORKSPACE" ]; then
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"
fi
# Workspace (M13): defaults to a persistent per-agent workspace
# (workspaces/<agent>) so the agent has a real, host-visible home instead
# of the container's neutral /workspace. Override with --workspace <ws>.
[ -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"
+7
View File
@@ -50,4 +50,11 @@ bootstrap_runtime_dir() {
echo "bootstrap: created $MOSAIC_DEV_DIR"
fi
touch "$MOSAIC_DEV_DIR/$POC_ROOT_MARKER"
# Live user context layer (M14): seeded once, owned by the user from
# then on; dispatched to every agent launch without rebuilds.
mkdir -p "$MOSAIC_DEV_DIR/user"
if [ ! -f "$MOSAIC_DEV_DIR/user/USER.md" ]; then
printf '# User\n\nDescribe yourself, your machine, and your preferences here.\nThis file is dispatched to every Mosaic agent launch.\n' \
> "$MOSAIC_DEV_DIR/user/USER.md"
fi
}
+11
View File
@@ -236,6 +236,17 @@ EOF
printf '{"missionVersion":1,"id":"m-pol","objective":"o","capabilities":{"tools":["sudo"]}}' > "$SANDBOX/pol-m.json"
expect_exit "invalid mission capabilities rejected" 2 -- \
env MOSAIC_CONFIG="$SANDBOX/mock-adapters.json" $TASK validate "$SANDBOX/pol-t.json"
# live user context (M14): dispatched to every launch without rebuild
mkdir -p "$SANDBOX/data/user"
printf '\nUSER-CANON-MARKER\n' >> "$SANDBOX/data/user/USER.md"
expect_exit "task run with user layer present" 0 -- \
env MOSAIC_CONFIG="$SANDBOX/mock-adapters.json" MOSAIC_MOCK_RESPONSE=MOSAIC_HELLO_OK \
scripts/run-task.sh run "$SANDBOX/ok.json"
grep -q 'USER CONTEXT: USER.md' "$SANDBOX/data/system-prompt.md" \
&& grep -q 'USER-CANON-MARKER' "$SANDBOX/data/system-prompt.md" \
&& check "user context dispatched into generated prompt" 0 \
|| check "user context dispatched into generated prompt" 1
else
echo "skip adapter seam cases (docker daemon unavailable)"
fi
+39 -4
View File
@@ -1,14 +1,33 @@
#!/bin/sh
# Load the four immutable contract files in fixed order and write the
# generated system prompt to /var/lib/mosaic/system-prompt.md.
# Load agent context and write the generated system prompt to
# /var/lib/mosaic/system-prompt.md.
#
# Order is normative: CONSTITUTION.md, STANDARDS.md, SOUL.md, USER.md.
# Layers, in normative order:
# 1. Immutable contracts (image): CONSTITUTION, STANDARDS, SOUL
# 2. Agent identity (when the launcher names the agent)
# 3. Mission (when the task/launcher provides one)
# 4. Live user context (M14): <dataRoot>/user/*.md - user-owned,
# dispatched to every launch without rebuilds
set -eu
CONTRACT_DIR="${1:-/opt/mosaic/contracts}"
OUT="${2:-/var/lib/mosaic/system-prompt.md}"
FILES="CONSTITUTION.md STANDARDS.md SOUL.md USER.md"
FILES="CONSTITUTION.md STANDARDS.md"
# SOUL slot (M15): the contract SOUL.md is the DEFAULT persona; a launched
# agent seat overrides it with its own runtime SOUL (governance contracts
# are never overridden).
SOUL_SRC="$CONTRACT_DIR/SOUL.md"
SOUL_HEADER="SOUL.md"
if [ -n "${MOSAIC_AGENT_SOUL_FILE:-}" ]; then
if [ ! -r "$MOSAIC_AGENT_SOUL_FILE" ]; then
echo "load-contracts: agent SOUL not readable: $MOSAIC_AGENT_SOUL_FILE" >&2
exit 1
fi
SOUL_SRC="$MOSAIC_AGENT_SOUL_FILE"
SOUL_HEADER="SOUL.md (agent seat override)"
fi
if [ ! -d "$CONTRACT_DIR" ]; then
echo "load-contracts: contract directory not found: $CONTRACT_DIR" >&2
@@ -33,14 +52,30 @@ for f in $FILES; do
printf '\n' >> "$TEMP"
done
printf '===== CONTRACT: %s =====\n' "$SOUL_HEADER" >> "$TEMP"
cat "$SOUL_SRC" >> "$TEMP"
printf '\n' >> "$TEMP"
# Agent identity (M13): when the launcher names the agent, the generated
# prompt states it - SOUL.md provides the persona, this provides the name.
if [ -n "${MOSAIC_AGENT_NAME:-}" ]; then
printf '===== AGENT IDENTITY =====\n' >> "$TEMP"
printf 'agent name: %s\n' "$MOSAIC_AGENT_NAME" >> "$TEMP"
[ -n "${MOSAIC_AGENT_ROLE:-}" ] && printf 'agent role: %s\n' "$MOSAIC_AGENT_ROLE" >> "$TEMP"
printf '\n' >> "$TEMP"
fi
# Live user context (M14): every *.md in /var/lib/mosaic/user (sorted) is
# appended - the user owns this layer and edits it without rebuilds.
USER_DIR="/var/lib/mosaic/user"
if [ -d "$USER_DIR" ]; then
for f in $(ls "$USER_DIR"/*.md 2>/dev/null | sort); do
printf '===== USER CONTEXT: %s =====\n' "$(basename "$f")" >> "$TEMP"
cat "$f" >> "$TEMP"
printf '\n' >> "$TEMP"
done
fi
# Sanctioned mission injection point (M4): when the task runner provides a
# mission snapshot, its objective and directives are appended AFTER the
# immutable contracts. Runtime data; never part of the contract fixtures.
+2
View File
@@ -1,3 +1,5 @@
# POC user
This is an isolated local runtime test.
The user's name is Jason.