From df9e036214cd1ba167c48285b621ec39a6439b64 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 4 Oct 2026 14:26:50 -0500 Subject: [PATCH] docs(plans): meta-harness survey; lead decision 47 Filbert's survey of Pi, Claude Code and Codex hooks as a record. No hook is a hard block alone; credential scope, verbs, tool ceiling and a container are. Two DEFERRED items. Co-Authored-By: Claude Opus 5.5 --- .../work/meta-harness-survey-2026-10-04.md | 929 ++++++++++++++++++ docs/SESSIONS.md | 2 + docs/plans/2026-09-26_lead-decisions.md | 24 + docs/plans/DEFERRED.md | 11 + 4 files changed, 966 insertions(+) create mode 100644 agents/filbert/work/meta-harness-survey-2026-10-04.md diff --git a/agents/filbert/work/meta-harness-survey-2026-10-04.md b/agents/filbert/work/meta-harness-survey-2026-10-04.md new file mode 100644 index 00000000..5b932710 --- /dev/null +++ b/agents/filbert/work/meta-harness-survey-2026-10-04.md @@ -0,0 +1,929 @@ +# Meta-harness survey: Pi, Claude Code and Codex CLI (2026-10-04) + +Filbert, for Sage. Slice 1, step 8 (`docs/plans/2026-10-04_foundation-direction.md`, +lead decisions 43 and 44). This is research only: nothing here is built or committed. + +The survey covers how each harness loads seven things: the system prompt and +context files, skills, hooks, MCP, permissions and sandbox, per-session +environment and credentials, and headless runs. It then maps what +`adapters/` and the seat launchers already do, and describes what a +generator would have to emit for each harness. Section 1 answers the key +question: where can a role's decision classes be a hard block, and where are +they only prompt text? + +It reads alongside Darkwing's draft, +`agents/darkwing/work/slice1-data-model-2026-10-04.md` (role schema +version 2, the action vocabulary and layered variables). I use their terms: +`withinRole`, `crossRole`, gated, and the action vocabulary from their +section 1.4. + +## Versions and method + +| Harness | Version | Source used | +|---|---|---| +| Pi | `@earendil-works/pi-coding-agent` 0.85.1, the `package.json` pin, matching `node_modules` | Docs and compiled source in `node_modules/@earendil-works/` | +| Claude Code | 2.1.289 (`claude --version`) | https://code.claude.com/docs/en/ and local `claude --help` | +| Codex CLI | 0.160.0 (`codex --version`) | https://developers.openai.com/codex/ and source at tag `rust-v0.160.0` (commit `a956835d0207`), plus local `--help` | + +Three research subagents gathered the material. I re-checked the claims +that decide the key question against the source or docs myself (section 9). +No credential file was opened. For the local config files +(`~/.claude/settings.json`, `~/.codex/config.toml`, `~/.pi/agent/settings.json`), +only key names were read. + +**Citation keys** + +| Key | Meaning | +|---|---| +| `pi::` | relative to `node_modules/@earendil-works/` at 0.85.1 | +| `[cc: §
]` | `https://code.claude.com/docs/en/`, at that heading. `sdk-hooks` is `agent-sdk/hooks`, `sdk-ts` is `agent-sdk/typescript`, `sdk-perm` is `agent-sdk/permissions` | +| `[cc:help]` | local `claude --help`, 2.1.289 | +| `[cx:D:#
]` | `https://developers.openai.com/codex/` | +| `[cx:S:]` | `github.com/openai/codex/blob/rust-v0.160.0/` | +| `[cx:H:]` | local `codex --help` | +| `[repo::]` | this repository at `bb7e37dd` | + +**Inferred** marks a conclusion drawn from code or docs that the source +doesn't state outright. + +**Pi bundle note.** `pi` runs `dist/bundle/cli.js` (`pi:pi-coding-agent/package.json` +`bin`), not the `dist/core` files cited below (lead decision 32 n1). I checked +that the bundle contains the enforcement paths I rely on: +`chunks/chunk-JVUZSMYM.js` holds "Extension failed, blocking execution", +"Tool execution was blocked" and `isAllowedTool`. The `dist/core` line +numbers are given because they are readable. + +**Doc drift.** The Claude and Codex docs are living sites. Codex's describe +a newer release than 0.160.0. Where the docs and the binary or source +disagree, this survey follows the binary or source and says so. + +--- + +## 1. Answer to the key question + +**A hook refusal is a hard block only when three conditions hold:** + +1. **Fail closed.** The harness runs the hook on every path to the action, + and a crash, timeout or missing hook stops the action. +2. **Recognisable action.** The hook can tell the action apart from + everything else: a typed tool call such as `mosaic_decision_raise` or + `mcp__mosaic__push`, not free shell text. +3. **No other route.** The agent has no other way to the action: no + credential in its environment or in a file it can read, and no network + path around the tool. + +**How each harness meets them:** + +| | Pi 0.85.1 | Claude Code 2.1.289 | Codex 0.160.0 | +|---|---|---|---| +| 1. Fail closed | **Yes** for `tool_call`. A thrown handler blocks the tool. A failed `-e` load exits 1. Every other event fails open. | **Partly.** Command and HTTP hooks fail **open** on crash, missing path, bad JSON or timeout. Deny rules hold in every mode. SDK callback hooks fail closed on PreToolUse timeout. | **No.** Hooks fail open on spawn error, timeout, bad JSON, or exit codes other than 0 and 2. Async hooks can't block. The docs call hooks "not a complete enforcement boundary". | +| 2. Recognisable action | Only for typed tools. A shell command is a string to pattern-match. | Same. The docs call `Bash(rm *)` "not a security boundary". | Same. Execpolicy rules match an argv prefix, and opaque scripts get through. | +| 3. No other route | **No**, unless there's a container. Pi has no sandbox and no path confinement, and `bash` inherits the full process environment. | Bash sandbox only (bubblewrap). File tools, MCP servers and hooks run outside it. | bwrap and seccomp sandbox for spawned commands. By default `*KEY*` and `*TOKEN*` variables pass through, and `/` stays readable. | + +**Conclusion.** None of the three harnesses can make a shell-text hook a +boundary. That matches Darkwing's section 1.4: a hook is an early stop and a +log line. A decision class becomes a hard block only in these layers: + +1. **Credential scope.** The role holds no token that can do a gated action. + Darkwing calls this "the hard line", and I agree. +2. **The `mosaic` verb.** It refuses a crossRole or gated action unless it is + given a resolved decision id. It must run in a process that holds the + credential the agent lacks, which is Darkwing's broker in 6.3. +3. **The harness's tool ceiling.** + - Pi: `--tools` filters the registry, and extensions can't re-add a name. + - Claude: deny rules and `--tools`. + - Codex: sandbox mode and requirements pins. + + The ceiling removes direct routes. It can't stop `bash` from doing + whatever the credential and network allow. +4. **An OS boundary.** A container or bwrap with the network policy actually + enforced. Without one, condition 3 fails on every harness. + +When those four are in place, a hook on the typed tool adds something +real: it refuses early, in the turn, with a pointer to the decision verb, +and it logs `action.refused` with layer `hook`. Pi's `tool_call` is the only +hook of the three that is itself fail-closed. Claude's command hooks and all +Codex hooks fail open. So on those two harnesses, any block that matters must +also be a deny rule, an execpolicy `forbidden` rule, or a missing credential. + +**Per decision class:** + +| Class | Prompt text (all three) | Pi | Claude Code | Codex | +|---|---|---|---|---| +| Routine | The only control. No gate is needed: these actions have no outside effect. | none | none | none | +| withinRole | States what the role may do alone. | `tool_result` / `tool_execution_end` can log, but fail open. The verb's event row is the record. | PostToolUse logs, fail-open. The verb is the record. | PostToolUse logs, fail-open. The verb is the record. | +| crossRole | Tells the agent to raise a decision. | **Hard** for typed tools via `tool_call` block. Advisory for `bash` text. The verb refuses. | **Hard** via a deny rule on the typed MCP tool. The hook is advisory (fail-open). The verb refuses. | Advisory hook. Execpolicy `forbidden` holds for an argv prefix only. The verb refuses. | +| Gated | Tells the agent to raise a decision routed to the human. | As crossRole, plus no credential in the container. | As crossRole, plus no credential. The sandbox `credentials` deny covers Bash only. | As crossRole, plus no credential. Needs a `shell_environment_policy` exclude and `deny_read`. | + +--- + +## 2. Pi 0.85.1 + +### 2.1 System prompt and context files + +- **Default prompt.** In order: + 1. Fixed preamble + 2. Tool list + 3. Guidelines + 4. Append section + 5. `` with each context file + 6. Skills XML (only when `read` or `bash` is active) + 7. cwd + + Source: `pi:pi-coding-agent/dist/core/system-prompt.js:81-116`. +- **`--system-prompt`.** Replaces items 1–3 only. The append section, context + files, skills and cwd are still added + (`pi:pi-coding-agent/dist/core/system-prompt.js:15-34`; `pi:pi-coding-agent/README.md:610`). + It also turns off `SYSTEM.md` discovery + (`pi:pi-coding-agent/dist/core/resource-loader.js:381`). +- **`--append-system-prompt`.** Repeatable. It turns off `APPEND_SYSTEM.md` + discovery (`pi:pi-coding-agent/dist/core/resource-loader.js:386-390`). + Each flag value is read as a file if that path exists, and used as literal + text otherwise (`pi:pi-coding-agent/dist/core/resource-loader.js:17-31`). +- **`SYSTEM.md` / `APPEND_SYSTEM.md`.** Pi checks project `.pi/` first, but + only if the project is trusted, then the global agent dir + (`pi:pi-coding-agent/dist/core/resource-loader.js:809-830`). +- **Context files.** Pi takes the first of `AGENTS.override.md`, `AGENTS.md`, + `AGENTS.MD`, `CLAUDE.md`, `CLAUDE.MD` in each directory. + - Order: the global agent-dir file first, then every ancestor from `/` down + to the cwd (`pi:pi-coding-agent/dist/core/resource-loader.js:32-52, 82-109`). + - Context files load **regardless of project trust** + (`pi:pi-coding-agent/docs/security.md:27`). + - `--no-context-files` turns them off + (`pi:pi-coding-agent/dist/core/resource-loader.js:371-378`). It does not turn + off `SYSTEM.md`, `APPEND_SYSTEM.md` or skills, which have their own flags + (inferred from separate code paths, `:371-399`). +- **Per-turn rewrite by extensions.** `before_agent_start` can replace the + system prompt for a turn (`pi:pi-coding-agent/docs/extensions.md:530-565`). + `before_provider_request` can rewrite the provider payload (`:705-720`). + Enforcement implication: a loaded extension can change what the model is + told. + +### 2.2 Skills and commands + +- **Skill locations.** + - `~/.pi/agent/skills/` and `~/.agents/skills/`. + - Project `.pi/skills/` and `.agents/skills/` (trusted projects only). + - Packages and the `skills` setting. + - `--skill `, which still applies under `--no-skills`. + + Source: `pi:pi-coding-agent/docs/skills.md:24-42`. +- **What the model sees.** Only each skill's name and description go into + the prompt (`:65-72`). `disable-model-invocation: true` hides a skill + (`:150`). +- **Skill `allowed-tools` is not enforced.** It is documented as experimental + (`pi:pi-coding-agent/docs/skills.md:149`), and nothing in `dist` reads it. +- **`/skill:name args`.** Expands in every mode, including `-p` and RPC + (`pi:pi-coding-agent/dist/core/agent-session.js:983-996`). +- **Prompt templates.** `--no-prompt-templates` turns them off + (`pi:pi-coding-agent/dist/cli/args.js:167`). +- **Extension commands.** `pi.registerCommand` runs before the `input` event + and skips it (`pi:pi-coding-agent/dist/core/agent-session.js:828-835`). + +### 2.3 Hooks (extensions) + +- **Loading.** Extensions are TypeScript loaded through jiti. + - `-e/--extension` is repeatable. + - `--no-extensions` drops discovered and settings extensions but keeps the + `-e` ones (`pi:pi-coding-agent/dist/core/resource-loader.js:409-411`, checked). + - A load failure, a missing `-e` path, or a duplicate tool or flag name exits + 1 in every mode (`pi:pi-coding-agent/dist/main.js:722-730`). +- **Events that can block.** + - `tool_call` returns `{block: true, reason?, terminate?}` + (`pi:pi-coding-agent/docs/extensions.md:778-818`). The first blocking + handler wins (`pi:pi-coding-agent/dist/core/extensions/runner.js:745-763`, checked). + - `input` can swallow a prompt. + - The `session_before_*` events can cancel. +- **Notification-only events.** `session_start`, `session_shutdown`, + `agent_end`, `agent_settled`, `tool_execution_*` + (`pi:pi-coding-agent/docs/extensions.md:277-349`). +- **Every model tool call passes `tool_call`.** That includes extension and + SDK tools, through `beforeToolCall` + (`pi:pi-coding-agent/dist/core/agent-session.js:223-243`; + `pi:pi-agent-core/dist/agent-loop.js:400-437`, checked). A blocked call + becomes an error result, and `execute` is never called. +- **Failure behaviour (checked).** + - A throwing `tool_call` handler blocks the tool: the error is rethrown and + caught in `prepareToolCall`, which returns an error result + (`pi:pi-coding-agent/dist/core/agent-session.js:237-242`; + `pi:pi-agent-core/dist/agent-loop.js:452-458`). The docs say "`tool_call` + errors block the tool (fail-safe)" (`pi:pi-coding-agent/docs/extensions.md:2925`). + - Every other handler that throws is logged, and the agent continues + (`:2924`). + - With no UI (`-p`, json), `ctx.ui.confirm` returns `false` + (`pi:pi-coding-agent/dist/core/extensions/runner.js:88-91`). +- **What the hook doesn't see.** + - RPC `bash` and interactive `!` commands go through `user_bash`, not + `tool_call` and not `--tools` + (`pi:pi-coding-agent/dist/modes/rpc/rpc-mode.js:441-459`). That route is + host-driven, not model-driven. + - An extension can rewrite `event.input` with no re-validation + (`pi:pi-coding-agent/docs/extensions.md:786-791`). +- **Lead decision 31.** A Console-bound seat loads no explicit extensions and + launches with `--no-extensions` and no `--extension` + ([repo:docs/plans/2026-09-26_lead-decisions.md:392-405]). A Pi gate + extension conflicts with that until CHAT-06 brings reviewed, pinned + extensions back. See section 8. + +### 2.4 MCP + +None, by design: "**No MCP.** Build CLI tools with READMEs (see Skills), +or build an extension that adds MCP support" +(`pi:pi-coding-agent/README.md:499`). The route is an extension that runs an +MCP client and calls `pi.registerTool()` for each server tool +(`pi:pi-coding-agent/docs/extensions.md:2365-2400`). Tools registered that way +are subject to `--tools` and `tool_call` like any other (inferred, +`pi:pi-coding-agent/dist/core/agent-session.js:2105-2180`). + +### 2.5 Permissions and sandbox + +- **No permission prompts and no sandbox.** "No permission popups. Run in a + container, or build your own confirmation flow with extensions" + (`pi:pi-coding-agent/README.md:503`; `pi:pi-coding-agent/docs/security.md:31-35`). + Project trust "is not a sandbox" (`:7`). +- **Tool allowlist.** `--tools a,b`, `--exclude-tools`, `--no-tools`, + `--no-builtin-tools` (`pi:pi-coding-agent/dist/cli/args.js:94-111`). + - The allowlist filters the registry, and `setActiveToolsByName` ignores + names outside it (`pi:pi-coding-agent/dist/core/agent-session.js:2105-2159, + 659-673`, checked). An extension therefore can't switch an excluded tool + back on. + - Gap: an extension can register its own tool under an allowed name such as + `read` (`pi:pi-coding-agent/docs/extensions.md:2080-2093`). +- **No path confinement.** Absolute and `~` paths resolve as given + (`pi:pi-coding-agent/dist/core/tools/path-utils.js:38-44`, checked). A + read-only tool set can still read `auth.json`. +- **Project trust.** `-a/--approve` trusts project-local files for one run, + and `-na/--no-approve` ignores them (`pi:pi-coding-agent/README.md:615-616`; + `pi:pi-coding-agent/dist/cli/args.js:205-208`, checked). Headless runs with no + saved decision silently skip project `.pi/` resources + (`pi:pi-coding-agent/docs/security.md:29`). + +### 2.6 Per-session environment and credentials + +- **Agent directory.** `PI_CODING_AGENT_DIR` relocates the whole agent dir + (`pi:pi-coding-agent/dist/config.js:406, 421-462`): `auth.json`, + `models.json`, `settings.json`, `sessions/`, `trust.json`, and the global + `AGENTS.md`, `SYSTEM.md` and skills. +- **Credential resolution.** `--api-key`, then `auth.json`, then the + environment variable, then `models.json` + (`pi:pi-coding-agent/docs/providers.md:310-317`). + - An `auth.json` `key` of `"!command"` runs a shell command + (`:159-179`). + - SDK: `InMemoryCredentialStore` (`pi:pi-coding-agent/docs/sdk.md:466-474`). +- **The `bash` tool inherits Pi's whole environment.** The spawn env is + `...process.env` (`pi:pi-coding-agent/dist/utils/shell.js:117-123`, + checked). The model's bash also gets `PI_SESSION_ID`, `PI_PROVIDER` and + others (`pi:pi-coding-agent/docs/environment-variables.md:11-49`). Any key in + Pi's environment is printable by the model. +- **Sessions.** `--session-dir`, `--session-id`, `--fork`, `--no-session`, + `-c` (`pi:pi-coding-agent/dist/cli/args.js:46-90`). + +### 2.7 Headless + +- **Modes.** `-p` (text), `--mode json` (JSONL events) and `--mode rpc` + (JSONL commands over stdio) (`pi:pi-coding-agent/docs/rpc.md:20-37`). An + unknown `--mode` value is ignored silently + (`pi:pi-coding-agent/dist/cli/args.js:40-45`). +- **Exit codes.** + - 0 on success. + - 1 when `stopReason` is `error` or `aborted`, in text mode only + (`pi:pi-coding-agent/dist/modes/print-mode.js:110-118`). + - 1 on a thrown error or a startup error. + - 143 on SIGTERM, 129 on SIGHUP. + - Inferred: in `--mode json`, a non-throwing LLM error exits 0. +- **SDK.** `createAgentSession({ tools, customTools, resourceLoader, ... })` + (`pi:pi-coding-agent/docs/sdk.md:16-66`). + +--- + +## 3. Claude Code 2.1.289 + +### 3.1 System prompt and context files + +- **CLAUDE.md scopes.** + - Managed: `/etc/claude-code/CLAUDE.md`, which can't be excluded. + - User: `~/.claude/CLAUDE.md` and `rules/`. + - Project: `./CLAUDE.md`, `.claude/rules/`. + - Local: `CLAUDE.local.md`. + + Files in the cwd and its ancestors load at launch, root first. + Subdirectory files load lazily. `AGENTS.md` is read only when no CLAUDE.md + exists (v2.1.277+). `@imports` go up to four hops. + Source: [cc:memory § How CLAUDE.md files load; § AGENTS.md]. +- **CLAUDE.md arrives as a user message after the system prompt** + [cc:memory § Claude isn't following my CLAUDE.md]. It is "not a hard + enforcement layer" [cc:memory § Exclude specific CLAUDE.md files]. +- **System prompt flags.** + - `--system-prompt` / `--system-prompt-file` replace the prompt; + `--append-system-prompt` / `--append-system-prompt-file` append + [cc:cli-reference § System prompt flags]. + - The `-file` variants and `--max-turns` are documented but absent from + `[cc:help]`. Inferred: they are hidden flags. Not exercised. + - Replacing the prompt does not remove CLAUDE.md + [cc:sdk-sysprompt § Turn off the context your agent replaces]. +- **Turning CLAUDE.md off.** `CLAUDE_CODE_DISABLE_CLAUDE_MDS=1`, + `--setting-sources` without `project`/`user`/`local`, `--bare`, or + `--safe-mode` [cc:env-vars; cc:help]. Auto memory has its own switch, + `CLAUDE_CODE_DISABLE_AUTO_MEMORY=1` [cc:memory § Enable or disable auto memory]. + +### 3.2 Skills and commands + +- **Where skills are discovered.** + - Managed, `~/.claude/skills`, and project `.claude/skills` (walking up to + the repo root). + - Nested directories, lazily. + - `--add-dir` directories and plugins. + - claude.ai sync. + + Source: [cc:skills § Where skills live]. `.claude/commands/x.md` and + `.claude/skills/x/SKILL.md` both create `/x`. +- **Restricting skills.** + - Permission rules: `Skill`, `Skill(name)`. + - `skillOverrides`, `disable-model-invocation`, `--disable-slash-commands` + [cc:skills § Restrict Claude's skill access]. + - A skill's `allowed-tools` pre-approves tools for one turn. It doesn't + restrict anything, and workspace trust doesn't gate it + [cc:skills § Pre-approve tools for a skill]. +- **Plugins, loaded per session.** `--plugin-dir` / `--plugin-url` bundle + skills, agents, hooks, MCP and output styles [cc:help]. + +### 3.3 Hooks + +- **Events.** These include SessionStart, UserPromptSubmit, PreToolUse, + PermissionRequest, PostToolUse, Stop, SubagentStart/Stop and SessionEnd + [cc:hooks § Hook lifecycle]. +- **Where hooks come from.** User, project, local and managed settings, + plugins, skill and subagent frontmatter, and SDK callbacks. All of them + merge [cc:hooks § Hook locations]. +- **How a hook blocks.** Exit 2 blocks PreToolUse, and JSON can't undo it. + `hookSpecificOutput.permissionDecision: "deny"` also blocks. Deny rules are + evaluated whatever a hook returns, so a hook's `allow` can't override them + [cc:hooks § Exit code 2 behavior per event; cc:permissions § Extend permissions with hooks]. +- **Fail-open (checked in the fetched docs).** + - Exit 1 or any other code is non-blocking. + - A hook that can't start is non-blocking: "a mistyped path in + `settings.json` leaves the gate silently disabled" (hooks.md line 837 of the + fetched page). + - Invalid JSON, a non-2xx HTTP response, or a command or HTTP timeout is + also non-blocking [cc:hooks § Exit code output; § Timeouts]. +- **The exception is SDK callback hooks (checked).** On a PreToolUse timeout, + "Claude Code doesn't run the tool call" [cc:sdk-hooks § Hook timeout]. They + also survive `allowManagedHooksOnly` and `disableAllHooks`. What happens + when a callback throws was not established. +- **Coverage.** + - Hooks fire inside subagents and for MCP tools (`mcp__server__tool`). + - They do not fire for `@file` prompt references, and `--bare` skips them. + - Hooks run outside the sandbox [cc:hooks; cc:sandboxing]. +- **Trust.** `-p` and SDK sessions treat the folder as trusted, so a repo's + `.claude/settings.json` hooks run with no dialog [cc:hooks § Security considerations]. + +### 3.4 MCP + +- **Scopes.** Local and user in `~/.claude.json`, project `.mcp.json`, plugin, + and claude.ai connectors. Managed `managed-mcp.json` takes exclusive + control [cc:mcp § MCP installation scopes; cc:managed-mcp]. +- **Per-session servers.** `--mcp-config ` with + `--strict-mcp-config` uses only those servers [cc:cli-reference]. +- **`-p` connects `.mcp.json` servers without asking** + [cc:headless § Start faster with bare mode]. +- **Tool names** are `mcp____`, and permission rules accept + them [cc:permissions]. MCP servers run outside the Bash sandbox + [cc:sandboxing]. + +### 3.5 Permissions and sandbox + +- **Evaluation order.** + 1. PreToolUse hooks + 2. Deny rules + 3. Ask rules + 4. Permission mode + 5. Allow rules + 6. `canUseTool` or `--permission-prompt-tool` + + Deny rules block in every mode, including `bypassPermissions`. "Permission + rules are enforced by Claude Code, not by the model" + [cc:permissions; cc:sdk-perm § How permissions are evaluated]. +- **Modes.** `default`, `acceptEdits`, `plan`, `auto`, `dontAsk` (denies + anything that would prompt) and `bypassPermissions` + [cc:permission-modes]. `--permission-prompts none` denies prompts unless a + PermissionRequest hook allows them (v2.1.259+) + [cc:headless § Turn off permission prompts in unattended runs]. +- **Bash rules are text matches.** `Bash(rm *)` does not catch `/bin/rm` or + `bash -c 'rm …'` [cc:permissions § What a Bash rule doesn't match]. +- **Sandbox.** bubblewrap on Linux, covering Bash and its children only. + - Off by default. + - Keys: `sandbox.enabled`, `failIfUnavailable`, `allowUnsandboxedCommands:false`, + `network.allowedDomains`, `filesystem.denyRead`, `credentials` + [cc:sandboxing]. +- **Precedence.** Managed, then CLI/`--settings`, then local, then project, + then user. A deny at any level can't be overridden + [cc:settings § Settings precedence]. +- **`--restricted`.** Removes code-running tools and WebFetch, ignores user, + project and local settings, and refuses bypass [cc:help]. + +### 3.6 Per-session environment and credentials + +- **`CLAUDE_CONFIG_DIR`** relocates settings, sessions, plugins and + `.credentials.json`. It is the documented way to run multiple accounts + [cc:env-vars; cc:authentication § Credential management]. + - `~/.claude.json` (MCP and trust state) is not governed by + `settingSources`. Relocate it with `CLAUDE_CONFIG_DIR` + [cc:sdk-features § What settingSources does not control]. +- **Configuration per run.** `--settings ` fills the CLI layer. + `--setting-sources ""` drops user, project and local; managed policy loads + regardless [cc:cli-reference; cc:sdk-ts § Options]. +- **Credential precedence.** + 1. Cloud provider + 2. `ANTHROPIC_AUTH_TOKEN` + 3. `ANTHROPIC_API_KEY` + 4. `apiKeyHelper` + 5. `CLAUDE_CODE_OAUTH_TOKEN` + 6. `/login` OAuth + + Source: [cc:authentication § Authentication precedence]. +- **`CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1`** strips credentials from Bash, hook + and stdio-MCP subprocess environments [cc:env-vars]. + +### 3.7 Headless + +- **`-p` output.** `--output-format text|json|stream-json`, `--json-schema`, + `--max-turns`, `--max-budget-usd`. `-p` skips the trust dialog, and + settings files that fail validation are "silently ignored" [cc:help]. +- **`--bare`.** + - Skips hooks, plugins, CLAUDE.md, auto memory and keychain/OAuth. + - Connects only command-line MCP servers. + - Authenticates only from `ANTHROPIC_API_KEY` or an `apiKeyHelper`. + - The docs say it "will become the default for `-p` in a future release" + [cc:headless § Start faster with bare mode]. +- **Agent SDK.** `settingSources: []`, `hooks` (in-process callbacks), + `canUseTool` (reached only when the flow falls through to a prompt), + `disallowedTools`, `tools`, `mcpServers`, `strictMcpConfig`, and `env` + (which replaces the environment) [cc:sdk-ts § Options]. + +--- + +## 4. Codex CLI 0.160.0 + +### 4.1 System prompt and context files + +- **Base prompt.** `model_instructions_file`, then inline `instructions`, + then the built-in prompt [cx:S:codex-rs/core/src/config/mod.rs ~L3990]. + - The docs call `instructions` "reserved". The source uses it. + - `developer_instructions` becomes a developer-role message + [cx:S:codex-rs/config/src/config_toml.rs]. +- **Global AGENTS.md.** `$CODEX_HOME/AGENTS.override.md`, else + `$CODEX_HOME/AGENTS.md` [cx:S:codex-rs/codex-home/src/instructions/mod.rs]. + - The legacy `~/.codex/instructions.md` is not read. Inferred: the copy on + this machine is inert. +- **Project chain.** From the project root (`.git` marker) down to the cwd. + - Files are concatenated root first, with a **total** budget of 32 KiB + [cx:S:codex-rs/core/src/agents_md.rs]. The docs say the limit is per file. + - The chain is skipped only when the project is explicitly + `trust_level = "untrusted"` [cx:S:agents_md.rs L64]. +- **Config layers, low to high:** + 1. Defaults + 2. `/etc/codex/config.toml` + 3. Cloud bundle + 4. `$CODEX_HOME/config.toml` + 5. `$CODEX_HOME/.config.toml` (`-p`) + 6. Project `.codex/config.toml` (trusted projects only) + 7. `-c` flags + + Source: [cx:S:codex-rs/config/src/loader/mod.rs L107-134]. + +### 4.2 Skills and commands + +- **Skill roots.** Project `<.codex>/skills`, repo `.agents/skills` (cwd up to + the project root), user `~/.agents/skills` and `$CODEX_HOME/skills` + (deprecated), admin `/etc/codex/skills`, bundled, and plugins + [cx:S:codex-rs/ext/skills/src/host_roots.rs]. +- **Trust.** Inferred: repo `.agents/skills` loads with no trust check + [cx:S:codex-rs/config/src/state.rs]. +- **Controls.** `[[skills.config]] enabled=false`; + `policy.allow_implicit_invocation` [cx:D:skills]. +- **Custom prompts.** `~/.codex/prompts` has no loader in 0.160.0. Inferred: + removed. + +### 4.3 Hooks + +- **Status.** Stable and on by default. Events include SessionStart, + UserPromptSubmit, PreToolUse, PermissionRequest, PostToolUse, Stop and + SessionEnd [cx:D:hooks]. +- **Sources.** `hooks.json` or a `[hooks]` table next to each config layer, + plugins, and managed `requirements.toml` [cx:D:hooks#Where Codex looks for hooks]. +- **Trust.** Non-managed hooks are trusted by hash after review in `/hooks`. + - `--dangerously-bypass-hook-trust` skips that check [cx:H:exec]. + - Project hooks need a trusted project. + - `allow_managed_hooks_only` keeps only managed hooks. +- **How a hook blocks.** PreToolUse blocks on `permissionDecision: "deny"`, + legacy `{"decision":"block"}`, or exit 2 with non-empty stderr. + `"ask"` and `continue:false` are unsupported, and the tool proceeds + [cx:D:hooks#PreToolUse]. +- **Fail-open (checked in source).** In + [cx:S:codex-rs/hooks/src/events/pre_tool_use.rs], a spawn error or timeout + sets `Failed` with `should_block` false (L205-211). Exit 2 with empty + stderr, invalid JSON, and any other exit code also set `Failed` without + blocking (L254-286). + - Async hooks can't block [cx:S:codex-rs/hooks/src/engine/mod.rs ~L154]. + - Hosted tools such as web search are not covered. + - The docs: "Treat tool hooks as a useful guardrail, not a complete + enforcement boundary" [cx:D:hooks#Tool coverage]. +- **Hooks run unsandboxed.** They run through the user's shell (inferred: no + sandbox reference in the hooks crate). +- **`notify`.** Fires after each turn, fire-and-forget. + +### 4.4 MCP + +- **Configuration.** Codex is a client over stdio and streamable HTTP. + - Per-server keys include `enabled_tools`, `disabled_tools`, `required` + and per-tool `approval_mode` [cx:D:config-reference]. + - Project servers load only in trusted projects. + - In `exec`, a failing `required` server exits with an error + [cx:D:noninteractive]. +- **Managed allowlist.** Requirements can pin a server's identity. +- **Not sandboxed.** MCP servers run outside the sandbox + [cx:D:permissions#What the network proxy does not control]. +- **`codex mcp-server` was removed** in 0.160.0 [cx:L]. + +### 4.5 Permissions and sandbox + +- **Approval.** `-a on-request|never`; `untrusted` and `on-failure` are + rejected by the CLI [cx:H:root]. Config also takes a `granular` table. +- **Sandbox modes.** `read-only`, `workspace-write`, `danger-full-access`, + and named `[permissions.]` profiles [cx:D:permissions]. + - With no `sandbox_mode` set, any trust decision means `workspace-write`; + otherwise the default is read-only + [cx:S:codex-rs/config/src/config_toml.rs ~L826-834]. +- **Linux sandbox.** bubblewrap with `--ro-bind / /`, writable roots, + `.git` and `.codex` re-bound read-only, `no_new_privs`, and a seccomp + network filter [cx:S:codex-rs/linux-sandbox/src/bwrap.rs]. + - Network is off in `workspace-write` unless `network_access=true`. + - Domain rules need the experimental `network_proxy`. +- **Execpolicy.** `.rules` files with Starlark + `prefix_rule(..., decision=allow|prompt|forbidden)`, next to each config + layer. The most restrictive rule wins, and matching is on argv prefix only + [cx:D:rules; cx:S:codex-rs/core/src/exec_policy.rs ~L395]. +- **Managed `requirements.toml`.** In `/etc/codex/` or the cloud bundle. It + pins allowed approval policies, sandbox modes, MCP identities, hooks, + rules, `deny_read` and features [cx:D:enterprise/managed-configuration]. +- **Bypass flags.** `--dangerously-bypass-approvals-and-sandbox` and + `-s danger-full-access`. Inferred: only requirements can forbid them. + +### 4.6 Per-session environment and credentials + +- **`CODEX_HOME`.** Isolates config, auth, sessions, skills and hooks per + process [cx:H:root]. +- **Auth.** + - `CODEX_API_KEY` from the environment (exec and app-server). + - Then ephemeral tokens. + - Then the persisted store (`file`, `keyring`, `auto`, `ephemeral`) + [cx:S:codex-rs/login/src/auth/manager.rs ~L1488]. + - `OPENAI_API_KEY` is not used for login. +- **Model-run commands inherit secrets by default (checked).** + `shell_environment_policy` defaults to `inherit = "all"`, and + `ignore_default_excludes` defaults to true + ([cx:S:codex-rs/config/src/shell_environment_policy.rs:136] + `unwrap_or(true)`). The `*KEY*`/`*SECRET*`/`*TOKEN*` excludes therefore + don't apply unless set to false. +- **`auth.json` is readable inside the sandbox** (inferred), unless + `deny_read` or a keyring/ephemeral store is used. + +### 4.7 Headless + +- **`codex exec`.** + - `--json` gives JSONL events. Other options: `-o FILE`, `--output-schema`, + `--ephemeral`, `--skip-git-repo-check`, `exec resume`. + - `exec` forces `approval_policy = never` + [cx:S:codex-rs/exec/src/lib.rs L576]. + - Exits 0 on success and 1 on any failure. + - `--full-auto` is rejected in 0.160.0. +- **`codex app-server`.** JSON-RPC over stdio, unix or ws. + - The host answers `item/commandExecution/requestApproval` and + `item/fileChange/requestApproval` in-band [cx:D:app-server]. + - Inferred: no answer means no run, so it fails closed. It covers + escalations only, not every tool call. +- **TS SDK.** Wraps `codex exec --experimental-json`. Its `env` option + replaces `process.env` [cx:S:sdk/typescript/src/exec.ts]. + +--- + +## 5. Side by side + +| Area | Pi 0.85.1 | Claude Code 2.1.289 | Codex 0.160.0 | +|---|---|---|---| +| Replace the system prompt | `--system-prompt` (drops `SYSTEM.md` discovery) | `--system-prompt[-file]` | `model_instructions_file` ("strongly discouraged") | +| Add to it | `--append-system-prompt` | `--append-system-prompt[-file]` | `developer_instructions`, `$CODEX_HOME/AGENTS.md` | +| Turn off ambient context | `--no-context-files` | `CLAUDE_CODE_DISABLE_CLAUDE_MDS=1`, `--setting-sources ""`, `--bare` | Only an explicit `untrusted` project entry | +| Per-session skills | `--skill ` with `--no-skills` | `--plugin-dir`, `--add-dir` | `$CODEX_HOME/skills` (deprecated), plugins | +| Pre-tool block | `tool_call`, **fail-closed** | PreToolUse, **fail-open**; SDK callback fail-closed on timeout | PreToolUse, **fail-open** | +| Hard tool ceiling | `--tools` (registry filter) | deny rules, `--tools`, `--disallowedTools` | sandbox mode; no per-tool built-in allowlist was found | +| MCP | none (extension only) | `--mcp-config` + `--strict-mcp-config` | `mcp_servers` in config, `enabled_tools` | +| OS sandbox | none | Bash only (bwrap) | spawned commands (bwrap + seccomp) | +| Config isolation | `PI_CODING_AGENT_DIR` | `CLAUDE_CONFIG_DIR` + `--settings` + `--setting-sources ""` | `CODEX_HOME` | +| Secrets in the shell env | inherited | inherited unless `SUBPROCESS_ENV_SCRUB=1` | inherited by default | +| Machine-wide lock | none | `/etc/claude-code` managed settings | `/etc/codex/requirements.toml` | +| Headless | `-p`, `--mode json\|rpc`, SDK | `-p`, stream-json, Agent SDK | `exec --json`, app-server, SDK | + +--- + +## 6. What exists today + +### 6.1 Container worker path (`adapters/pi`) + +`scripts/mosaic-task.mjs` runs `docker compose run --rm -T mosaic-agent`. +Inside the container: + +1. `src/run-agent.sh` checks the adapter name. +2. `src/load-contracts.sh` writes `/var/lib/mosaic/system-prompt.md` from: + 1. CONSTITUTION and STANDARDS + 2. SOUL, or `MOSAIC_AGENT_SOUL_FILE` + 3. agent identity + 4. `/var/lib/mosaic/user/*.md` + 5. the mission +3. `adapters/pi/adapter.sh` execs pi. + +| Area | What the adapter does | Hard? | +|---|---|---| +| System prompt | `--system-prompt ""` [repo:adapters/pi/adapter.sh:92], which also turns off `SYSTEM.md` discovery | n/a (advisory content) | +| Ambient context | `--offline --no-extensions --no-skills --no-prompt-templates --no-themes --no-context-files` | Yes, for what loads | +| Tools | `--tools $MOSAIC_TOOLS` [repo:adapters/pi/adapter.sh:48]. The role ceiling is intersected with the seat in `validateRole` / `resolve-role` [repo:scripts/mosaic-task.mjs:249-272, 674] | Yes (registry filter) | +| Skills | `--skill` per `MOSAIC_SKILLS` dir; a missing dir exits 2 [:57-58] | n/a | +| Hooks | none | — | +| MCP | none | — | +| Network | the role's `network` is declared and "enforced when network policy lands" [repo:scripts/mosaic-task.mjs:249-250]; `compose.yaml` sets no `network_mode` | **No** | +| Credentials | `ZAI_API_KEY`, `ANTHROPIC_API_KEY` env [repo:compose.yaml:36-37]; pi `auth.json` mounted read-only [:43] | **No.** A role with `bash` (researcher has it) can print the env keys, and `read` can open the mounted `auth.json` (Pi has no path confinement) | +| Headless | `-p "$MOSAIC_REQUEST"`, stdin empty, SIGKILL on timeout; stdout is the response, and exit 0 means success [repo:adapters/README.md:37-39] | text mode, so an LLM error exits 1 | +| Sandbox | the container: user 1000:1000, data root mounted | Yes for the filesystem, **no** for the network | + +The adapter contract [repo:adapters/README.md:24-27, 40-48] passes four +inputs: the prompt file, the request, the provider and the model. It also +requires that "adapters never read configuration files". It has no slot for +a tool policy, a gate, skills chosen per role, MCP config or role +credentials. The pi adapter takes tools and skills through compose +environment variables that the contract table doesn't list. Adding a harness +requires an allowlist entry; today the allowlist is +`SUPPORTED_ADAPTERS = ["pi", "mock"]` [repo:scripts/mosaic-config.mjs:33]. + +### 6.2 Host Pi seats (`scripts/agent-host-dev.sh`) + +The command is [repo:scripts/agent-host-dev.sh:135-141]: + +`pi --approve --offline --no-context-files --no-extensions --no-skills +--no-prompt-templates --no-themes --extension .pi/extensions/goal/index.ts +--skill … --tools read,bash,edit,write,grep,find,ls,goal_report +--append-system-prompt …` + +- **What it already does.** The prompt is a hashed snapshot. It assembles + CONSTITUTION, STANDARDS, the seat's SOUL, USER.md, AGENTS.md and the seat's + CONTEXT.md, records the sha256, and passes the result through + `--append-system-prompt`. Context-file and skill discovery are off, and + the `--tools` list is explicit. +- **Gaps:** + - It uses `--append-system-prompt`, not `--system-prompt`, so `SYSTEM.md` + discovery is still on. `--approve` trusts project-local `.pi/`. Today + the repo has no `.pi/SYSTEM.md` or `.pi/settings.json`, and + `--no-extensions` keeps `.pi/extensions/` out. But a commit that adds + `.pi/SYSTEM.md` would replace Pi's preamble in every host seat, and a + `.pi/settings.json` would merge over the global settings. + - There's no `PI_CODING_AGENT_DIR`. Every seat shares + `~/.pi/agent/settings.json`, `auth.json` and `trust.json`. The model can + read and edit any of them, because Pi has no path confinement. + - There's no role binding. Seats get a fixed tool list, not a role ceiling. + - There's no gate. The goal extension is the only extension, and it + handles `before_agent_start`, `tool_result`, `agent_settled`, + `session_start` and `session_shutdown`. None of these blocks a tool. + +### 6.3 Rocko (`agents/rocko/launch.sh`, Claude Code) + +The command is `claude --model sonnet --name Rocko --append-system-prompt +"$(cat context.md)" --resume|--session-id` [repo:agents/rocko/launch.sh:86-87], +registered through `scripts/mosaic launch --harness claude-code` [:8]. + +Nothing limits setting sources, CLAUDE.md, MCP or permissions, so: +- Jason's `~/.claude/settings.json` applies. Its keys include `hooks`, + `mcpServers`, `enabledPlugins` and `skipDangerousModePermissionPrompt` + (values not read). +- Project `CLAUDE.md` (`@AGENTS.md`) and `~/.claude/CLAUDE.md` load as a + user message on top of the appended context. AGENTS.md therefore reaches + the model twice. +- Rocko's tools aren't limited by any role. + +### 6.4 Codex + +No adapter, launcher or seat uses Codex. Filbert's seat runs Pi with an +`openai-codex` provider model, which is a different thing. Local +`~/.codex/config.toml` has project `trust_level` entries and a +`[shell_environment_policy.set]` table (values not read). + +--- + +## 7. What a generator must emit + +### 7.1 Inputs + +- The role definition, `roles/.json` version 2, with its contract file + (Darkwing 1.2). +- The resolved variables and their provenance: system, business, project, + agent (Darkwing 2). The agent layer carries `harness`, `model` and + thinking level. +- `contracts/`, and the frozen action vocabulary (Darkwing 1.4). +- Credential references, by service name. The generator never reads the + values. + +### 7.2 Outputs common to all harnesses + +Each launch gets a directory, for example `/launch//`. It is +written once, made read-only, and every file's sha256 goes into the +`session.launched` event. + +1. **`prompt.md`** holds `contracts/`, the role contract, the resolved + non-secret variables, and the role's authority rendered as prose: + - what the role may do alone; + - what needs a decision, and from whom; + - how to raise one: name the verb or tool. + + This is advisory. Its job is to keep the agent from reaching a block, not + to be the block. +2. **`policy.json`** holds the action-to-class map for this role instance, + the tool ceiling, the network class, and the command classifier patterns + from Darkwing 1.4 item 3. The gate reads it. +3. **The gate.** One harness-neutral program maps `{tool, input}` to + `{allow | block, reason, action, class}`, with a thin wrapper per + harness. It applies one rule to every call: a typed tool call for a + crossRole or gated action is blocked with a pointer to + `mosaic decision raise`. A shell command that matches a classifier + pattern is blocked the same way and logged as `hook` layer (advisory). +4. **Skills.** The `ms-*` set chosen for the role, plus a generated + decision-raising skill that documents the verbs. +5. **Typed tools for vocabulary actions.** `decision.raise`, `message.send` + and the role's withinRole actions are exposed as tools that call the + `mosaic` verbs through the broker. On Pi they are extension tools; on + Claude and Codex, a `mosaic` MCP server. This makes condition 2 in + section 1 hold, and it is the only way a hook block can be exact. +6. **A launch manifest.** argv, the names of environment variables (never + values), and file digests. + +### 7.3 Pi + +| Output | Form | +|---|---| +| Prompt | `--system-prompt /prompt.md` (replaces the preamble and turns off `SYSTEM.md` discovery) | +| Ambient off | `--no-context-files --no-skills --no-prompt-templates --no-themes --no-extensions --no-approve --offline` | +| Skills | `--skill `, repeated | +| Gate and typed tools | `-e `. It reads `policy.json` and registers a `tool_call` handler plus the typed tools. A load failure exits 1, so a missing gate refuses to start. **Blocked by decision 31 for Console-bound seats** (section 8) | +| Tool ceiling | `--tools `, including the typed tool names | +| Isolation | `PI_CODING_AGENT_DIR=/pi-agent`, holding a generated `settings.json`, no `auth.json` unless one is required, and no `trust.json` | +| Credentials | Provider key through env or `--api-key`. Role credentials stay in the broker, not in Pi's env. Anything in Pi's env is visible to `bash` | +| Headless | `-p` (text mode, so an LLM error exits 1) for workers; `--mode rpc` for Console-bound seats | +| Network | Only a container with `network_mode` set gives a hard line. Pi has no setting for it | + +### 7.4 Claude Code + +| Output | Form | +|---|---| +| Isolation | `CLAUDE_CONFIG_DIR=/claude`, `--setting-sources ""`, `CLAUDE_CODE_DISABLE_CLAUDE_MDS=1`, `CLAUDE_CODE_DISABLE_AUTO_MEMORY=1`, `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1`. Managed settings in `/etc/claude-code`, if any ever exist, still apply | +| Prompt | `--system-prompt-file` or `--append-system-prompt-file `. Both are hidden in 2.1.289 and need a probe. `--append-system-prompt "$(cat …)"` works today (Rocko) | +| Settings | `--settings /settings.json`, which must contain: (1) `permissions.deny` for every built-in tool outside the ceiling, plus `mcp__mosaic__` for crossRole and gated actions; (2) `hooks.PreToolUse` with matcher `*`, pointing at the gate wrapper; (3) `permissions.disableBypassPermissionsMode: "disable"`; (4) `sandbox` with `enabled`, `failIfUnavailable`, `allowUnsandboxedCommands:false` and `network.allowedDomains` from the role's network class; (5) `sandbox.filesystem.denyRead` for credential paths | +| Tool ceiling | `--tools `. Pi names map to Claude names: `read`→`Read`, `write`→`Write`, `edit`→`Edit`, `bash`→`Bash`, `grep`→`Grep`, `find`→`Glob`. `ls` has no one-to-one tool, so the mapping needs checking against [cc:tools-reference] | +| Skills | `--plugin-dir /plugin` with `skills/`. Whether `--add-dir` skills still load under `--setting-sources ""` is unverified | +| MCP | `--mcp-config /mcp.json --strict-mcp-config`, with the `mosaic` broker server only | +| Gate failure mode | Command hooks fail open, so a blocked action must also be a deny rule. The wrapper must turn every internal error into exit 2 (`trap`). The launcher must run a canary call that the gate has to refuse before the session counts as launched; that catches the "mistyped path" case. In headless SDK runs, an in-process callback hook fails closed on timeout | +| Credentials | Subscription OAuth lives in `.credentials.json` under the config dir, so a private `CLAUDE_CONFIG_DIR` needs its own login, a `CLAUDE_CODE_OAUTH_TOKEN` (`claude setup-token`), or `ANTHROPIC_API_KEY` (section 8, item 5). `--bare` would be the stronger hardening, but it accepts only an API key or `apiKeyHelper` | +| Headless | `-p --output-format stream-json --permission-mode dontAsk --max-turns N`, or the Agent SDK with `settingSources: []` and callback hooks | + +### 7.5 Codex CLI + +| Output | Form | +|---|---| +| Isolation | `CODEX_HOME=/codex` with a generated `config.toml`. Give the project path an explicit trust entry: `untrusted` keeps project config, hooks, rules and the AGENTS.md chain out; `trusted` lets them in | +| Prompt | `developer_instructions` from `prompt.md`, or `$CODEX_HOME/AGENTS.md`. `model_instructions_file` replaces the built-in prompt but is "strongly discouraged" | +| Skills | `$CODEX_HOME/skills` (deprecated) or a plugin. `~/.agents/skills` is shared user state, not per session. Repo `.agents/skills` loads without a trust check (inferred). This repo has no `.agents/` today | +| Gate | `[hooks]` PreToolUse with a command pointing at the gate wrapper. Fail-open, and it needs hook trust: either pre-recorded hash trust or `--dangerously-bypass-hook-trust`. Back it with execpolicy rules in `$CODEX_HOME/rules/` that `forbidden` the classified commands (`git push`, `git merge`, `tea`, `docker push`, `npm publish`); those are enforced before exec, by argv prefix | +| Sandbox | `sandbox_mode = "workspace-write"` set explicitly; `sandbox_workspace_write.network_access` from the role's network class; a named profile with `[permissions..filesystem]."" = "deny"` [cx:D:permissions]. These docs may describe a newer release than 0.160.0, so a probe must confirm it; the admin form is requirements `permissions.filesystem.deny_read`. Hooks and MCP servers run outside it | +| Env | `shell_environment_policy`: `inherit = "core"` (or `include_only`) and `ignore_default_excludes = false` | +| MCP | `[mcp_servers.mosaic]` with `required = true` and `enabled_tools` | +| Credentials | `CODEX_API_KEY` (exec and app-server only) or `cli_auth_credentials_store = "ephemeral"`. Role credentials stay in the broker | +| Headless | `codex exec --json` (exit 0/1, approval forced to `never`), or app-server JSON-RPC, where the host answers escalation approvals in-band | +| Lock | Only `/etc/codex/requirements.toml` can stop a flag such as `--dangerously-bypass-approvals-and-sandbox`. Inferred: since the launcher owns the argv, the agent can't add flags, so slice 1 doesn't need the lock | + +### 7.6 Changes to `adapters/` + +- **The adapter contract needs a version bump.** + - New input: `MOSAIC_LAUNCH_DIR`, which points at the generated, read-only + bundle (prompt, policy, settings, skills, MCP config, manifest). + - The rule "adapters never read configuration files" needs restating: the + adapter reads only the generated bundle, never the system or business + config. + - The undocumented `MOSAIC_TOOLS` and `MOSAIC_SKILLS` inputs move into + the bundle. +- **`SUPPORTED_ADAPTERS`** gains `claude` and `codex` only when their + adapters exist and pass the suites. +- **Two kinds of agent.** Darkwing 6.5 separates role agents (interactive, + with a role binding and git) from workers (no git, no credentials). The + generator serves both, but the worker path must keep its invariant: no + `git.*` and no credential-bearing typed tools. + +--- + +## 8. Points for Sage, each with a recommendation + +1. **Decision 31 against a Pi gate extension.** Pi's only fail-closed hook + is an extension, and decision 31 bars explicit extensions in + Console-bound seats until CHAT-06. + - Recommendation: the gate extension enters through the CHAT-06 path. It + should be a single file with no imports outside its pinned tree, + digest-checked at launch. + - Until then, Pi role seats get no hook, and the hard lines are the + credential scope, the verbs and `--tools`. Nothing that matters is lost, + because the hook was never the boundary. +2. **Brief wording.** The slice 1 brief should name the hard layers + (credential scope, verbs, tool ceiling, container) and call hooks early + refusal plus logging. This agrees with Darkwing 1.4, item 3. +3. **Machine-wide locks are out of scope.** `/etc/claude-code` and + `/etc/codex/requirements.toml` would also govern Jason's own sessions. + That makes them a policy change, which is gated. Recommendation: don't + emit them in slice 1. Flags set by the launcher are enough, because the + agent can't change its own argv. + - One caveat: a same-user agent can edit its generated config files + before a resume or reload. Claude notices settings changes during a + session (it has a `ConfigChange` event for user, project, local and + policy settings, [cc:hooks]). How a `--settings` file is treated when it + changes is unverified. Pi reads the agent dir again on `/reload`. A read-only mode on the + launch dir doesn't stop the same user from changing it back. Only a + container or a separate OS user closes that. +4. **Host-seat trust (`--approve`).** It's latent, not live. + Recommendation: + - the generator emits `--no-approve` and `--system-prompt`; + - for `scripts/agent-host-dev.sh`, record a bounded follow-up (it isn't + my file to change). +5. **Claude credentials for isolated seats.** A private `CLAUDE_CONFIG_DIR` + loses the shared subscription login. The options are a per-role + `claude setup-token` OAuth token (credential minting, gated) or an API + key (spend, gated). Either way it's Jason's decision, so it belongs with + the per-role credentials step (step 7) and shouldn't be decided here. +6. **Provider keys visible in workers.** This exists today and isn't new to + slice 1, but slice 1 role tokens must not travel the same way. + - The container passes `ZAI_API_KEY` and `ANTHROPIC_API_KEY` in the + environment, and Pi's `bash` inherits it. + - Pi's `read` can open the mounted `auth.json`. + + Recommendation: role credentials stay in the broker process (Darkwing + 6.3) and never enter the agent's environment or filesystem. +7. **Order.** As the direction doc says: Pi first, with the generator, the + bundle and a gate wrapper that also works as a plain verb-side check; + then Claude; then Codex. Codex is entirely new work with the weakest + hooks, and app-server approvals cover escalations only. + +--- + +## 9. Verification + +**Checked myself, against source or fetched docs:** +- **Pi `tool_call` block and fail-closed paths:** + - `runner.js:745-763`; + - `agent-session.js:230-243`, which wraps a non-Error throw as "Extension + failed, blocking execution"; + - `agent-loop.js:400-458`; + - `docs/extensions.md:2924-2925`; + - the same strings and `isAllowedTool` are present in the shipped bundle + chunk. +- **Pi `--no-extensions`:** loads only the CLI `-e` set + (`resource-loader.js:409-411`). +- **Pi `--approve`:** means "Trust project-local files for this run" + (`README.md:615`, `args.js:205, 307`). +- **Pi `bash` environment:** spreads `process.env` (`utils/shell.js:117-123`). +- **Pi allowlist:** `isAllowedTool` filters (`agent-session.js:2105-2160`). +- **Codex PreToolUse:** spawn error or timeout sets `Failed` and doesn't + block; any other exit code sets `Failed` + (`hooks/src/events/pre_tool_use.rs:205-211, 278-286`). +- **Codex `ignore_default_excludes`:** defaults to true + (`config/src/shell_environment_policy.rs:136`). +- **Claude command hooks:** "a mistyped path … leaves the gate silently + disabled" (fetched hooks page). +- **Claude SDK callbacks:** a PreToolUse timeout means the tool isn't run + (fetched `agent-sdk/hooks` page, "Hook timeout"). +- **Repository line references** in section 6. + +**Not checked (subagent findings I didn't re-read):** +- **Not exercised at all.** Nothing was run against a live harness: no hook + probes and no canary runs. The hidden Claude flags + (`--system-prompt-file`, `--max-turns`) were not exercised. +- **Still inferred.** These remain marked as inferred in the text: + - the Codex project-skills trust gap; + - Codex's readable `auth.json`; + - the Pi json-mode exit code. +- **Not established.** What happens when a Claude SDK callback hook throws. + +Before the build brief relies on a block, a probe matrix should run each +harness at its pinned version, with these cases: +- a gate that blocks; +- a gate that crashes; +- a gate at a missing path; +- a gate that times out; +- a `bash` route to the same action. + +**A note on the research workspace.** The Codex and Claude subagents briefly +shared one docs scratch directory, and some Claude doc files may have been +overwritten with Codex pages. The Claude agent refetched into its own +directory before writing. I spot-checked the two Claude claims that matter +most in its own copy, and both are Claude pages. diff --git a/docs/SESSIONS.md b/docs/SESSIONS.md index c4452568..441181a2 100644 --- a/docs/SESSIONS.md +++ b/docs/SESSIONS.md @@ -469,3 +469,5 @@ are never rewritten or removed; corrections are new entries. 2026-10-04T19:12:27Z | Filbert (T3 Claude Code, thread 9cb9731e) | meta-harness survey started (slice 1 step 8, Sage ASSIGNMENT) | research only in agents/filbert/work/; no commits; row 5 round 2 takes priority 2026-10-04T19:12:38Z | Sage (T3 Claude Code, thread 1ef1e4f8) | PRDY round 1, seat launches (decision 45) | PRD draft 0.1 at docs/prd/mosaic-stack.md; launched Researcher (T3 Sonnet 5.5, thread 9bac0794) for the Vikunja and Pocket ID report; assigned Darkwing the slice 1 data model note and Filbert the meta-harness survey (both T3 Opus 5.5, existing threads); Dewey on the row 5 rework; running under the new limits: Opus 3 of 4 (dewey, darkwing, filbert), Sonnet 1 of 4 (researcher) 2026-10-04T19:19:14Z | Sage (T3 Claude Code, thread 1ef1e4f8) | slice 1 data model received | Darkwing's note (948b94ce) and prototype committed as records; lead decision 46 accepts 6.1-6.5, 6.7, 6.8 and sends 6.6 (PM launching sessions) to Jason in PRDY round 3 +2026-10-04T19:26:12Z | Filbert (T3 Claude Code, thread 9cb9731e) | meta-harness survey done (slice 1 step 8) | agents/filbert/work/meta-harness-survey-2026-10-04.md sha256 05f83807…0989; Pi tool_call is the only fail-closed hook of the three, so hard lines are credential scope, verbs, tool ceiling and container; no commits +2026-10-04T19:26:50Z | Sage (T3 Claude Code, thread 1ef1e4f8) | meta-harness survey received | Filbert's survey (05f83807) committed as a record; lead decision 47 rules on its section 8; two DEFERRED items (host-seat --approve, worker provider-key exposure) diff --git a/docs/plans/2026-09-26_lead-decisions.md b/docs/plans/2026-09-26_lead-decisions.md index 7cc8d672..3f71f1ff 100644 --- a/docs/plans/2026-09-26_lead-decisions.md +++ b/docs/plans/2026-09-26_lead-decisions.md @@ -729,3 +729,27 @@ which stay with him. Each item names who decided it and what happened. enough: `INSERT OR REPLACE` overwrote a row in the prototype despite UPDATE and DELETE triggers. Each table also needs a BEFORE INSERT guard, which the prototype verified. +47. **Sage's rulings on the meta-harness survey (2026-10-04).** Source: + Filbert's `agents/filbert/work/meta-harness-survey-2026-10-04.md` + (sha256 05f83807…), section 8. The survey's finding: no harness's hook + is a hard block on its own. Pi's `tool_call` hook fails closed. Claude + Code's command hooks and Codex's hooks fail open. Nothing was run + against a live harness. The survey's section 9 has a probe matrix that + must pass before any build brief relies on a block. + 1. Pi gate extension: accepted. It enters through the CHAT-06 path + (decision 31) as one digest-checked file. Until then Pi role seats + get no hook. + 2. Brief wording: accepted. The hard layers are credential scope, the + verbs refusing without a resolved decision, the tool ceiling and a + container. Hooks are early refusal plus logging. + 3. No machine-wide `/etc` locks in slice 1. They would govern Jason's + own sessions, which makes them a gated policy change. + 4. Host-seat `--approve`: the generator emits `--no-approve` and + `--system-prompt`. A bounded follow-up for + `scripts/agent-host-dev.sh` goes in DEFERRED. + 5. Claude credentials for isolated seats: a per-role setup token or an + API key. Both are gated, so this goes to Jason at slice 1 step 7. + 6. Role credentials stay in the broker and never enter an agent's + environment or files. The existing exposure of worker provider keys + goes in DEFERRED. + 7. Build order: Pi, then Claude Code, then Codex. diff --git a/docs/plans/DEFERRED.md b/docs/plans/DEFERRED.md index 03100a32..32f2a793 100644 --- a/docs/plans/DEFERRED.md +++ b/docs/plans/DEFERRED.md @@ -174,6 +174,17 @@ at every gate. Started 2026-09-12 during the control board MVP. #1507 (comment 26685). Fix: have `record` check the comment's issue and author the way `resolve` does, or refuse a comment id it can't verify. Found by Filbert. (2026-10-04, #1508) +- **Host seats launch Pi with `--approve`.** A future `.pi/SYSTEM.md` or + `.pi/settings.json` in the checkout would load in every host seat. This + is latent, since neither file exists today. Fix in + `scripts/agent-host-dev.sh`: pass `--no-approve` and an explicit system + prompt. Filbert, meta-harness survey section 8.4. (2026-10-04) +- **Workers can read provider keys.** The worker container passes + `ZAI_API_KEY` and `ANTHROPIC_API_KEY` in the environment, which Pi's + `bash` inherits, and Pi's `read` can open the mounted `auth.json`. + Slice 1 role tokens stay in the broker (lead decision 47). The workers' + own exposure needs a separate piece. Filbert, survey section 8.6. + (2026-10-04) ## Queue