Files
stack/tasks/foundation-phase2-source-facts.json
jason.woltje 8ebddd6f93 feat(foundation): offline synthetic scope/permission inspector (FI-FILBERT-8 APPROVED r6)
Rocko-authored, Filbert-reviewed inspector (r6 manifest
a4a44930...) with full review/build/verdict evidence under
docs/plans/reviews. 43/0 selftests, oracle zero-disagreement,
foundation checker PASS. Owner A9 acceptance recorded separately.
2026-09-07 14:06:35 -05:00

8 lines
15 KiB
JSON

{
"taskVersion": 1,
"id": "t-foundation-phase2-source-facts",
"prompt": "Read-only source investigation for Mosaic Stack #53 phase 2. You are a tool-free fact-finding worker, not an approver or implementer. Analyze ONLY supplied source excerpts from repository baseline 69d1bb3aa4b826218aa4cca3710f2d98c0b9d7ba. Do not invoke tools, inspect credentials, echo personal system context, propose code edits, or claim you tested runtime behavior. Your only deliverable is the final response, captured by the standard run recorder.\nGoal: identify precisely what current launch/session, permissions, and audit plumbing supports before a future scoped-workspace contract is designed.\nReturn at most 1600 words: (1) a table of source-backed observations with exact file:line citations; (2) what these excerpts do NOT establish, explicitly limited to inspected code; (3) concrete enforcement obligations for a future design, not approval of any implementation.\nFocus: exact vs latest session selection; first-use history vs directory nonemptiness; one active execution/controller; shared SOUL/mission/prompt paths; role and mission intersections on headless vs interactive paths; network declarations vs enforcement; whether tool allowlists isolate files; run evidence trust under broad mounts; outcome/timeout evidence vs actual process death; stdout result vs trusted per-action audit; disabled extensions vs assumed hook coverage. Distinguish observed facts, deductions and unverified capabilities. Do not infer repository-wide absence from excerpts. Do not treat comments as proof if executable code differs. Self-check every assertion against a quoted line range; mark unknowns. Facts only, no independent plan verdict.\n\nSOURCE EXCERPTS:\nFILE scripts/agent.sh:78-90\n78: AGENTS_DIR=\"${MOSAIC_AGENTS_DIR:-agents}\"\n79: # Fail closed (owner decision, #46): an explicit agents-dir override that\n80: # cannot resolve the named seat refuses the launch - a seatless launch has\n81: # no identity, no SOUL, and no role ceiling to bind. Unset the override for\n82: # the M13 plain governed TUI.\n83: if [ -n \"${MOSAIC_AGENTS_DIR:-}\" ] && [ ! -f \"$AGENTS_DIR/$NAME/agent.json\" ]; then\n84: echo \"agent: no seat definition for '$NAME' in overridden agents dir: $AGENTS_DIR (unset MOSAIC_AGENTS_DIR for a plain governed TUI)\" >&2\n85: exit 4\n86: fi\n87: ROLE=\"\"\n88: DEFCAPS=\"\"\n89: AGENT_DEF_SKILLS=\"\"\n90: AGENT_DEF_SKILLS=\"\"\n\nFILE scripts/agent.sh:123-138\n123: export MOSAIC_AGENT_SOUL_FILE=\"/var/lib/mosaic/agents/$NAME/SOUL.md\"\n124: # Seat record: written once at instantiation.\n125: SEAT=\"$MOSAIC_DEV_DIR/agents/$NAME/seat.json\"\n126: if [ ! -f \"$SEAT\" ]; then\n127: printf '{\"seatVersion\":1,\"name\":\"%s\",\"role\":\"%s\",\"instantiatedAt\":\"%s\"}\\n' \\\n128: \"$NAME\" \"$ROLE\" \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" > \"$SEAT\"\n129: fi\n130: fi\n131: \n132: SESSION=\"${SESSION:-agent-$NAME}\"\n133: mkdir -p \"$MOSAIC_DEV_DIR/sessions/$SESSION\"\n134: export MOSAIC_SESSION_DIR=\"/var/lib/mosaic/sessions/$SESSION\"\n135: export MOSAIC_AGENT_NAME=\"$NAME\"\n136: [ -n \"$ROLE\" ] && export MOSAIC_AGENT_ROLE=\"$ROLE\"\n137: export MOSAIC_INTERACTIVE=1\n138: if [ -z \"$TOOLS\" ] && [ -n \"$DEFCAPS\" ]; then TOOLS=\"$DEFCAPS\"; fi\n\nFILE scripts/agent.sh:140-161\n140: # Role ceiling (M18): a declared role binds to roles/<role>.json; its tools\n141: # are a ceiling that the seat definition or CLI may narrow, never escalate\n142: # past. A missing or invalid contract refuses the launch - a declared role\n143: # that resolves to nothing is the under-equipped-seat failure mode.\n144: if [ -n \"$ROLE\" ]; then\n145: ROLES_DIR=\"${MOSAIC_ROLES_DIR:-roles}\"\n146: ROLE_FILE=\"$ROLES_DIR/$ROLE.json\"\n147: [ -r \"$ROLE_FILE\" ] || { echo \"agent: role '$ROLE' is declared but has no contract: $ROLE_FILE\" >&2; exit 2; }\n148: ROLE_OUT=\"$(node scripts/mosaic-task.mjs resolve-role \"$ROLE_FILE\")\" || { echo \"agent: invalid role contract: $ROLE_FILE\" >&2; exit 2; }\n149: ROLE_CEILING=\"$(printf '%s\\n' \"$ROLE_OUT\" | sed -n 's/^MOSAIC_ROLE_TOOLS=//p')\"\n150: if [ -n \"$TOOLS\" ]; then\n151: REQUESTED_TOOLS=\"$TOOLS\"\n152: 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\")\"\n153: if [ -z \"$TOOLS\" ]; then\n154: echo \"agent: capability policy: role '$ROLE' ceiling and requested tools have nothing in common -> tool-free seat\" >&2\n155: elif [ \"$TOOLS\" != \"$REQUESTED_TOOLS\" ]; then\n156: echo \"agent: capability policy: role '$ROLE' ceiling narrowed tools -> $TOOLS\" >&2\n157: fi\n158: else\n159: TOOLS=\"$ROLE_CEILING\"\n160: fi\n161: fi\n\nFILE scripts/agent.sh:181-199\n181: if [ -n \"$MISSION\" ]; then\n182: [ -r \"$MISSION\" ] || { echo \"agent: mission file not readable: $MISSION\" >&2; exit 4; }\n183: mkdir -p \"$MOSAIC_DEV_DIR/agent-missions\"\n184: cp \"$MISSION\" \"$MOSAIC_DEV_DIR/agent-missions/$NAME.json\"\n185: export MOSAIC_MISSION_FILE=\"/var/lib/mosaic/agent-missions/$NAME.json\"\n186: fi\n187: \n188: # Workspace (M13): defaults to a persistent per-agent workspace\n189: # (workspaces/<agent>) so the agent has a real, host-visible home instead\n190: # of the container's neutral /workspace. Override with --workspace <ws>.\n191: [ -n \"$WORKSPACE\" ] || WORKSPACE=\"$NAME\"\n192: case \"$WORKSPACE\" in *[!A-Za-z0-9._-]*|'') echo \"agent: invalid workspace name\" >&2; exit 4;; esac\n193: mkdir -p \"$MOSAIC_DEV_DIR/workspaces/$WORKSPACE\"\n194: export MOSAIC_WORKSPACE=\"/var/lib/mosaic/workspaces/$WORKSPACE\"\n195: \n196: echo \"agent: launching TUI agent '$NAME' (session: $SESSION, adapter: $MOSAIC_ADAPTER, model: $MOSAIC_MODEL)\"\n197: echo \"agent: contracts + $([ -n \"$MISSION\" ] && echo 'mission' || echo 'no mission') loaded; exit the TUI with /quit\"\n198: # No -T: the TTY is the point. Ctrl+C twice or /quit exits.\n199: exec docker compose run --rm mosaic-agent\n\nFILE adapters/pi/adapter.sh:23-50\n23: # Workspace (M5): run inside the provided workspace when present.\n24: if [ -n \"${MOSAIC_WORKSPACE:-}\" ]; then\n25: mkdir -p \"$MOSAIC_WORKSPACE\"\n26: cd \"$MOSAIC_WORKSPACE\"\n27: fi\n28: \n29: # Session (M6/M11): default ephemeral (--no-session). With a declared\n30: # session dir: persist there and resume the most recent session. With a\n31: # fork source: branch the source session file into the target dir\n32: # (pi --fork) - the ancestor session is never modified.\n33: SESSION_FLAGS=\"--no-session\"\n34: if [ -n \"${MOSAIC_SESSION_FORK:-}\" ]; then\n35: [ -n \"${MOSAIC_SESSION_DIR:-}\" ] || { echo \"pi adapter: session fork requires MOSAIC_SESSION_DIR\" >&2; exit 2; }\n36: mkdir -p \"$MOSAIC_SESSION_DIR\"\n37: SESSION_FLAGS=\"--fork $MOSAIC_SESSION_FORK --session-dir $MOSAIC_SESSION_DIR\"\n38: elif [ -n \"${MOSAIC_SESSION_DIR:-}\" ]; then\n39: mkdir -p \"$MOSAIC_SESSION_DIR\"\n40: SESSION_FLAGS=\"--session-dir $MOSAIC_SESSION_DIR\"\n41: if [ -n \"$(ls -A \"$MOSAIC_SESSION_DIR\" 2>/dev/null)\" ]; then\n42: SESSION_FLAGS=\"$SESSION_FLAGS -c\"\n43: fi\n44: fi\n45: \n46: # Capabilities (M5): explicit allowlist or no tools.\n47: TOOLS_FLAG=\"--no-tools\"\n48: [ -n \"${MOSAIC_TOOLS:-}\" ] && TOOLS_FLAG=\"--tools $MOSAIC_TOOLS\"\n49: \n50: # Skills (M17): explicitly provided skill dirs replace discovery. When none\n\nFILE adapters/pi/adapter.sh:65-106\n65: REQUEST_ARG=\"\"\n66: if [ \"$INTERACTIVE\" = \"1\" ]; then\n67: PRINT_MODE=\"\"\n68: else\n69: REQUEST_ARG=\"$MOSAIC_REQUEST\"\n70: fi\n71: \n72: # All flags documented in the pi package README (CLI Reference):\n73: # -p/--print one-shot mode: print the response and exit (omitted in\n74: # interactive TUI mode)\n75: # --system-prompt replace the default prompt with the generated one\n76: # --no-* no ambient context/skills/extensions/templates/themes\n77: # SESSION_FLAGS ephemeral | persistent | forked (per env)\n78: # TOOLS_FLAG per capabilities\n79: # --offline no startup network operations (update checks/telemetry)\n80: PROMPT_CONTENT=\"$(cat \"$MOSAIC_SYSTEM_PROMPT_FILE\")\"\n81: set -- \\\n82: --offline \\\n83: --no-extensions \\\n84: $SKILLS_FLAG \\\n85: --no-prompt-templates \\\n86: --no-themes \\\n87: --no-context-files \\\n88: $TOOLS_FLAG \\\n89: $SESSION_FLAGS \\\n90: --provider \"$PI_PROVIDER\" \\\n91: --model \"$PI_MODEL\" \\\n92: --system-prompt \"$PROMPT_CONTENT\"\n93: # One-shot mode appends -p and the request (both safely quoted);\n94: # interactive mode appends nothing - clean TUI.\n95: [ \"$INTERACTIVE\" = \"1\" ] || set -- \"$@\" -p \"$MOSAIC_REQUEST\"\n96: exec pi \"$@\"\n\nFILE src/run-agent.sh:37-41\n37: /opt/mosaic/src/load-contracts.sh /opt/mosaic/contracts /var/lib/mosaic/system-prompt.md\n38: \n39: export MOSAIC_SYSTEM_PROMPT_FILE=\"/var/lib/mosaic/system-prompt.md\"\n40: \n41: exec \"$ADAPTER_SCRIPT\"\n\nFILE src/load-contracts.sh:40-42\n40: TEMP=\"$OUT.partial\"\n41: : > \"$TEMP\"\n42: \n\nFILE src/load-contracts.sh:68-77\n68: # Live user context (M14): every *.md in /var/lib/mosaic/user (sorted) is\n69: # appended - the user owns this layer and edits it without rebuilds.\n70: USER_DIR=\"/var/lib/mosaic/user\"\n71: if [ -d \"$USER_DIR\" ]; then\n72: for f in $(ls \"$USER_DIR\"/*.md 2>/dev/null | sort); do\n73: printf '===== USER CONTEXT: %s =====\\n' \"$(basename \"$f\")\" >> \"$TEMP\"\n74: cat \"$f\" >> \"$TEMP\"\n75: printf '\\n' >> \"$TEMP\"\n76: done\n77: fi\n\nFILE src/load-contracts.sh:97-98\n97: mv \"$TEMP\" \"$OUT\"\n98: echo \"load-contracts: wrote $OUT from $CONTRACT_DIR\" >&2\n\nFILE compose.yaml:39-43\n39: # Configured runtime state root (from config.json dataRoot).\n40: - ${MOSAIC_DATA_ROOT:?MOSAIC_DATA_ROOT must be set by scripts/load_config (run via scripts/*.sh)}:/var/lib/mosaic\n41: # Runtime credential only: pi auth file mounted READ-ONLY.\n42: # Never copied into the image.\n43: - ${PI_AUTH_FILE:-/home/jwoltje/.pi/agent/auth.json}:/home/node/.pi/agent/auth.json:ro\n\nFILE scripts/mosaic-task.mjs:249-253\n249: // ceiling \u2014 seats may narrow it, never escalate past it. network is declared\n250: // now and enforced when network policy lands. Strict schema: unknown keys\n251: // refuse, name must match the filename, wrong document kind refuses.\n252: function validateRole(document, file) {\n253: rejectUnknownKeys(document, [\"roleVersion\", \"name\", \"tools\", \"network\"], \"role\");\n\nFILE scripts/mosaic-task.mjs:271-274\n271: }\n272: return { roleVersion: 1, name: document.name, tools: [...seen], network: document.network ?? \"none\" };\n273: }\n274: \n\nFILE scripts/mosaic-task.mjs:295-326\n295: function writeOnce(file, content) {\n296: const fd = fs.openSync(file, \"wx\", 0o644); // exclusive: never overwrite\n297: try {\n298: fs.writeFileSync(fd, content);\n299: } finally {\n300: fs.closeSync(fd);\n301: }\n302: }\n303: \n304: function runTask(taskFile, options = {}) {\n305: const resolved = JSON.parse(\n306: spawnSync(process.execPath, [path.join(PROJECT_ROOT, \"scripts\", \"mosaic-config.mjs\"), \"validate\"], {\n307: cwd: PROJECT_ROOT,\n308: encoding: \"utf8\",\n309: maxBuffer: 1024 * 1024,\n310: }).stdout,\n311: );\n312: \n313: const task = validateTask(readJsonFile(taskFile, \"task file\"), path.resolve(taskFile));\n314: const runId = `r-${utcStamp()}-${randomBytes(3).toString(\"hex\")}`;\n315: const runDir = path.join(runsRoot(resolved), runId);\n316: \n317: fs.mkdirSync(runDir, { recursive: true });\n318: \n319: // Immutable input snapshots (evidence of exactly what was executed).\n320: const rawTask = fs.readFileSync(path.resolve(taskFile), \"utf8\");\n321: writeOnce(path.join(runDir, \"task.json\"), rawTask);\n322: if (task.missionSnapshot) {\n323: const missionPath = path.resolve(path.dirname(path.resolve(taskFile)), task.mission);\n324: writeOnce(path.join(runDir, \"mission.json\"), fs.readFileSync(missionPath, \"utf8\"));\n325: }\n326: \n\nFILE scripts/mosaic-task.mjs:364-380\n364: let effectiveTools = task.tools;\n365: let policyNote = null;\n366: if (task.missionSnapshot?.capabilities) {\n367: const missionTools = task.missionSnapshot.capabilities.tools;\n368: if (effectiveTools) {\n369: effectiveTools = effectiveTools.filter((t) => missionTools.includes(t));\n370: if (effectiveTools.length === 0) {\n371: policyNote = `capability policy: mission ${task.missionSnapshot.id} and task request no tools in common -> tool-free run`;\n372: }\n373: } else {\n374: effectiveTools = [...missionTools];\n375: }\n376: }\n377: if (policyNote) process.stderr.write(`mosaic-task: ${policyNote}\\n`);\n378: spawnEnv.MOSAIC_TOOLS = effectiveTools ? effectiveTools.join(\",\") : \"\";\n379: \n380: // Session (M6): persistent named session dir, passed as container path.\n\nFILE scripts/mosaic-task.mjs:408-466\n408: [\"compose\", \"run\", \"--rm\", \"-T\", \"mosaic-agent\", task.prompt],\n409: {\n410: cwd: PROJECT_ROOT,\n411: env: spawnEnv,\n412: input: \"\", // stdin detached: print mode must never wait on a terminal (see issue #5)\n413: encoding: \"utf8\",\n414: maxBuffer: 16 * 1024 * 1024,\n415: timeout: task.timeoutSeconds * 1000,\n416: killSignal: \"SIGKILL\",\n417: },\n418: );\n419: const finishedAt = new Date();\n420: \n421: fs.writeFileSync(stderrFile, proc.stderr ?? \"\", { flag: \"wx\" });\n422: \n423: const response = (proc.stdout ?? \"\").replace(/^[^\\S\\n]+/, \"\").replace(/[^\\S\\n]+$/, \"\").trim();\n424: let status = \"succeeded\";\n425: let reason = null;\n426: let expected = task.expectExact;\n427: \n428: if (proc.error && proc.error.code === \"ETIMEDOUT\") {\n429: status = \"failed\";\n430: reason = \"timeout\";\n431: } else if (proc.error) {\n432: status = \"failed\";\n433: reason = `spawn-error: ${proc.error.code ?? proc.error.message}`;\n434: } else if (proc.status !== 0) {\n435: status = \"failed\";\n436: reason = \"exit-nonzero\";\n437: } else if (expected !== null && response !== expected) {\n438: status = \"failed\";\n439: reason = \"expect-mismatch\";\n440: }\n441: \n442: const result = {\n443: runVersion: 1,\n444: runId,\n445: taskId: task.id,\n446: missionId: task.missionId,\n447: status,\n448: reason,\n449: request: task.prompt,\n450: response,\n451: expectedExact: expected,\n452: ...(options.retriedFrom ? { retriedFrom: options.retriedFrom } : {}),\n453: workspace: task.workspace,\n454: tools: effectiveTools,\n455: session: task.session,\n456: sessionForkFrom: task.sessionForkFrom,\n457: exitCode: proc.status,\n458: signal: proc.signal ?? null,\n459: provider: resolved.execution.provider,\n460: model: resolved.execution.model,\n461: startedAt: startedAt.toISOString(),\n462: finishedAt: finishedAt.toISOString(),\n463: durationMs: finishedAt.getTime() - startedAt.getTime(),\n464: };\n465: \n466: writeOnce(path.join(runDir, \"result.json\"), `${JSON.stringify(result, null, 2)}\\n`);",
"workspace": ":run",
"timeoutSeconds": 300
}