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
This commit is contained in:
@@ -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.
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"agentVersion": 1,
|
||||
"name": "researcher",
|
||||
"role": "researcher",
|
||||
"capabilities": { "tools": ["read", "bash"] }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+56
-5
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# POC user
|
||||
|
||||
This is an isolated local runtime test.
|
||||
|
||||
The user's name is Jason.
|
||||
Reference in New Issue
Block a user