feat(roles): M18 seat-role progressive capability restriction (#45)
Role contracts (roles/<role>.json): roleVersion, name bound to filename, tools ceiling (subset of pi built-ins), network declared (none|api-only| open; enforced when network policy lands). Strict schema, fail closed - a non-role document refuses resolution. mosaic-task.mjs resolve-role: config-free contract validation, emits MOSAIC_ROLE_TOOLS / MOSAIC_ROLE_NETWORK. agent.sh: a declared role binds to its contract. Missing/invalid contract refuses the launch (exit 2, names the role - the under-equipped-seat failure mode, mirroring M17 skills). Effective tools = ceiling ∩ requested (CLI --tools or agent.json caps); no request -> ceiling stands; narrowing and tool-free outcomes loud on stderr. Adapters unchanged; headless M9 chain (mission ∩ task) untouched. Ships roles/researcher.json (existing seat declares the role; without the contract the fail-closed gate would refuse its launch). Task suite 74 -> 88: contract resolution, wrong-kind/name/network/ duplicate/unsupported/missing refusals, ceiling narrowing E2E (mock adapter), tool-free E2E, missing-contract refusal. Test-authoring correction recorded in BUILD-LOG (a check that registered on one path only, caught by count arithmetic). Suites 24/88/14/17 + verify green.
This commit is contained in:
@@ -109,6 +109,29 @@ 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
|
||||
|
||||
# Role ceiling (M18): a declared role binds to roles/<role>.json; its tools
|
||||
# are a ceiling that the seat definition or CLI may narrow, never escalate
|
||||
# past. A missing or invalid contract refuses the launch - a declared role
|
||||
# that resolves to nothing is the under-equipped-seat failure mode.
|
||||
if [ -n "$ROLE" ]; then
|
||||
ROLES_DIR="${MOSAIC_ROLES_DIR:-roles}"
|
||||
ROLE_FILE="$ROLES_DIR/$ROLE.json"
|
||||
[ -r "$ROLE_FILE" ] || { echo "agent: role '$ROLE' is declared but has no contract: $ROLE_FILE" >&2; exit 2; }
|
||||
ROLE_OUT="$(node scripts/mosaic-task.mjs resolve-role "$ROLE_FILE")" || { echo "agent: invalid role contract: $ROLE_FILE" >&2; exit 2; }
|
||||
ROLE_CEILING="$(printf '%s\n' "$ROLE_OUT" | sed -n 's/^MOSAIC_ROLE_TOOLS=//p')"
|
||||
if [ -n "$TOOLS" ]; then
|
||||
REQUESTED_TOOLS="$TOOLS"
|
||||
TOOLS="$(node -e 'const c=process.argv[1].split(",").filter(Boolean);const r=process.argv[2].split(",").filter(Boolean);process.stdout.write(r.filter(t=>c.includes(t)).join(","))' "$ROLE_CEILING" "$REQUESTED_TOOLS")"
|
||||
if [ -z "$TOOLS" ]; then
|
||||
echo "agent: capability policy: role '$ROLE' ceiling and requested tools have nothing in common -> tool-free seat" >&2
|
||||
elif [ "$TOOLS" != "$REQUESTED_TOOLS" ]; then
|
||||
echo "agent: capability policy: role '$ROLE' ceiling narrowed tools -> $TOOLS" >&2
|
||||
fi
|
||||
else
|
||||
TOOLS="$ROLE_CEILING"
|
||||
fi
|
||||
fi
|
||||
export MOSAIC_TOOLS="${TOOLS:+$TOOLS}"
|
||||
|
||||
# Skills (M17): seat definition may declare skill names; each must be
|
||||
|
||||
Reference in New Issue
Block a user