feat(release): recursion guard for the health gate; run-task drift warning; M20 packages/* decision recorded (#39)
- release.sh health gate runs with MOSAIC_ENSURE_SKIP=1: the gated task run cannot re-enter release self-determination - run-task.sh warns on release drift instead of silently using a stale image - ROADMAP: M20 decision recorded (packages/* monorepo at usurpation, continuity-first); restructure sequenced as M20 phase 1 Closes #39
This commit is contained in:
+26
-4
@@ -22,6 +22,7 @@ MISSION=""
|
||||
WORKSPACE=""
|
||||
SESSION=""
|
||||
TOOLS=""
|
||||
SKILLS=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
@@ -29,6 +30,7 @@ while [ $# -gt 0 ]; do
|
||||
--workspace) WORKSPACE="${2:?}"; shift 2 ;;
|
||||
--session) SESSION="${2:?}"; shift 2 ;;
|
||||
--tools) TOOLS="${2:?}"; shift 2 ;;
|
||||
--skills) SKILLS="${2:?}"; shift 2 ;;
|
||||
--help|-h) sed -n '2,12p' "$0"; exit 0 ;;
|
||||
*) NAME="$1"; shift ;;
|
||||
esac
|
||||
@@ -57,6 +59,8 @@ fi
|
||||
AGENTS_DIR="${MOSAIC_AGENTS_DIR:-agents}"
|
||||
ROLE=""
|
||||
DEFCAPS=""
|
||||
AGENT_DEF_SKILLS=""
|
||||
AGENT_DEF_SKILLS=""
|
||||
if [ -f "$AGENTS_DIR/$NAME/agent.json" ]; then
|
||||
DEFAULTS_FILE="$(mktemp)"
|
||||
node -e '
|
||||
@@ -73,13 +77,14 @@ if (p.capabilities !== undefined) {
|
||||
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");
|
||||
fs.writeFileSync(process.argv[2], "AGENT_DEF_ROLE=" + (p.role || "") + "\nAGENT_DEF_CAPS=" + tools + "\nAGENT_DEF_SKILLS=" + ((p.skills && Array.isArray(p.skills)) ? p.skills.join(",") : "") + "\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=""
|
||||
AGENT_DEF_ROLE=""; AGENT_DEF_CAPS=""; AGENT_DEF_SKILLS=""
|
||||
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=}" ;;
|
||||
AGENT_DEF_ROLE=*) AGENT_DEF_ROLE="${line#AGENT_DEF_ROLE=}" ;;
|
||||
AGENT_DEF_CAPS=*) AGENT_DEF_CAPS="${line#AGENT_DEF_CAPS=}" ;;
|
||||
AGENT_DEF_SKILLS=*) AGENT_DEF_SKILLS="${line#AGENT_DEF_SKILLS=}" ;;
|
||||
esac
|
||||
done < "$DEFAULTS_FILE"
|
||||
rm -f "$DEFAULTS_FILE"
|
||||
@@ -106,6 +111,23 @@ export MOSAIC_INTERACTIVE=1
|
||||
if [ -z "$TOOLS" ] && [ -n "$DEFCAPS" ]; then TOOLS="$DEFCAPS"; fi
|
||||
export MOSAIC_TOOLS="${TOOLS:+$TOOLS}"
|
||||
|
||||
# Skills (M17): seat definition may declare skill names; each must be
|
||||
# enabled in <dataRoot>/skills-enabled or the launch refuses - a silently
|
||||
# under-equipped seat is the failure mode this prevents.
|
||||
SKILLS_LIST="${SKILLS:-$AGENT_DEF_SKILLS}"
|
||||
if [ -n "$SKILLS_LIST" ]; then
|
||||
mkdir -p "$MOSAIC_DEV_DIR/skills-enabled"
|
||||
RESOLVED=""
|
||||
OLDIFS=$IFS; IFS=','
|
||||
for s in $SKILLS_LIST; do
|
||||
case "$s" in *[!A-Za-z0-9._-]*|'') echo "agent: invalid skill name: '$s'" >&2; exit 2;; esac
|
||||
[ -d "$MOSAIC_DEV_DIR/skills-enabled/$s" ] || { echo "agent: skill '$s' is declared but not enabled (scripts/skill.sh activate $s)" >&2; exit 1; }
|
||||
RESOLVED="${RESOLVED:+$RESOLVED,}/var/lib/mosaic/skills-enabled/$s"
|
||||
done
|
||||
IFS=$OLDIFS
|
||||
export MOSAIC_SKILLS="$RESOLVED"
|
||||
fi
|
||||
|
||||
if [ -n "$MISSION" ]; then
|
||||
[ -r "$MISSION" ] || { echo "agent: mission file not readable: $MISSION" >&2; exit 4; }
|
||||
mkdir -p "$MOSAIC_DEV_DIR/agent-missions"
|
||||
|
||||
+3
-1
@@ -49,7 +49,9 @@ health_check() { # returns 0 only when the marker path passes; $1 = fault inject
|
||||
task="$tmp/fault-task.json"
|
||||
fi
|
||||
local rc=0
|
||||
scripts/run-task.sh run "$task" >/dev/null 2>&1 || rc=$?
|
||||
# MOSAIC_ENSURE_SKIP: the gate's run must not re-enter release
|
||||
# self-determination (recursion guard).
|
||||
MOSAIC_ENSURE_SKIP=1 scripts/run-task.sh run "$task" >/dev/null 2>&1 || rc=$?
|
||||
[ -n "$tmp" ] && rm -rf "$tmp"
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env bash
|
||||
# Skill lifecycle: install / activate / deactivate / uninstall / list.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/skill.sh install <source-dir | bundled-name>
|
||||
# scripts/skill.sh activate <name>
|
||||
# scripts/skill.sh deactivate <name>
|
||||
# scripts/skill.sh uninstall <name>
|
||||
# scripts/skill.sh list
|
||||
#
|
||||
# Layout (machine-scoped, under the data root):
|
||||
# <dataRoot>/skills-available/<name> installed, not loadable
|
||||
# <dataRoot>/skills-enabled/<name> loadable by agent launches
|
||||
#
|
||||
# A skill not in skills-enabled is not enabled or available for use.
|
||||
# Lifecycle: install -> available; activate -> enabled; deactivate ->
|
||||
# available; uninstall -> removed (only from available).
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
# shellcheck source=common.sh
|
||||
source scripts/common.sh
|
||||
|
||||
load_config
|
||||
load_release
|
||||
bootstrap_runtime_dir
|
||||
|
||||
ENABLED="$MOSAIC_DEV_DIR/skills-enabled"
|
||||
AVAILABLE="$MOSAIC_DEV_DIR/skills-available"
|
||||
mkdir -p "$ENABLED" "$AVAILABLE"
|
||||
|
||||
die() { local code="$1"; shift; echo "skill: $*" >&2; exit "$code"; }
|
||||
|
||||
is_skill_dir() { [ -f "$1/SKILL.md" ]; }
|
||||
|
||||
resolve_source() { # bundled name (skills/<name> in repo) or explicit path
|
||||
if [ -d "skills/$1" ]; then printf 'skills/%s' "$1"; return; fi
|
||||
if [ -d "$1" ]; then printf '%s' "$1"; return; fi
|
||||
die 4 "source skill dir not found: $1"
|
||||
}
|
||||
|
||||
frontmatter_name() { # extract name: from SKILL.md frontmatter
|
||||
node -e '
|
||||
const fs = require("fs");
|
||||
const t = fs.readFileSync(process.argv[1], "utf8");
|
||||
const m = t.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
if (!m) process.exit(1);
|
||||
const name = (m[1].match(/^name:\s*(.+)$/m) || [])[1];
|
||||
if (!name) process.exit(1);
|
||||
process.stdout.write(name.trim());
|
||||
' "$1/SKILL.md"
|
||||
}
|
||||
|
||||
case "${1:-list}" in
|
||||
install)
|
||||
src="${2:?usage: skill.sh install <source-dir|bundled-name>}"
|
||||
resolve_source "$src" >/dev/null || die 4 "source skill dir not found: $src"
|
||||
SRC_DIR="$(resolve_source "$src")"
|
||||
is_skill_dir "$SRC_DIR" || die 4 "not a skill (missing SKILL.md): $SRC_DIR"
|
||||
NAME="$(basename "$SRC_DIR")"
|
||||
[ -d "$AVAILABLE/$NAME" ] && die 1 "already installed: $NAME"
|
||||
mkdir -p "$AVAILABLE"
|
||||
cp -r "$SRC_DIR" "$AVAILABLE/$NAME"
|
||||
echo "skill: installed $NAME -> skills-available"
|
||||
echo "skill: activate with scripts/skill.sh activate $NAME"
|
||||
;;
|
||||
activate)
|
||||
name="${2:?usage: skill.sh activate <name>}"
|
||||
[ -d "$AVAILABLE/$name" ] || die 4 "not installed: $name"
|
||||
[ -d "$ENABLED/$name" ] && die 1 "already enabled: $name"
|
||||
mv "$AVAILABLE/$name" "$ENABLED/$name"
|
||||
echo "skill: enabled $name"
|
||||
;;
|
||||
deactivate)
|
||||
name="${2:?usage: skill.sh deactivate <name>}"
|
||||
[ -d "$ENABLED/$name" ] || die 4 "not enabled: $name"
|
||||
mv "$ENABLED/$name" "$AVAILABLE/$name"
|
||||
echo "skill: deactivated $name -> skills-available"
|
||||
;;
|
||||
uninstall)
|
||||
name="${2:?usage: skill.sh uninstall <name>}"
|
||||
if [ -d "$ENABLED/$name" ]; then
|
||||
die 1 "refusing: $name is enabled - deactivate first"
|
||||
fi
|
||||
[ -d "$AVAILABLE/$name" ] || die 4 "not installed: $name"
|
||||
rm -rf "$AVAILABLE/$name"
|
||||
echo "skill: uninstalled $name"
|
||||
;;
|
||||
list)
|
||||
echo "enabled:"
|
||||
for d in "$ENABLED"/*/; do
|
||||
[ -d "$d" ] && echo " $(basename "$d")"
|
||||
done
|
||||
echo "available:"
|
||||
for d in "$AVAILABLE"/*/; do
|
||||
[ -d "$d" ] && echo " $(basename "$d")"
|
||||
done
|
||||
;;
|
||||
*)
|
||||
echo "usage: scripts/skill.sh install <src> | activate <name> | deactivate <name> | uninstall <name> | list" >&2
|
||||
exit 4
|
||||
;;
|
||||
esac
|
||||
+40
-16
@@ -191,22 +191,45 @@ EOF
|
||||
expect_exit "retry of missing run exits 4" 4 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/mock-adapters.json" node scripts/mosaic-task.mjs retry r-missing
|
||||
|
||||
# session fork plumbing (M11): fork source + target dir delivered
|
||||
printf '{"taskVersion":1,"id":"t-forkplumb","prompt":"x","session":"fork-child","sessionForkFrom":"base"}' > "$SANDBOX/forkplumb.json"
|
||||
mkdir -p "$SANDBOX/data/sessions/base"
|
||||
printf '{}' > "$SANDBOX/data/sessions/base/20260903T000000-plumb.jsonl"
|
||||
expect_exit "fork task runs via mock" 0 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/mock-adapters.json" MOSAIC_MOCK_RESPONSE=MOCKED \
|
||||
scripts/run-task.sh run "$SANDBOX/forkplumb.json"
|
||||
FL="$(ls -dt "$SANDBOX/data/runs"/r-* | head -1)"
|
||||
grep -q '^MOSAIC_SESSION_FORK=/var/lib/mosaic/sessions/base/20260903T000000-plumb.jsonl$' "$FL/stderr.txt" 2>/dev/null \
|
||||
&& grep -q '^MOSAIC_SESSION_DIR=/var/lib/mosaic/sessions/fork-child$' "$FL/stderr.txt" 2>/dev/null \
|
||||
&& check "fork source + target delivered to adapter" 0 \
|
||||
|| check "fork source + target delivered to adapter" 1
|
||||
printf '{"taskVersion":1,"id":"t-f2","prompt":"x","sessionForkFrom":"base"}' > "$SANDBOX/noforktarget.json"
|
||||
expect_exit "fork without session target exits 2" 2 -- $TASK validate "$SANDBOX/noforktarget.json"
|
||||
printf '{"taskVersion":1,"id":"t-f3","prompt":"x","session":"base","sessionForkFrom":"base"}' > "$SANDBOX/selfork.json"
|
||||
expect_exit "self-fork exits 2" 2 -- $TASK validate "$SANDBOX/selfork.json"
|
||||
# skills lifecycle (M17)
|
||||
expect_exit "skill install bundled ms-tools" 0 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/mock-adapters.json" scripts/skill.sh install ms-tools
|
||||
[ -d "$SANDBOX/data/skills-available/ms-tools" ] \
|
||||
&& check "installed to skills-available" 0 || check "installed to skills-available" 1
|
||||
expect_exit "double install refuses" 1 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/mock-adapters.json" scripts/skill.sh install ms-tools
|
||||
expect_exit "activate enables skill" 0 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/mock-adapters.json" scripts/skill.sh activate ms-tools
|
||||
[ -d "$SANDBOX/data/skills-enabled/ms-tools" ] \
|
||||
&& check "enabled dir populated" 0 || check "enabled dir populated" 1
|
||||
expect_exit "uninstall while enabled refuses" 1 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/mock-adapters.json" scripts/skill.sh uninstall ms-tools
|
||||
expect_exit "deactivate moves back to available" 0 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/mock-adapters.json" scripts/skill.sh deactivate ms-tools
|
||||
expect_exit "uninstall removes from available" 0 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/mock-adapters.json" scripts/skill.sh uninstall ms-tools
|
||||
|
||||
# seat skills dispatch (mock evidence)
|
||||
mkdir -p "$SANDBOX/agents/skillseat" "$SANDBOX/data/user"
|
||||
printf '# User\n\nname: Jason\n' > "$SANDBOX/data/user/USER.md"
|
||||
printf '{"agentVersion":1,"name":"skillseat","skills":["ms-tools"]}' > "$SANDBOX/agents/skillseat/agent.json"
|
||||
printf '{"agentVersion":1,"name":"skillseat","soulPlaceholder":true}' > /dev/null # no-op
|
||||
printf '# SOUL - skillseat\n\nMethodical. Verifies before claiming.\n' > "$SANDBOX/agents/skillseat/SOUL.md"
|
||||
# Pre-align the release pointer so ensure does not fire under the mock
|
||||
# adapter (the gate needs a real model; the mock cannot answer).
|
||||
REL="$(tr -d '[:space:]' < RELEASE)"
|
||||
mkdir -p "$SANDBOX/data/state"
|
||||
printf '{"pointerVersion":1,"release":"%s","imageTag":"mosaic-poc-agent:0.84.4-r%s","activatedAt":"2026-01-01T00:00:00Z"}\n' "$REL" "$REL" > "$SANDBOX/data/state/active.json"
|
||||
scripts/agent.sh skillseat </dev/null >"$SANDBOX/seat-stdout.txt" 2>"$SANDBOX/seat-stderr.txt"
|
||||
RC=$?
|
||||
if [ "$RC" -eq 0 ]; then
|
||||
PASS=$((PASS+1)); echo "${C_OK}OK${C_RESET} seat with enabled skill launches"
|
||||
else
|
||||
FAIL=$((FAIL+1)); echo "${C_FAIL}FAIL${C_RESET} seat with enabled skill launches (exit $RC)" >&2
|
||||
echo "SEATCASE stderr:" >&2; cat "$SANDBOX/seat-stderr.txt" >&2
|
||||
fi
|
||||
grep -q '^MOSAIC_SKILLS=/var/lib/mosaic/skills-enabled/ms-tools$' "$SANDBOX/seat-stderr.txt" 2>/dev/null \
|
||||
&& check "skill path delivered to adapter" 0 || check "skill path delivered to adapter" 1
|
||||
|
||||
# capability policy (M9): least-privilege intersection
|
||||
POL="$SANDBOX/data/workspaces"; mkdir -p "$POL"
|
||||
@@ -236,6 +259,7 @@ 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"
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user