1pub const PI_CORE_TOML: &str = r#"# built-in preset: pi-core — the §1 core with pi's exact defaults, plus pi's four kept extras.
37schema_version = 1
38
39[core]
40effort = "medium" # pi default thinking level (pi§3, src:core/defaults.ts:3)
41max_tool_output_bytes = 51200 # pi's shared truncation policy: 50KB / 2000 lines (pi§1, truncate.ts)
42project_context = true # AGENTS.md/CLAUDE.md global + ancestor walk (pi§2 "Context files")
43env_context = true # pi appends Current date + cwd to the prompt (pi§2, system-prompt.ts:88-170)
44
45[core.retry] # pi agent-level auto-retry (pi§3)
46enabled = true
47max_retries = 3
48base_delay_ms = 2000
49
50[core.tools]
51enabled = ["read_file", "bash", "edit_file", "write_file"] # pi's default-ACTIVE four (pi§1, src:core/sdk.ts:245)
52schema_tier = "full"
53[core.tools.read_file]
54multimodal = true # pi read returns images as attachments (pi§1, read.ts)
55
56[core.skills]
57enabled = true # agentskills.io discovery + progressive disclosure (pi§2; D-7 met by read_file)
58 # No `harness` here: `supercode-default` extends this preset and is DEFINED as
59 # today's unfiltered default stack (§4 intro), which discovers no SKILL.md
60 # package and offers no `skill` tool. Naming a root table here would change what
61 # `supercode` with no config file at all does.
62
63[core.compaction]
64enabled = true
65after_messages = 0 # pi's trigger is token pressure, never message count (pi§2)
66reserve_tokens = 16384 # compaction.reserveTokens default (pi§2, pi§6)
67keep_recent_tokens = 20000 # compaction.keepRecentTokens default (pi§2)
68summarize = true # structured Goal/Constraints/Progress/… summary (pi§2, compaction.md)
69
70[core.steering]
71steering_mode = "one-at-a-time" # pi delivery-mode defaults (pi§3 "Message queue", pi§6)
72follow_up_mode = "one-at-a-time"
73
74# ---- modules ON (each is on pi's kept-list, pi§10 closing) ----
75[capabilities.trust]
76enabled = true
77default = "ask"
78# pi's ONE built-in gate (pi§4, trust-manager.ts; defaultProjectTrust "ask")
79[capabilities.session_tree]
80enabled = true
81branch_summaries = true
82labels = true
83# THE core pi feature (pi§10; D5)
84[capabilities.session_share]
85enabled = true
86# /share gist public link (pi§8); human /export HTML is core now (§1.6, S6) and on regardless
87[capabilities.server]
88enabled = true
89# --mode json / --mode rpc embedding ladder (pi§8, pi§10)
90[capabilities.plugins]
91enabled = true
92# everything-is-an-extension (pi§7; D-10 dep satisfied by trust above)
93[capabilities.tui]
94enabled = true
95# §1.9 recorded deviation, default-on in parity presets
96
97# ---- notable OFFs (each a pi FIRST-PARTY omission, pi§10 / catalog §3) ----
98[capabilities.tools_search]
99enabled = false
100# grep/find/ls exist but are OPT-IN even in pi (pi§1 "--tools"); one line re-enables
101[capabilities.mcp]
102enabled = false
103# "intentionally does not include built-in MCP" (pi§7)
104[capabilities.subagents]
105enabled = false
106# example extension only (pi§3 "NO subagents")
107[capabilities.permissions]
108enabled = false
109approval = "never"
110sandbox = "danger_full_access"
111 # pi has NO popups/rules/sandbox (pi§4; README "Permissions & containerization").
112 # C3 fires its MANDATORY warning here BY DESIGN — pi's own docs say containers, not trust in the harness.
113[capabilities.plan_mode]
114enabled = false
115# example ext only (pi§10)
116[capabilities.todos]
117enabled = false
118# example ext only (pi§10)
119[capabilities.tools_background]
120enabled = false
121# "tmux instead" (pi§10)
122[capabilities.tools_web]
123enabled = false
124# web search ships as a SKILL in pi (pi§10)
125[capabilities.checkpoint]
126enabled = false
127# git-checkpoint example ext only (pi§5)
128[capabilities.memory]
129enabled = false
130# no memory subsystem (pi§2)
131[capabilities.hooks]
132enabled = false
133# pi's "hooks" are code extensions, not config-registered commands
134[capabilities.deferred_tools]
135enabled = false
136[capabilities.cache]
137enabled = false
138[capabilities.reduction]
139enabled = false
140# supercode-only OPTIONAL policies off; A7 truncation + rehydrate stay always-on core regardless (§1.13, S1/S7 — no longer a D-8/D-8-error risk)
141[capabilities.model_catalog]
142enabled = false
143[capabilities.model_oauth]
144enabled = false
145# pi HAS /login OAuth (pi§9) — deferred module 27, recorded gap
146"#;
147
148pub const CC_PARITY_TOML: &str = r#"# built-in preset: cc-parity — Claude Code's default surface, composed.
150schema_version = 1
151
152[core]
153model = "anthropic/claude-opus-4-8" # CC account-default Opus 4.8 (cc§9 "Account-type defaults")
154effort = "medium"
155env_context = true # CC startup context: cwd/git status (cc§2 "Startup context"); BP-4 adds the approval/sandbox policy line the catalog:90 semantics name, re-emitted per turn on change
156context_injections = true # BP-4 (catalog:91): CC splices ambient reminder blocks into every session (hook `additionalContext`, `<system-reminder>` blocks, cc§2) — arms `crate::context_injection`'s built-in blocks + the runtime splice seam
157project_context = true # CLAUDE.md tiers + directory walk (cc§2); global tier included (§1.4)
158nested_instructions = true # S6/S12 home: subdir CLAUDE.md loaded on demand, CC default (catalog:84) — closes a gap-ledger row
159instruction_imports = true # S6/S12 home: `@path` imports, depth 4, CC default (catalog:85) — closes a gap-ledger row
160project_root_markers = [".git"] # BP-4: bounds the ANCESTOR walk (catalog:81) — CC walks UP from cwd and concatenates root→cwd, closest read last (cc§2 "Directory-walk loading")
161project_doc_max_bytes = 0 # BP-4 (catalog:87): CC documents NO byte cap on CLAUDE.md — its hygiene levers are the two below — and §3.1 spells "uncapped" as 0. Stated explicitly rather than left absent so the preset says what CC does instead of leaving it to a default.
162project_doc_excludes = [] # BP-4 (catalog:87): CC's `claudeMdExcludes` (cc§2) — glob/absolute-path list of CLAUDE.md files to skip; CC ships it EMPTY, same as the rule sets below
163project_doc_strip_comments = true # BP-4 (catalog:87): CC strips block-level `<!-- … -->` from CLAUDE.md before injection so maintainer notes cost no tokens (cc§2 "HTML comment stripping")
164parallel_tool_calls = true # BP-2: CC runs an assistant turn's independent sibling calls as a concurrent batch (cc§1, catalog:59) — the gated batch path in `Agent::run_tools_concurrently`, armed
165tool_output_spill = true # BP-2: CC's Bash-overflow → session file recovery door (catalog:58): a capped tool result keeps its full bytes in a per-session spill file the notice names, readable with `read_file` — no `capabilities.reduction` required
166file_mentions = true # BP-5 (catalog D2 "@-file mentions / attachments"): `@` in the prompt injects that file's context (cc§2 "`@`-file mentions"). Deny-rule aware, as CC documents it — a mention resolving to a protected path (see `permissions.protected_paths` below) inlines the refusal, never the bytes.
167output_style = "default" # BP-5 (catalog D2 "Output style / personality module"): CC's `outputStyle` setting at the value CC itself ships — the Default style, which appends nothing to the base prompt (cc§7 "Output styles"). The LAYER is what this arms: naming any other style (`explanatory`, `learning`, or a `~/.claude/output-styles/<name>.md` of the user's own) swaps the response-style instructions without touching `system_prompt`.
168path_rules = true # BP-5 (catalog D2 "Path-scoped rules"): CC's `.claude/rules/*.md` (cc§2). A rule with no `paths:` joins the instruction blob at startup; a rule WITH `paths:` waits and is injected the first time a tool touches a matching file — CC's own "loads only when Claude touches matching files".
169
170[core.model_switch]
171allow_switch = true # BP-13 (catalog D9 "Mid-session model switching"): CC's `/model` + Alt+P change the model without losing the session (cc§9). Arms the governed switch — reasoning-artifact filtering (dep 8) plus a persisted `model_change` record in the session journal.
172 # `notice` is deliberately NOT set: CC switches SILENTLY (no injected switch instructions); that is Codex's behaviour, and cx-parity sets it there.
173
174[core.tools]
175# BP-3: `current_time`/`sleep` join the optional default-tool names (the
176# `view_image` precedent, §1.2's "fifth optional default-tool name, not a new
177# module"). The catalog's clock/sleep row is `✓*` for CC — ScheduleWakeup
178# paces the loop but neither reports the time nor pauses it — so cc-parity
179# supplies the capability itself rather than the footnote.
180enabled = ["read_file", "bash", "edit_file", "write_file", "current_time", "sleep"]
181schema_tier = "full"
182[core.tools.read_file]
183multimodal = true # CC Read renders images/PDFs/notebooks (cc§1 Read)
184line_numbers = true # BP-2: CC Read's `cat -n` gutter, numbered from `offset` (cc§1 Read, catalog:26)
185[core.tools.edit_file]
186require_read_before_edit = true # S6/S12 home: CC Edit refuses unless the file was read this conversation (catalog:32) — closes the cc-parity gap-ledger row
187notebook_aware = true # S6/S12 home: NotebookEdit cell-level replace/insert/delete (catalog:40) — closes the cc-parity gap-ledger row
188[core.tools.bash]
189timeout_secs = 120 # CC default 2 min, model-raisable (cc§1 Bash)
190
191[core.skills]
192enabled = true # SKILL.md dirs + commands, descriptions-only until invoked (cc§7 Skills)
193harness = "claude-code" # BP-6: the loop discovers SKILL.md from CC's own documented roots and precedence — enterprise/managed > `~/.claude/skills` > plugin bundles (`plugin:skill`) > project `.claude/skills`, plus nested subdirectory skills as `dir:skill` (cc§7 "Skill locations & precedence")
194shell_injection = true # BP-5 (catalog D2 "Shell-output injection in templates/skills"): CC executes `` !`cmd` `` inline and ```` ```! ```` blocks inside a skill/command body AT LOAD TIME (cc§7 "Dynamic context injection"), disabled org-wide with `disableSkillShellExecution` — this key is that switch stated positively. Every extracted command is decided by the one permissions engine with THIS preset's rules and protected paths; under `approval = "untrusted"` a bare `bash` is `Ask`, so a body that wants its own command run declares it in `allowed-tools`, exactly as CC requires.
195
196[core.compaction]
197enabled = true
198summarize = true # CC auto-compaction near limit + /compact [instructions] (cc§2). BP-4: this key now arms the CORE span-summary side-call too — design §1.5 makes "an LLM summary of the compacted span" part of obligation 5, and §3.1 annotates this very key "SpanSummary side-call … D-9 small-model fallback"
199reserve_tokens = 16384 # CC's threshold is pct-based (CLAUDE_CODE_AUTOCOMPACT_PCT_OVERRIDE, cc§2); reserve is our §1.5 equivalent
200focus_instructions = "" # BP-4 (catalog:98): CC has NO standing compaction focus — steering is per invocation, `/compact [instructions]` (cc§2). Stated empty so the preset says that, rather than leaving the knob to a default, and so the mechanism is armed at the one place it belongs: `Agent::compact_now(focus)`.
201
202[core.session]
203append_only = true # BP-8 (catalog:150): CC flushes every event to its session JSONL as the turn runs (cc§5), so a crash mid-turn keeps the turn. Arms `crate::session_journal` — supercode's own store otherwise rewrites `<name>.jsonl` only at end-of-turn.
204queue_persist = true # BP-8 (catalog:154): CC writes QUEUE-OPERATION records for prompts typed while it is busy (cc§5), so a pending input survives a restart. The journal armed above is where those records go.
205auto_title = true # BP-7 (catalog:150): CC writes `ai-title` records — a cheap-model title for every session (cc§5). The titler, its small-model routing and its `SessionStore::set_title` write were all built and CLI-wired at P4b; this preset never set the gate, so under cc-parity no title was ever generated. `small_model` is pinned below (`capabilities.model_catalog`), so this runs on Haiku, not Opus.
206
207[core.prompts]
208# BP-7 (catalog §4a "Review mode"): CC's `/code-review` + `/security-review` are
209# purpose-built review TURNS with a fixed report format, not a separate agent
210# (cc§10). This template IS that format; `Agent::review` sends it as an ordinary
211# turn of this same session, so the review inherits the session's tools,
212# permissions, transcript and records.
213code-review = """Review the current code changes as a dedicated review turn. {args}
214Inspect the diff and the files it touches with the available tools before judging anything.
215Report in exactly these sections, omitting a section only when it is genuinely empty:
2161. Correctness — defects, in severity order (blocker / major / minor), each with file:line and the failing case.
2172. Security — untrusted input reaching a trust boundary, secrets, injection, permission widening.
2183. Reuse and simplification — existing code the change should have used; code the change makes dead.
2194. Verdict — one line: SHIP, SHIP WITH FIXES, or DO NOT SHIP, and why."""
220
221# ---- modules ON ----
222[capabilities.tools_search]
223enabled = true
224glob = true
225content_search = true
226list_dir = false
227 # Glob + Grep, promptless read-class (cc§1); list_dir OFF (S15 fix) — CC lists dirs via Bash/Glob, Read rejects directories (catalog:33 fn²), so the module's dir-listing sub-tool would be a capability CC users never see
228[capabilities.tools_web]
229enabled = true
230fetch = true
231search = true
232# WebFetch + WebSearch (cc§1)
233[capabilities.tools_question]
234enabled = true
235# AskUserQuestion (cc§1)
236[capabilities.todos]
237enabled = true
238persist = true
239goals = true
240# Task*/TodoWrite checklist, persists across compaction (cc§1, cc§3)
241# BP-7 (catalog:138): `goals` is this module's persistent-objective variant (design §2 module 7) — CC's `/goal`, a standing condition restated at the tail of every request and persisted as `<session>.goal.json`, distinct from the per-stretch `update_plan` checklist above.
242[capabilities.plan_mode]
243enabled = true
244effort = "high" # BP-13 (catalog D9, the plan-mode half of "Reasoning effort / thinking budgets"): planning is the phase that most rewards deeper reasoning, and CC's plan mode is where a session does its thinking (cc§3). While the mode is live the request carries this level instead of `[core] effort`; leaving the mode restores it.
245# Shift+Tab / EnterPlanMode read-only mode (cc§3); dep met by permissions.rules below
246[capabilities.subagents]
247enabled = true
248max_depth = 2
249background = true
250background_prompts = "parent"
251 # Agent tool; background-by-default v2.1.198+, nested allowed (cc§1, cc§3)
252 # C6 resolved via the schema key (S2 fix, not prose): background_prompts = "parent" — background children surface prompts in the parent session (cc§3, claude-code.md:98)
253[capabilities.tools_background]
254enabled = true
255# run_in_background + Ctrl+B (cc§1, cc§8); C6: same parent-surfaced queue as subagents.background_prompts above
256
257[capabilities.permissions]
258enabled = true
259approval = "untrusted" # CC tiered default: read-only never prompts, Bash/edits prompt first-use (cc§4 "Tiered defaults")
260 # No C3: approval != never.
261auto_approved_tools = ["read_file", "glob", "search", "ask_user", "current_time", "sleep", "enter_plan_mode", "exit_plan_mode", "update_plan"]
262 # CC read-only tier (cc§4); `list_dir` removed (S15 — module tool is off above)
263 # BP-3: the new no-side-effect tools join that tier — CC never prompts before
264 # AskUserQuestion/EnterPlanMode/ExitPlanMode, and under `approval = "untrusted"`
265 # every tool NOT listed here asks first, which would put a permission prompt in
266 # front of the question prompt (and would refuse both outright in a headless run).
267 # `exit_plan_mode` carries its own explicit plan approval, so the generic gate in
268 # front of it is pure double-prompting.
269 # BP-8 (catalog:156): `update_plan` joins the same tier — CC's TodoWrite is a
270 # checklist write with no side effect outside the session and never prompts
271 # (cc§1/cc§3). Under `approval = "untrusted"` it otherwise asks on every plan
272 # update, which in a headless run refuses the plan outright — i.e. the plan row's
273 # own behaviour would be unreachable under this preset.
274# module 12 in table form (S5): OS sandbox OFF (CC's `/sandbox` is opt-in, cc§4), fs tier unconfined.
275# One key `permissions.sandbox` — the table form, not the bare-scalar shorthand, so no collision.
276[capabilities.permissions.sandbox]
277enabled = false
278tier = "danger_full_access"
279env_policy = "inherit" # BP-10 (catalog "Child-process env sanitization", cc `✓*`): CC's env control is `sandbox.credentials.envVars` — an OPT-IN of the opt-in `/sandbox` (cc§4). With the OS sandbox off above, a CC Bash call inherits the user's environment, so the preset SAYS "inherit" rather than leaving the knob to a default that happens to agree; cx-parity states the other posture on the same key.
280escalation = "ask" # BP-10 (catalog "Sandbox-escalation path", cc `✓*` allowUnsandboxedCommands): with `enabled = false` nothing is confined, so this never fires under cc-parity today — it is the answer for the `/sandbox` session, where CC asks before running a command outside the sandbox rather than refusing it outright.
281[capabilities.permissions.sandbox.network]
282enabled = false # BP-10: CC's own default. `/sandbox` is opt-in and its network proxy with it (cc§4 `sandbox.network.*`); stated so the preset carries CC's posture on the key instead of defaulting to it silently.
283[capabilities.permissions.rules]
284enabled = true # deny→ask→allow first-match IS the CC algebra — native, no translation (C5 decision; cc§4 "Rule sets & evaluation")
285deny = []
286ask = []
287allow = [] # CC ships empty rule sets; "don't ask again" persists into allow at runtime (cc§4)
288[capabilities.permissions.protected_paths]
289enabled = true # never-auto-approved set (cc§4 "Protected paths")
290# Rule-layer floor: file-tools + bash redirect targets + apply_patch + known
291# argv-writers (tee/dd/cp/mv/install/sed -i/truncate/ln); an opaque or
292# dynamic bash write is forced to Ask. Complete OS-level write confinement
293# is `permissions.sandbox`'s job (module 10), not this table's — see
294# `crate::permissions` module doc / `Config::permissions_protected_paths`.
295paths = [".git/**", ".env*", ".claude/**", ".vscode/**", ".idea/**", "~/.claude/settings*"]
296[capabilities.permissions.approvals]
297persist = true # BP-10 (catalog "Session approval caching"): CC records "don't ask again" PER PROJECT + command, not per process (cc§4) — the grant is still there tomorrow. Stored beside the session's other per-project records ($SUPERCODE_HOME/approvals/<project tag>.json) and reversible: delete it (or `ApprovalCache::clear`) and the next matching call asks again.
298
299[capabilities.trust]
300enabled = true
301default = "ask"
302# workspace trust gates project allow-rules (cc§4)
303[capabilities.mcp]
304enabled = true
305# stdio/HTTP/OAuth, resources, prompts-as-commands (cc§7)
306[capabilities.deferred_tools]
307enabled = true
308core = ["read_file", "bash", "edit_file", "write_file", "glob", "search", "update_plan", "ask_user", "enter_plan_mode", "exit_plan_mode", "current_time", "sleep"]
309 # CC defers MCP tool definitions BY DEFAULT behind ToolSearch (cc§7 "Tool search"); builtins stay eager
310 # BP-3: the new built-ins are eager for the same reason the older ones are — CC advertises AskUserQuestion/EnterPlanMode/ExitPlanMode up front, and a question tool the model must first tool_search for is not the same capability
311[capabilities.hooks]
312enabled = true
313# config-registered lifecycle hooks (cc§7: 30 events; module ships the CC-compatible subset first)
314[capabilities.memory]
315enabled = true
316# auto memory MEMORY.md + topic files (cc§2); D-9 dep → model_catalog below
317[capabilities.checkpoint]
318enabled = true
319# per-prompt file-history-snapshot → /rewind (cc§5)
320[capabilities.session_tree]
321enabled = true
322branch_summaries = false
323labels = false
324 # CC has the tree DATA MODEL (uuid/parentUuid, cc§5) + /rewind; summaries/labels are pi-isms
325[capabilities.model_catalog]
326enabled = true
327small_model = "anthropic/claude-haiku-4-5"
328fallback = [] # CC ships NO fallback model; `--fallback-model` (≤3) is per-invocation (cc§9). The chain is EXECUTED by the loop when one is given — see `Agent::complete_with_fallback`.
329provider = "anthropic" # BP-13: which provider's alias scope is in force — CC's friendly names resolve per provider/account (cc§9)
330account = "max" # BP-13 (cc§9 "Account-type defaults"): on Max, `default`/`best` mean Opus; another plan's scope would mean something else
331service_tier = "auto" # BP-13 (catalog D9 "Fast mode / service tiers"): CC's standard tier; `/fast` moves the session to `priority` and warns about the cache churn (cc§9)
332allowed_models = [] # BP-13 (catalog D9 "Org model allowlists"): CC ships availableModels EMPTY — unrestricted. Stated so the preset says CC's posture rather than leaving it defaulted.
333denied_models = []
334 # aliases + ANTHROPIC_SMALL_FAST_MODEL + fallback chains (cc§9)
335[capabilities.model_catalog.aliases]
336"*[1m]" = "{}[1m]" # BP-13: CC's 1M-context SUFFIX form (cc§9 `sonnet[1m]`) — a pattern alias whose captured stem is itself alias-resolved, so `sonnet[1m]` lands on `anthropic/claude-sonnet-4-6[1m]`
337[capabilities.model_catalog.providers.anthropic.aliases]
338fast = "anthropic/claude-haiku-4-5" # BP-13: provider-scoped — `fast` means Haiku only while this session talks to Anthropic
339[capabilities.model_catalog.providers.anthropic.accounts.max.aliases]
340default = "anthropic/claude-opus-4-8" # BP-13 (cc§9): the Max plan's account defaults
341best = "anthropic/claude-opus-4-8"
342[capabilities.model_catalog.models."anthropic/claude-haiku-4-5"]
343max_effort = "low" # BP-13: the per-model effort TIER — the small/fast model is not asked for deep reasoning, whatever `[core] effort` says
344[capabilities.tui]
345enabled = true
346
347# ---- notable OFFs ----
348[capabilities.tools_apply_patch]
349enabled = false
350# CC is edit-only (C1; catalog §5 conflict 1)
351[capabilities.tools_persistent_shell]
352enabled = false
353[capabilities.lsp]
354enabled = false
355# CC's LSP is inactive until a plugin installs it (cc§1) — off matches default
356[capabilities.formatters]
357enabled = false
358[capabilities.session_share]
359enabled = false
360# no PUBLIC share links in CC (D5 OC+PI-only row); `/export`+`/copy` are core now (§1.6 `export_format`, S6) and stay on regardless
361[capabilities.server]
362enabled = false
363# CC has no local HTTP server surface; SDK is in-process
364[capabilities.reduction]
365enabled = false
366[capabilities.cache]
367enabled = true
368plan = "imported_prefix"
369warnings = true
370# BP-4 deviation from §4.2's own `enabled = false` line, recorded here rather than silently:
371# that line's reason ("CC caching is provider-automatic") does not survive the catalog's own
372# grading of the same behavior. catalog:110 "Cache-aware context architecture" marks CC ✓ with
373# the cache-action matrix, cache-preserving `/cd` and TTL switches (cc§2) — i.e. CC deliberately
374# SHAPES the cached prefix and warns when an action would churn it, which is a harness behavior,
375# not a provider one (Anthropic prompt caching is driven by explicit breakpoints, and something
376# has to place them). supercode's equivalent is exactly `CachePlan::ImportedPrefix` + the
377# imported-prefix compaction clamp + `AgentEvent::CacheWarning`, all already implemented and
378# wired — with the module off they simply never fired under this preset, which is the gap the
379# ledger row named. `warnings = true` is C2's referee, and now reaches `Config::cache_warnings`.
380[capabilities.structured_output]
381enabled = false
382# --json-schema is headless-only surface; enable per-run
383[capabilities.model_oauth]
384enabled = false
385# recorded gap: CC's DEFAULT auth is subscription OAuth (cc§9) — module 27 deferred
386"#;
387
388pub const CX_PARITY_TOML: &str = r#"# built-in preset: cx-parity — Codex's default surface, composed.
390schema_version = 1
391
392[core]
393effort = "medium" # model_reasoning_effort default tier (cx§6, cx§9)
394env_context = true # <environment_context> block: cwd/sandbox/approval (cx§2) — BP-4 supplies the approval/sandbox line this comment already claims, and re-emits the block on change (cx§2 "re-emitted on change")
395context_injections = true # BP-4 (catalog:91): Codex's whole `context/` library (~25 block types) is spliced at assembly time (cx§2) — arms `crate::context_injection`'s built-in blocks + the runtime splice seam
396project_context = true # AGENTS.md hierarchy, root-down concat, 32KiB cap (cx§2)
397project_root_markers = [".git"] # BP-4: cx's own `project_root_markers` default (cx§6 config census) — the git root the AGENTS.md walk climbs to before descending root→cwd (cx§2)
398project_doc_max_bytes = 32768 # BP-4 (catalog:87): cx's documented `project_doc_max_bytes` default, 32 KiB (cx§2, cx§6 "project_doc_max_bytes (default 32768)") — the cap the `project_context` line above already claims but nothing enforced
399project_doc_excludes = [] # BP-4: Codex has no exclude list (that is CC's `claudeMdExcludes`); stated empty so the preset's hygiene posture is complete rather than defaulted
400project_doc_strip_comments = false # BP-4: Codex strips nothing from AGENTS.md — the HTML-comment strip is CC-only (catalog:87)
401# P2 placement fix: §4.3's own TOML block places `shell_env_snapshot` under
402# `[core.tools]`, but §3.1's schema (the "annotated, exhaustive" canonical
403# definition, line ~598) defines it as a direct `[core]` scalar, not a
404# `core.tools.*` key — `CoreToolsConfig` has no such field, so a literal
405# under-`[core.tools]` placement would silently parse-and-drop it. Moved
406# here to match §3.1 (the schema doc doesn't have this key twice with two
407# different homes; §4.3 is corrected to agree with it).
408shell_env_snapshot = true # S6/S12 home: shell-env snapshotting, cx stable-on feature (catalog:338) — closes a gap-ledger row
409parallel_tool_calls = true # BP-2: Codex runs sibling tool calls concurrently behind its RwLock gate (cx§1, catalog:59) — the same batch path, armed
410tool_output_spill = true # BP-2: Codex token-caps a tool result with no spill file of its own (catalog:58 `✓*`); supercode's capped result names a per-session spill file the model reads back with `cat` (cx's own read pathway), so the truncation is recoverable rather than lossy
411file_mentions = true # BP-5 (catalog D2 "@-file mentions / attachments"): Codex's `@`-mention popup and `/mention` insert a path into the prompt (cx§2 "`@`-mentions (files)"); the path then has to become CONTEXT, which is what this key does. Same permissions-engine read check the cc side gets — cx's `.codex/**` protected paths refuse in place.
412output_style = "none" # BP-5 (catalog D2 "Output style / personality module"): Codex's `personality` key at its neutral value (cx§6 `personality (none|friendly|pragmatic)`, cx§2 "Personality layer", `/personality`). `friendly`/`pragmatic` are the swaps; `none` is the selection Codex makes when nobody has chosen, and it appends nothing.
413# C4 (catalog §5 conflict 4): Codex's base prompt VARIES BY APPROVAL MODE (cx§2: "proactively run
414# tests only under never"). This preset pins prompt + approval together; when CONTINUING an
415# imported rollout, the emulate path replays the rollout's own persisted base_instructions
416# verbatim (session_meta carries them — cx§2:101; supercode SessionMeta.system_prompt), which is
417# exact prompt parity by construction rather than imitation.
418
419[core.tools]
420# BP-3: five more optional default-tool names (the `view_image` precedent),
421# each closing a gap-ledger row the catalog scores `✓` for Codex:
422# `request_user_input` — cx's own experimental spelling of the question
423# tool, registered as an ALIAS of the same tool object `ask_user` is, so
424# a continued Codex session's calls keep resolving (catalog:45).
425# `current_time` + `sleep` — cx's clock/sleep features (catalog:53).
426# `get_context_remaining` + `new_context` — cx's token_budget feature
427# (catalog:54).
428# `image_gen` — cx's image feature (catalog:52); the tool posts to the
429# SESSION's own provider `/v1/images/generations` and reports
430# unsupported_action when that provider has no image route.
431enabled = ["bash", "view_image", "request_user_input", "current_time", "sleep", "get_context_remaining", "new_context", "image_gen"]
432 # Codex has NO read/write/edit/glob/grep function tools:
433 # reads via shell (cat, rg), writes via apply_patch (cx§1 "File reads/writes"; D1 footnote ¹).
434 # Disabling edit/write advertising is ALSO the C1 resolution.
435 # `view_image` (S6/S12 home, catalog:28) closes the gap-ledger row: with `read_file` off, cx-parity
436 # would otherwise have NO image-input pathway at all, unlike stock Codex's dedicated tool.
437schema_tier = "full"
438
439[core.skills]
440enabled = true # SKILL.md discovery, $skill mentions (cx§7 Skills).
441harness = "codex" # BP-6: the loop discovers SKILL.md from Codex's own documented roots — admin `/etc/codex/skills`, the bundled `$CODEX_HOME/skills/.system` cache, user `~/.agents/skills` + `$CODEX_HOME/skills`, repo `.agents/skills` from cwd to the repo root (cx§7) — invoked by `$slug` mention or the `skill` tool
442implicit_match = false # cx§7 also matches a skill IMPLICITLY from its description; off here, so only an explicit `$slug`/`/skill:` invocation or a `skill` tool call ever spends a body's tokens
443 # D-7 (S3-amended, no longer a judgment call): the read pathway is bash (`cat`) in the codex
444 # shape — §2.1's D-7 now names read_file|bash explicitly; the resolver warns, doesn't error.
445
446[core.compaction]
447enabled = true
448summarize = true # /compact + auto-compaction at model_auto_compact_token_limit (cx§2). BP-4: also arms the CORE span-summary side-call (design §1.5 obligation 5; §3.1 "SpanSummary side-call")
449focus_instructions = "" # BP-4 (catalog:98): Codex has no standing compaction focus either — `/compact [instructions]` steers per invocation (cx§2). Empty states that explicitly.
450reserve_tokens = 16384 # BP-1: §4.3's own TOML block armed NEITHER trigger, so `Agent::maybe_compact`
451 # returned false on its `threshold.is_none() && reserve_tokens.is_none()` guard
452 # and cx-parity could never compact at all — contradicting the `summarize` line's
453 # own comment ("auto-compaction at model_auto_compact_token_limit"). That limit is
454 # an absolute TOKEN limit, i.e. §1.5's context-window-pressure trigger, not a
455 # message count — so the pressure knob is the one to arm (pi-core's `after_messages
456 # = 0` comment states the same reading: "the trigger is token pressure, never
457 # message count"). Value transcribed from cc-parity/pi-core's own 16384, the §1.5
458 # `reserve_tokens` equivalent this schema expresses a foreign auto-compact
459 # threshold as.
460
461[core.model_switch]
462allow_switch = true # BP-13 (catalog D9 "Mid-session model switching"): Codex's `/model` changes the model without losing the thread (cx§9). Arms the governed switch — dep-8 reasoning-artifact filtering plus a persisted `model_change` record in the session journal.
463notice = true # BP-13: Codex additionally INJECTS switch instructions into the conversation on a mid-session change (cx§9), so the incoming model reads the handoff instead of inferring it. CC does not, which is why cc-parity leaves this unset.
464
465[core.session]
466append_only = true # BP-8 (catalog:150): Codex appends every rollout line and flushes per line, with a retry (cx§5) — a crash mid-turn keeps the turn. Arms `crate::session_journal`.
467 # `queue_persist` is deliberately NOT set: catalog:154 is `—` for Codex (no queue-operation records), and a cx-parity session must not gain an input-durability guarantee stock Codex does not have.
468auto_title = true # BP-7 (catalog:150, cx `✓*`): Codex derives a title/preview for every rollout into its SQLite index and offers `/title` for a manual override (cx§5). supercode's equivalent is the same small-model titler cc-parity uses; the VARIANT the catalog footnotes is that cx derives its default from the first message rather than a model call, which is why this row is `✓*` for cx and not `✓`.
469
470[core.prompts]
471# BP-7 (catalog §4a "Review mode"): Codex ships `/review` AND a `codex review`
472# subcommand with a `review_model` of its own (cx§3). The template below is the
473# report format; the model choice stays this session's model, since cx-parity
474# pins no `review_model` (upstream leaves it unset by default too).
475code-review = """Review the current code changes as a dedicated review turn. {args}
476Read the diff and the files it touches with the shell before judging anything.
477Report in exactly these sections, omitting a section only when it is genuinely empty:
4781. Correctness — defects, in severity order (blocker / major / minor), each with file:line and the failing case.
4792. Security — untrusted input reaching a trust boundary, secrets, injection, permission widening.
4803. Reuse and simplification — existing code the change should have used; code the change makes dead.
4814. Verdict — one line: SHIP, SHIP WITH FIXES, or DO NOT SHIP, and why."""
482
483# ---- modules ON ----
484[capabilities.tools_persistent_shell]
485enabled = true
486# exec_command/write_stdin PTY unified exec (cx§1); supercode has it (builtins.rs:981-984)
487[capabilities.tools_apply_patch]
488enabled = true
489per_model = true
490 # freeform envelope, default write path (cx§1); per_model honors C1 via model_catalog bits (cx§9)
491[capabilities.todos]
492enabled = true
493persist = true
494goals = true
495# update_plan is ALWAYS registered (cx§1); goals (S6/S12 home, catalog:138, cx `/goal`) is the persistent-objective variant of this same module — closes a gap-ledger row
496# BP-7: the `goals` key above is that variant, armed. Codex keeps its goal in `goals_1.sqlite`; supercode keeps the same single record in `<session>.goal.json`, restated at the tail of every request while it stands.
497[capabilities.tools_web]
498enabled = true
499fetch = false
500search = true
501 # Codex has hosted web_search but NO web-fetch tool (cx§1); cached mode default
502[capabilities.tools_background]
503enabled = true
504# background terminals, /ps //stop (cx§1); C6 (S8-corrected defense): under `model_requested`, tools run sandboxed WITHOUT prompting unless the model itself escalates — from a background task's perspective that's an auto-run default, satisfying C6's auto-policy requirement without needing a separate allow-list
505[capabilities.subagents]
506enabled = true
507max_depth = 1
508background = true
509background_prompts = "auto_policy"
510 # multi_agent default-on, agents.max_depth default 1 (cx§1, cx§6)
511 # BP-7 (catalog:135 "Background subagents + resume", cx `✓ v2 mailbox
512 # (send_message/wait/interrupt)`): §4.3's own line read
513 # `background = false`, which contradicted the very column it was
514 # transcribing — Codex's multi-agent v2 detaches children and talks to
515 # them through a mailbox. On, with `background_prompts = "auto_policy"`
516 # (C6's required companion, and the honest one for cx: under
517 # `approval = "model_requested"` tools run sandboxed without prompting
518 # unless the MODEL escalates, so a detached child has no interactive
519 # prompt to surface to a parent — the same reasoning
520 # `capabilities.tools_background`'s own C6 comment above already gives).
521[capabilities.tools_question]
522enabled = true
523# BP-3 (flipped from `false`): the catalog scores cx `✓*` for the structured
524# user-question tool — `request_user_input` EXISTS at the pin, behind an
525# experimental flag — and the parity ledger's denominator is that column, so
526# cx-parity has to supply the capability rather than the footnote. The module
527# registers the tool; `[core.tools] enabled` above additionally registers
528# Codex's own spelling as an alias. §2.1's `tools_question → tui|server` dep is
529# met by `[capabilities.tui]` below.
530[capabilities.deferred_tools]
531enabled = true
532core = ["bash", "shell", "apply_patch", "update_plan", "ask_user", "request_user_input", "current_time", "sleep", "get_context_remaining", "new_context", "image_gen"]
533 # ToolExposure::Deferred + native tool_search is Codex's own mechanism (cx§1)
534 # BP-3: the new built-ins stay eager — a feature tool the model must tool_search for first is not the same capability Codex ships
535[capabilities.structured_output]
536enabled = true
537# --output-schema final-response contract (cx§8); module 33, Config.response_format
538
539[capabilities.permissions]
540enabled = true
541approval = "model_requested" # S8 fix: Codex `on-request` default is "the MODEL decides when to ask" (cx§4, protocol.rs:921-924) — NOT supercode's `OnRequest` (client-side allowlist check, config.rs:39-40, 299-302); using the wrong enum value would prompt on every non-allowlisted tool call where stock Codex prompts almost never. `model_requested` is the NEW distinct mode (§3.2) the module must re-implement escalation-initiated-by-the-model for.
542[capabilities.permissions.sandbox]
543tier = "workspace_write" # writes in cwd + tmp, no network (cx§4); RECOMMENDED POSTURE (S16 fix), not upstream's labeled default — codex.md names no sandbox mode "(default)" (unlike approval); this is upstream's own steered guidance ("prefer --sandbox workspace-write", the deprecated --full-auto warning)
544 # BP-10: the TABLE form of the same key (§3.1 defines `sandbox = "X"` as identical to `sandbox = { tier = "X" }`), so the three knobs below can be stated. `enabled` is deliberately left UNSET, exactly as the bare form left it — `crate::sandbox::os_sandbox_active` then keeps the tier-driven trigger this preset already had.
545env_policy = "filtered" # BP-10 (catalog "Child-process env sanitization", cx ✓): Codex's `shell_environment_policy` filters secrets from a spawned shell BY DEFAULT (cx§4). Until this line, cx-parity resolved to `Inherit` and no sanitization happened under the preset at all — the mechanism existed and nothing armed it.
546escalation = "ask" # BP-10 (catalog "Sandbox-escalation path", cx ✓): when a confining tier cannot be enforced on this host, Codex does not silently run unconfined — the user is asked. No handler installed still denies (fail-closed, `crate::sandbox::resolve_escalation`).
547[capabilities.permissions.sandbox.network]
548enabled = true # BP-10 (catalog "Network sandbox / domain rules", cx ✓): `workspace_write` cuts subprocess network (cx§4). Real on this host: macOS seatbelt `(deny network*)`, Linux a fresh network namespace. Per-DOMAIN filtering of arbitrary subprocess traffic is the remaining gap — see the ledger row's note.
549[capabilities.permissions.rules]
550enabled = true # execpolicy .rules allow/prompt/forbidden → translated into deny→ask→allow (C5)
551deny = []
552ask = []
553allow = []
554[capabilities.permissions.protected_paths]
555enabled = true
556# Rule-layer floor (file-tools + bash redirect targets + apply_patch + known
557# argv-writers; opaque/dynamic bash writes forced to Ask) — NOT the same as
558# cx's `workspace_write` OS sandbox read-only mount above; see
559# `crate::permissions` module doc for exactly what is/isn't covered here.
560paths = [".git/**", ".codex/**"] # read-only even inside writable roots (cx§4 workspace-write)
561[capabilities.permissions.approvals]
562persist = true # BP-10: cx's `with_cached_approval` grants are saved as prefix rules that outlive the process (cx§4). Same per-project store and the same one-file reversibility as cc-parity.
563
564# BP-10 (catalog "Named permission profiles", cx §4 `[permissions.<name>]`
565# Beta: "extends, fs+net rules"). Reusable, INHERITABLE bundles, selectable
566# per run without editing config — `supercode --permission-profile <name>`
567# (sugar for `-c capabilities.permissions.profile=<name>`, so the selection
568# goes through the same resolver every other key does). The three bundles
569# below are Codex's own three postures expressed in this schema; no
570# `profile` key is set, so cx-parity's own top-level permission keys stand
571# until a run names one.
572# Each bundle carries the SANDBOX TIER + RULES cx§4 names ("extends, fs+net
573# rules"), deliberately not `approval`: the approval mode is coupled to the
574# base prompt (C4) and to §2.2 C6's background-prompt dependency, so a
575# bundle that silently changed it would make a permission switch a
576# loop-shape switch. The mechanism accepts `approval` from a user's own
577# bundle; these shipped three do not use it.
578[capabilities.permissions.profiles.read-only]
579sandbox = "read_only"
580[capabilities.permissions.profiles.workspace-write]
581sandbox = "workspace_write"
582[capabilities.permissions.profiles.locked-down]
583extends = "read-only" # the inheritance half of the row: this bundle IS read-only, plus a shell floor
584rules = { deny = ["bash", "shell", "background_exec"] }
585
586[capabilities.trust]
587enabled = true
588default = "ask"
589# [projects] trust_level gate + hook hash-trust (cx§4:153, cx§7)
590[capabilities.mcp]
591enabled = true
592serve = true
593# full client stack (cx§7); serve = codex mcp-server analog (module 16)
594[capabilities.hooks]
595enabled = true
596# CC-compatible 10-event shape, hash-trusted (cx§7 "Lifecycle hooks")
597[capabilities.model_catalog]
598enabled = true
599provider = "openai" # BP-13: the alias scope in force for this session (cx§9)
600service_tier = "auto" # BP-13 (catalog D9 "Fast mode / service tiers"): cx's `model_service_tier` default; `/fast` moves the session to `priority` (cx§9)
601allowed_models = [] # BP-13 (catalog D9): Codex pins features/profiles by requirements, not by a model allowlist — stated empty so the preset says so
602denied_models = []
603# capability bits (apply_patch_tool_type, supports_search_tool) drive
604 # per-model tool swaps — the C1 resolution machinery (cx§9 "Model catalog")
605# BP-5 (catalog D2 "Per-model-family base-prompt selection", cx§2 "Per-model
606# base instructions"): Codex selects its system prompt PER MODEL FAMILY from
607# bundled markdown — gpt_5_codex_prompt.md, gpt-5.1-codex-max_prompt.md,
608# gpt-5.2-codex_prompt.md, gpt_5_1_prompt.md, gpt_5_2_prompt.md, and
609# prompt_with_apply_patch_instructions.md, the variant that appends the full
610# apply_patch tutorial. The FUNCTIONAL split across that set is exactly that
611# tutorial: a codex-tuned family is taught the patch envelope, a general
612# family is not. These two entries are that split, in supercode own words —
613# never a copy of upstream prompt text. A model matching neither keeps
614# core.system_prompt, so this table narrows nothing.
615# Keys are model-id globs and the MOST SPECIFIC (longest) match wins:
616# openai/gpt-5.2-codex takes *codex*, openai/gpt-5.1 takes *gpt-5*.
617[capabilities.model_catalog.base_prompts]
618"*gpt-5*" = """
619You are a coding agent running in a terminal. Work through the shell: read
620with cat/rg, change files by writing them out, and verify with the project own
621commands before reporting anything as done. Prefer the smallest change that
622fixes the problem, and say plainly what you did and what you did not check.
623"""
624"*codex*" = """
625You are a coding agent running in a terminal. Work through the shell: read
626with cat/rg, change files by writing them out, and verify with the project own
627commands before reporting anything as done. Prefer the smallest change that
628fixes the problem, and say plainly what you did and what you did not check.
629
630File edits go through the apply_patch envelope. One envelope may carry several
631operations, and every path is relative to the working directory:
632
633*** Begin Patch
634*** Add File: path/to/new.rs
635+the whole new file, one + per line
636*** Update File: path/to/existing.rs
637@@ context line locating the hunk
638-the exact line being replaced
639+its replacement
640*** Delete File: path/to/gone.rs
641*** End Patch
642
643Context lines carry a leading space, removals a -, additions a +. An update
644whose context does not appear verbatim in the file is rejected whole, so read
645the file first and quote it exactly.
646"""
647[capabilities.model_catalog.models."openai/gpt-5*"]
648apply_patch = true # BP-13: the freeform apply_patch envelope IS this family's write path (cx§1) — so `edit_file`/`write_file` are never co-advertised to it
649search_tool = true
650[capabilities.model_catalog.models."openai/gpt-4*"]
651apply_patch = false # BP-13: a family the catalog marks as NOT taking the envelope gets `edit_file`/`write_file` instead — the tool SURFACE adapts to the model, inside `ToolRegistry::from_config`'s own selection
652[capabilities.tui]
653enabled = true
654
655# ---- notable OFFs ----
656[capabilities.tools_search]
657enabled = false
658# no glob/grep tools; "prefer rg" via shell is prompt guidance (cx§2)
659[capabilities.plan_mode]
660enabled = false
661# /plan is effort-tier steering, not a CC/OC restriction mode (cx§6; catalog D1 CC+OC)
662[capabilities.memory]
663enabled = false
664# [features].memories = false default (cx§6, cx§7)
665[capabilities.checkpoint]
666enabled = true
667restore = false
668# BP-7 (catalog:112 "Turn diff tracking", cx `✓ turn_diff_tracker`): §4.3's own
669# line read `enabled = false` with the reason "no shadow-git; ghost_snapshot is a
670# legacy no-op (cx§6)". That reason is about the RESTORE half, and it still
671# stands — `restore = false` states it as a key rather than as prose, and
672# `CheckpointObserver::restore` refuses under it. But Codex genuinely DOES track
673# each turn's cumulative file diff (cx§3 `turn_diff_tracker`), and this module is
674# where supercode captures per-turn pre-images, so leaving the whole table off
675# meant cx-parity captured no turn-diff data at all — the gap the ledger row
676# named. On: tracking without restoring, which is exactly Codex's shape.
677# `file-checkpointing-code-restore` is `—` for cx and stays a cc-only row.
678[capabilities.session_tree]
679enabled = false
680# rollout is STRICTLY LINEAR (C7); fork = truncate+copy (D5 footnote ¹³)
681[capabilities.session_share]
682enabled = false
683[capabilities.lsp]
684enabled = false
685[capabilities.formatters]
686enabled = false
687[capabilities.server]
688enabled = false
689# app-server parity is out of preset scope — see gaps
690[capabilities.reduction]
691enabled = false
692[capabilities.cache]
693enabled = false
694[capabilities.model_oauth]
695enabled = false
696# ChatGPT-subscription login (cx§9) — module 27 deferred
697"#;
698
699pub const OC_PARITY_TOML: &str = r#"# built-in preset: oc-parity — opencode's default surface, composed.
701schema_version = 1
702
703[core]
704env_context = true
705project_context = true # AGENTS.md + instructions[] concat (oc§6)
706nested_instructions = true # S6/S12 home: nested AGENTS.md auto-attached only for touched-file dirs, oc default (catalog:84; opencode.md:34 "nested-AGENTS.md") — closes an oc-parity gap-ledger row
707instruction_imports = true # S6/S12 home: `instructions[]` config imports, oc default (catalog:85; opencode.md:367) — closes an oc-parity gap-ledger row
708max_tool_output_bytes = 51200 # tool_output.max_bytes default 51200 / 2000 lines (oc§1 Truncate service)
709
710[core.session]
711auto_title = true # S6/S12 home: hidden title+summary agents (deny-all utility agents) on small_model, oc default (catalog:150; opencode.md:169-170,235) — closes an oc-parity gap-ledger row; small_model is "" below so this falls back to the main model per D-9 until a cheap model is configured
712
713[core.tools]
714enabled = ["read_file", "bash", "edit_file", "write_file"] # oc registry core (oc§1; read subsumes ls)
715schema_tier = "full"
716[core.tools.read_file]
717multimodal = true # images/PDFs as attachments (oc§1 read)
718[core.tools.bash]
719timeout_secs = 120 # flags.bashDefaultTimeoutMs default 120000 (oc§1 bash)
720
721[core.skills]
722enabled = true # skill tool + .opencode/skills + remote registries (oc§7)
723harness = "opencode" # BP-6: the loop reads opencode's own roots — `{skill,skills}` under the global config dir and every `.opencode` dir (oc§7)
724
725[core.compaction]
726enabled = true
727summarize = true # compaction{auto,prune,…} (oc§6)
728
729# ---- modules ON ----
730[capabilities.tools_search]
731enabled = true
732# glob + grep via ripgrep (oc§1)
733[capabilities.todos]
734enabled = true
735persist = true
736# todowrite → SQLite todo table (oc§1)
737[capabilities.tools_web]
738enabled = true
739fetch = true
740search = false
741 # webfetch is default; websearch only under the Zen provider / exa flags (oc§1 "webSearchEnabled")
742[capabilities.subagents]
743enabled = true
744max_depth = 2
745background = false
746 # task tool → child session via parentID, resumable task_id (oc§1); background is env-gated experimental → off
747[capabilities.tools_apply_patch]
748enabled = true
749per_model = true
750 # THE C1 precedent: swapped in (edit/write out) for gpt-* models (oc§1 apply_patch; catalog §5 conflict 1)
751[capabilities.plan_mode]
752enabled = false
753# S18 fix (flipped from `true`): opencode's plan_enter/plan_exit TOOLS — exactly what this module is defined by (§2 module 8) — are DENY-BY-DEFAULT at the pin (opencode.md:251), and this preset's own translated rule set below denies them. What oc actually runs by default is the LEGACY generation: the plan agent is a permission-ruleset agent (edit denied) — already expressible as an agent-scoped `permissions.rules` restriction, not the tool-based `plan_mode` module. Enabling `plan_mode` here would contradict oc's own deny-default; off is the honest reading.
754
755[capabilities.permissions]
756enabled = true
757approval = "on_request" # ask-flow with once|always|reject replies (oc§4 "Ask/approve flow")
758sandbox = "danger_full_access" # opencode has NO OS sandbox (catalog D4: sandbox is CC+CX only)
759[capabilities.permissions.rules]
760enabled = true
761# opencode's default policy, TRANSLATED per the C5 decision (last-match-wins → deny→ask→allow
762# first-match). Source policy (oc§4 "Default policy"): {"*": allow} with carve-outs
763# doom_loop: ask, external_directory: ask, question: deny, plan_enter/plan_exit: deny,
764# read {*.env: ask, *.env.*: ask, *.env.example: allow}.
765#
766# S4 fix — this is NOT "the same fixed point" as oc's last-match algebra, and is recorded honestly
767# as THREE NAMED DEVIATIONS rather than claimed as exact parity:
768# 1. `.env.example` → ASK here, not ALLOW. Under first-match deny→ask→allow, a read of
769# `.env.example` matches the ask-rule `read_file(*.env.*)` (glob matches) BEFORE the allow
770# list is ever consulted, so it asks where stock opencode allows. The engine's rule grammar
771# has no specificity/negation to express "ask unless a more-specific allow" — fixing this
772# would require adding that to the grammar (not done here); the deviation is in the SAFE
773# direction (stricter) and is named, not hidden.
774# 2. `doom_loop` is NOT a rule-language pattern at all — it's a repetition TRIGGER (same tool
775# call repeated), not a tool/path match. Routed instead to its actual mechanism: the P4
776# doom-loop breaker (a call-repetition counter + PreToolHook default, §5.2 P4) — no rule
777# entry for it below.
778# 3. `external_directory` is an oc PERMISSION CATEGORY (any tool touching paths outside the
779# worktree), not a tool name — routed instead to its actual permission category: `[core]
780# additional_dirs` (Config.additional_dirs, config.rs:169) governs which extra roots are
781# writable at all; paths outside cwd AND outside `additional_dirs` are simply not reachable,
782# which is a stricter (not equivalent) reading of oc's ask-by-default.
783deny = ["plan_enter", "plan_exit"] # matches module 8's off-by-default above (S18) and oc's own "plan_enter/plan_exit: deny"
784ask = ["read_file(*.env)", "read_file(*.env.*)"] # includes .env.example per deviation 1 above (glob matches before any allow)
785allow = ["*"]
786[capabilities.permissions.protected_paths]
787enabled = false
788# oc does .env protection through rules (above), not a path module
789
790[capabilities.trust]
791enabled = true
792default = "ask"
793# DELIBERATE SAFETY DEVIATION: opencode LACKS a project trust gate (catalog §3 closing) yet loads
794# .opencode/ plugins/tools/commands from the repo. Our resolver treats plugins→trust as a HARD dep
795# (D-10: "config-borne code execution without a trust gate is an injection hole") — so oc-parity
796# ships the gate ON. This only NARROWS behavior (§3.3 monotonic-tightening spirit); recorded, not hidden.
797
798[capabilities.mcp]
799enabled = true
800# local/remote/OAuth servers (oc§7)
801[capabilities.plugins]
802enabled = true
803# .opencode/plugin + npm specs (oc§7); dep on trust satisfied above
804[capabilities.lsp]
805enabled = true
806# 38 auto-spawned servers; diagnostics into edit/write results (oc§7, oc§10)
807[capabilities.formatters]
808enabled = true
809diff_back = true
810# ~27 format-on-write formatters; diff_back honors C10 (oc§7; oc§10; catalog §5 conflict 10)
811[capabilities.checkpoint]
812enabled = true
813# shadow-git snapshots + revert/unrevert (oc§4 "Snapshots"/"Revert")
814[capabilities.session_share]
815enabled = true
816# share manual|auto|disabled, default manual (oc§5, oc§6)
817[capabilities.server]
818enabled = true
819# the client/server split: every frontend is an HTTP client (oc§8)
820[capabilities.model_catalog]
821enabled = true
822small_model = ""
823# models.dev catalog + small_model config key (oc§6, oc§9)
824[capabilities.tui]
825enabled = true
826
827# ---- notable OFFs ----
828[capabilities.tools_question]
829enabled = false
830# question tool is DENY-by-default outside build/plan agents (oc§1, oc§4)
831[capabilities.tools_background]
832enabled = false
833# background subagents are env-gated experimental at the pin (oc§1)
834[capabilities.session_tree]
835enabled = false
836# oc sessions are parent/child linear, no in-place tree (D5; C7)
837[capabilities.memory]
838enabled = false
839[capabilities.hooks]
840enabled = false
841# no config-registered hooks; the plugin API is the interception layer (oc§7)
842[capabilities.deferred_tools]
843enabled = false
844# opencode advertises eagerly (D1: deferred is CC+CX)
845[capabilities.cache]
846enabled = false
847[capabilities.reduction]
848enabled = false
849# oc "prune" is the LOSSY analog (catalog §1 UNIQUE OC note); ours stays off to match, mechanism on per §1.13
850[capabilities.structured_output]
851enabled = false
852[capabilities.model_oauth]
853enabled = false
854# provider /login flows (oc§9) — module 27 deferred
855"#;
856
857pub const TOKEN_SAVER_TOML: &str = r#"# built-in preset: token-saver — the reduction spine over the minimal core.
859schema_version = 1
860extends = "pi-core" # smallest surface = cheapest surface; every knob below overrides it
861
862[core.tools]
863schema_tier = "minimal" # TR-8/T5 schema tiering (config.rs:219-225)
864# C9 (catalog §5 conflict 9): a GLOBAL minimal tier is a footgun for models trained on exact
865# schemas. Per-tool override survives the global — pin any load-bearing tool back:
866[core.tools.edit_file]
867schema_tier = "full" # exact-string edit is the least forgiving schema; keep it verbatim
868
869[core.compaction]
870enabled = true
871reserve_tokens = 24576 # trigger earlier than pi's 16384 — spend the summary, save the window
872keep_recent_tokens = 10000 # aggressive: half of pi's keep budget (recall traded — see caveats)
873summarize = true # SpanSummary side-call (reduce.rs:274-289) → small_model below (D-9)
874
875[capabilities.reduction] # module 23 — ALL genuinely-optional passes on (≡ CLI reduce=true, userconfig.rs:33-38)
876enabled = true
877# NOTE (S7): no `truncation` key here — A7 ToolOutputTruncated (reduce.rs:95-103) is always-on core
878# plumbing (§1.13), never a `[capabilities.reduction]` toggle, in token-saver same as every other preset.
879stale_reads = true # A8 FileReadElided (reduce.rs:104-111)
880diff_reads = true # TR-3 FileReadDiffed (reduce.rs:202-217)
881duplicates = true # TR-2 DuplicateOutput (reduce.rs:228-234)
882supersede = true # TR-6 Superseded (reduce/supersede.rs)
883tool_input_elision = true # TR-10 ToolInputElided (reduce.rs:148-169)
884normalize_output = true # T30 OutputNormalized (reduce/normalize.rs)
885image_redaction = true # A9 ImageRedacted (reduce.rs:112-116) — ON here, off everywhere else
886span_summaries = true # TR-7 (reduce/summarize.rs; D-9)
887handoff = true # reduce/handoff.rs — smallest-faithful-context model handoff
888
889[capabilities.deferred_tools] # module 24 — the FLAGSHIP lever (SPEC.md B6)
890enabled = true
891core = ["read_file", "bash", "edit_file", "write_file"] # builtins stay eager; everything else behind tool_search
892
893[capabilities.cache] # module 25 — the C2 referee
894enabled = true
895plan = "imported_prefix" # CachePlan::ImportedPrefix (config.rs:88-96)
896warnings = true # cache_warnings (config.rs:227-239): every prefix-churning feature must answer to this
897
898[capabilities.model_catalog] # module 26 — D-9 consumer
899enabled = true
900small_model = "anthropic/claude-haiku-4-5" # compaction summaries + span summaries route here, not the main model
901"#;
902
903pub const SUPERCODE_DEFAULT_TOML: &str = r#"# built-in preset: supercode-default — pi-core MINUS {trust, session_tree,
905# session_share, server, plugins}, PLUS the six extra with_builtins() builtins
906# ON, notify available, reduction off (design §4 intro paragraph, S10 fix: NOT
907# "pi-core plus extras" — pi-core itself turns those five modules ON to match
908# pi's kept-list, so this preset is pi-core's core knobs UNCHANGED with a
909# capability delta). This is what `supercode` resolves to with NO config file
910# at all — "today's defaults, named and warned" rather than implicit
911# (design:958-960).
912schema_version = 1
913extends = "pi-core"
914
915# core knobs identical to pi-core (§4.1's [core]/[core.retry]/[core.tools]/
916# [core.skills]/[core.compaction]/[core.steering] blocks) — unchanged, nothing
917# to override here; inherited verbatim via `extends`.
918
919# ---- the S10 delta over pi-core: OFF (today's CLI has none of these — "—"
920# across the board in §2's Today column) ----
921[capabilities.trust]
922enabled = false
923[capabilities.session_tree]
924enabled = false
925[capabilities.session_share]
926enabled = false
927[capabilities.server]
928enabled = false
929[capabilities.plugins]
930enabled = false
931
932# ---- the six extra with_builtins() builtins (tools/mod.rs:179-192), ON ----
933# list_dir/glob/search -> tools_search; apply_patch -> tools_apply_patch;
934# persistent_shell -> tools_persistent_shell; update_plan -> todos.
935[capabilities.tools_search]
936enabled = true
937[capabilities.tools_apply_patch]
938enabled = true
939# NOT per_model: with_builtins() registers every tool struct unconditionally
940# with no per-model filtering at all (§4.6 "faithful to today's actual
941# unfiltered default stack") — this is what makes C1's warning fire here,
942# honestly, rather than suppressing it with a `per_model` bit today's CLI
943# doesn't actually have.
944[capabilities.tools_persistent_shell]
945enabled = true
946[capabilities.todos]
947enabled = true
948
949# notify available (today's CLI already ships full notify support end to end
950# — userconfig.rs:61-71 — unlike pi-core, which doesn't mention it at all).
951[capabilities.notify]
952enabled = true
953
954# reduction off (policies only; A7 truncation + rehydrate stay always-on core
955# regardless, §1.13) — already off by inheritance from pi-core; restated for
956# clarity per the design intro's explicit "reduction off" callout.
957[capabilities.reduction]
958enabled = false
959
960# permissions stays off too (approval = never, sandbox = danger_full_access)
961# — identical to pi-core's own values (config.rs:36-38, tools/mod.rs:40-42);
962# restated verbatim so the C3 mandatory warning fires here by the same
963# mechanism as pi-core's, naming today's actual default stack rather than
964# leaving it implicit (design:971-974).
965[capabilities.permissions]
966enabled = false
967approval = "never"
968sandbox = "danger_full_access"
969"#;
970
971pub const RESERVED_PRESET_NAMES: &[&str] = &[
973 "pi-core",
974 "cc-parity",
975 "cx-parity",
976 "oc-parity",
977 "token-saver",
978 "supercode-default",
979];
980
981pub fn lookup(name: &str) -> Option<&'static str> {
986 match name {
987 "pi-core" => Some(PI_CORE_TOML),
988 "cc-parity" => Some(CC_PARITY_TOML),
989 "cx-parity" => Some(CX_PARITY_TOML),
990 "oc-parity" => Some(OC_PARITY_TOML),
991 "token-saver" => Some(TOKEN_SAVER_TOML),
992 "supercode-default" => Some(SUPERCODE_DEFAULT_TOML),
993 _ => None,
994 }
995}
996
997#[cfg(test)]
998mod tests {
999 use super::*;
1000 use crate::configfile::{resolve, HarnessConfig, ResolveOptions};
1001 use crate::modules::ModuleId;
1002 use crate::tools::ToolRegistry;
1003
1004 fn resolved(name: &str) -> crate::configfile::Resolved {
1007 let toml = lookup(name).unwrap();
1008 resolve(toml, None, &ResolveOptions { strict: true })
1009 .unwrap_or_else(|e| panic!("preset `{name}` failed to resolve: {e}"))
1010 }
1011
1012 fn registry_names(config: &crate::Config) -> Vec<String> {
1013 ToolRegistry::from_config(config)
1014 .iter()
1015 .map(|t| t.name().to_string())
1016 .collect()
1017 }
1018
1019 #[test]
1025 fn every_reserved_preset_parses() {
1026 for name in RESERVED_PRESET_NAMES {
1027 let toml = lookup(name).unwrap_or_else(|| panic!("no TOML for preset `{name}`"));
1028 HarnessConfig::from_toml_str(toml)
1029 .unwrap_or_else(|e| panic!("preset `{name}` failed to parse: {e}"));
1030 }
1031 }
1032
1033 #[test]
1036 fn lookup_returns_none_for_non_preset_names() {
1037 assert!(lookup("not-a-real-preset").is_none());
1038 assert!(lookup("./some/path.toml").is_none());
1039 assert!(lookup("").is_none());
1040 }
1041
1042 #[test]
1057 fn cc_parity_registers_and_advertises_the_web_tools() {
1058 let r = resolved("cc-parity");
1059 assert!(
1060 r.config.module_registry,
1061 "a resolved preset must drive the module registry with no experimental flag"
1062 );
1063 let names = registry_names(&r.config);
1064 for tool in ["web_fetch", "web_search"] {
1065 assert!(
1066 names.contains(&tool.to_string()),
1067 "cc-parity must register `{tool}`; registry = {names:?}"
1068 );
1069 assert!(
1070 r.config.tool_enabled(tool),
1071 "cc-parity must advertise `{tool}`"
1072 );
1073 }
1074 }
1075
1076 #[test]
1082 fn cc_parity_activates_the_mcp_client_module() {
1083 let r = resolved("cc-parity");
1084 assert!(r.config.module_activation.is_active(ModuleId::McpClient));
1085 assert_eq!(r.modules.get("mcp"), Some(&true));
1086 }
1087
1088 #[test]
1094 fn cx_parity_registers_view_image_and_no_file_tools() {
1095 let r = resolved("cx-parity");
1096 let names = registry_names(&r.config);
1097 assert!(
1098 names.contains(&"view_image".to_string()),
1099 "cx-parity must register `view_image` (its only image pathway); registry = {names:?}"
1100 );
1101 for tool in ["read_file", "write_file", "edit_file"] {
1102 assert!(
1103 !names.contains(&tool.to_string()),
1104 "cx-parity must NOT register `{tool}`; registry = {names:?}"
1105 );
1106 }
1107 assert!(names.contains(&"apply_patch".to_string()));
1109 assert!(names.contains(&"bash".to_string()));
1110 }
1111
1112 #[test]
1120 fn cc_parity_registers_the_new_core_tools() {
1121 let r = resolved("cc-parity");
1122 let names = registry_names(&r.config);
1123 for tool in [
1124 "ask_user",
1125 "enter_plan_mode",
1126 "exit_plan_mode",
1127 "current_time",
1128 "sleep",
1129 ] {
1130 assert!(
1131 names.contains(&tool.to_string()),
1132 "cc-parity must register `{tool}`; registry = {names:?}"
1133 );
1134 assert!(
1135 r.config.tool_enabled(tool),
1136 "cc-parity must advertise `{tool}`"
1137 );
1138 }
1139 for tool in [
1142 "request_user_input",
1143 "image_gen",
1144 "new_context",
1145 "get_context_remaining",
1146 ] {
1147 assert!(
1148 !names.contains(&tool.to_string()),
1149 "cc-parity must NOT register `{tool}`; registry = {names:?}"
1150 );
1151 }
1152 }
1153
1154 #[test]
1160 fn cx_parity_registers_the_new_core_tools_including_the_codex_spelling() {
1161 let r = resolved("cx-parity");
1162 let names = registry_names(&r.config);
1163 for tool in [
1164 "ask_user",
1165 "request_user_input",
1166 "current_time",
1167 "sleep",
1168 "get_context_remaining",
1169 "new_context",
1170 "image_gen",
1171 ] {
1172 assert!(
1173 names.contains(&tool.to_string()),
1174 "cx-parity must register `{tool}`; registry = {names:?}"
1175 );
1176 assert!(
1177 r.config.tool_enabled(tool),
1178 "cx-parity must advertise `{tool}`"
1179 );
1180 }
1181 for tool in ["enter_plan_mode", "exit_plan_mode"] {
1182 assert!(
1183 !names.contains(&tool.to_string()),
1184 "cx-parity keeps `[capabilities.plan_mode]` off, so `{tool}` must not be \
1185 registered; registry = {names:?}"
1186 );
1187 }
1188 }
1189
1190 #[test]
1194 fn the_new_core_tools_are_eager_under_both_parity_presets() {
1195 for (preset, tools) in [
1196 (
1197 "cc-parity",
1198 &[
1199 "ask_user",
1200 "enter_plan_mode",
1201 "exit_plan_mode",
1202 "current_time",
1203 "sleep",
1204 ][..],
1205 ),
1206 (
1207 "cx-parity",
1208 &[
1209 "ask_user",
1210 "request_user_input",
1211 "current_time",
1212 "sleep",
1213 "get_context_remaining",
1214 "new_context",
1215 "image_gen",
1216 ][..],
1217 ),
1218 ] {
1219 let r = resolved(preset);
1220 let crate::config::ToolAdvertising::Deferred { core } = &r.config.tool_advertising
1221 else {
1222 panic!("{preset} enables `deferred_tools`, so advertising must be Deferred");
1223 };
1224 for tool in tools {
1225 assert!(
1226 core.iter().any(|c| c == tool),
1227 "{preset} must advertise `{tool}` eagerly; core = {core:?}"
1228 );
1229 }
1230 }
1231 }
1232
1233 #[tokio::test]
1238 async fn cc_parity_ask_user_is_answered_by_a_mock_frontend() {
1239 struct Frontend;
1240 #[async_trait::async_trait]
1241 impl crate::mcp::McpElicitationHandler for Frontend {
1242 async fn handle(
1243 &self,
1244 request: &crate::mcp::ElicitationRequest,
1245 ) -> crate::mcp::ElicitationResponse {
1246 assert!(request.message.contains("Which database?"), "{request:?}");
1249 assert_eq!(
1250 request.requested_schema["properties"]["q1"]["type"],
1251 "string"
1252 );
1253 crate::mcp::ElicitationResponse {
1254 action: crate::mcp::ElicitationAction::Accept,
1255 content: Some(serde_json::json!({"q1": "sqlite"})),
1256 }
1257 }
1258 }
1259
1260 let r = resolved("cc-parity");
1261 let registry = ToolRegistry::from_config(&r.config);
1262 let tool = registry.get("ask_user").expect("cc-parity registers it");
1263 let mut ctx = crate::tools::ToolContext::new(std::env::temp_dir());
1264
1265 let args = serde_json::json!({"questions": [{
1268 "question": "Which database?",
1269 "header": "Database",
1270 "options": [{"label": "postgres"}, {"label": "sqlite"}]
1271 }]});
1272 let refused = tool.execute(args.clone(), &ctx).await;
1273 assert!(
1274 refused.is_err(),
1275 "headless must be deny-default, got {refused:?}"
1276 );
1277
1278 ctx.question_handler = Some(crate::tools::UserQuestionHandler(std::sync::Arc::new(
1279 Frontend,
1280 )));
1281 let answer = tool
1282 .execute(args, &ctx)
1283 .await
1284 .expect("the frontend answers");
1285 assert!(answer.contains("Database: sqlite"), "{answer}");
1286 }
1287
1288 #[tokio::test]
1292 async fn cc_parity_plan_exit_needs_the_approval_door() {
1293 struct Door(std::sync::atomic::AtomicBool);
1294 impl crate::permissions::PermissionsApprovalHandler for Door {
1295 fn ask(
1296 &self,
1297 req: &crate::permissions::ApprovalRequest,
1298 ) -> crate::permissions::ApprovalOutcome {
1299 assert_eq!(req.tool, "exit_plan_mode");
1300 assert!(
1301 req.subject
1302 .is_some_and(|s| s.contains("rewrite the parser")),
1303 "the approval must carry the plan: {:?}",
1304 req.subject
1305 );
1306 if self.0.swap(true, std::sync::atomic::Ordering::SeqCst) {
1308 crate::permissions::ApprovalOutcome::Allow
1309 } else {
1310 crate::permissions::ApprovalOutcome::Deny
1311 }
1312 }
1313 }
1314
1315 let r = resolved("cc-parity");
1316 let registry = ToolRegistry::from_config(&r.config);
1317 let enter = registry.get("enter_plan_mode").expect("registered");
1318 let exit = registry.get("exit_plan_mode").expect("registered");
1319 let mut ctx = crate::tools::ToolContext::new(std::env::temp_dir());
1320 ctx.approval_handler = Some(crate::tools::ToolApprovalHandler(std::sync::Arc::new(
1321 Door(std::sync::atomic::AtomicBool::new(false)),
1322 )));
1323
1324 enter
1325 .execute(serde_json::json!({}), &ctx)
1326 .await
1327 .expect("entering plan mode");
1328 assert!(ctx.plan_mode.is_active());
1329
1330 let refused = exit
1331 .execute(serde_json::json!({"plan": "rewrite the parser"}), &ctx)
1332 .await
1333 .expect("a refusal is a result, not an error");
1334 assert!(refused.contains("did NOT approve"), "{refused}");
1335 assert!(
1336 ctx.plan_mode.is_active(),
1337 "a refused exit must keep the mode on"
1338 );
1339
1340 let approved = exit
1341 .execute(serde_json::json!({"plan": "rewrite the parser"}), &ctx)
1342 .await
1343 .expect("approved exit");
1344 assert!(approved.contains("APPROVED"), "{approved}");
1345 assert!(!ctx.plan_mode.is_active());
1346 }
1347
1348 #[test]
1352 fn compaction_summarize_reaches_config_for_every_preset() {
1353 for name in RESERVED_PRESET_NAMES {
1354 let r = resolved(name);
1355 assert!(
1356 r.config.compaction_summarize,
1357 "preset `{name}` sets `core.compaction.summarize = true`; it must reach Config"
1358 );
1359 }
1360 }
1361
1362 #[test]
1369 fn cx_parity_arms_the_compaction_pressure_trigger() {
1370 let r = resolved("cx-parity");
1371 assert!(r.config.compaction_enabled);
1372 assert_eq!(r.config.compaction_reserve_tokens, Some(16384));
1373 }
1374
1375 #[test]
1379 fn module_registry_false_is_an_explicit_opt_out() {
1380 let toml = format!("{CC_PARITY_TOML}\n[experimental]\nmodule_registry = false\n");
1381 let r = resolve(&toml, None, &ResolveOptions::default()).expect("resolves");
1382 assert!(!r.config.module_registry);
1383 let names = registry_names(&r.config);
1384 let builtins: Vec<String> = ToolRegistry::with_builtins()
1385 .iter()
1386 .map(|t| t.name().to_string())
1387 .collect();
1388 assert_eq!(names, builtins);
1389 }
1390
1391 #[test]
1396 fn token_saver_and_supercode_default_extend_pi_core() {
1397 let ts = HarnessConfig::from_toml_str(TOKEN_SAVER_TOML).unwrap();
1398 assert_eq!(ts.extends.as_deref(), Some("pi-core"));
1399 let sd = HarnessConfig::from_toml_str(SUPERCODE_DEFAULT_TOML).unwrap();
1400 assert_eq!(sd.extends.as_deref(), Some("pi-core"));
1401 let pc = HarnessConfig::from_toml_str(PI_CORE_TOML).unwrap();
1402 assert_eq!(pc.extends, None);
1403 }
1404
1405 fn preset_ctx(config: &crate::Config) -> crate::tools::ToolContext {
1411 let (ctx, _, _) = crate::agent::build_tool_context(config);
1412 ctx
1413 }
1414
1415 fn tmp_dir(tag: &str) -> std::path::PathBuf {
1416 let dir = std::env::temp_dir().join(format!(
1417 "supercode-bp2-{tag}-{}-{}",
1418 std::process::id(),
1419 std::time::SystemTime::now()
1420 .duration_since(std::time::UNIX_EPOCH)
1421 .map(|d| d.as_nanos())
1422 .unwrap_or(0)
1423 ));
1424 std::fs::create_dir_all(&dir).unwrap();
1425 dir
1426 }
1427
1428 async fn run_tool(
1429 config: &crate::Config,
1430 tool: &str,
1431 args: serde_json::Value,
1432 ) -> crate::error::Result<String> {
1433 let registry = ToolRegistry::from_config(config);
1434 let t = registry
1435 .get(tool)
1436 .unwrap_or_else(|| panic!("`{tool}` is not registered under this preset"));
1437 t.execute(args, &preset_ctx(config)).await
1438 }
1439
1440 #[tokio::test]
1445 async fn cc_parity_read_file_numbers_lines_cat_n_style() {
1446 let mut r = resolved("cc-parity");
1447 assert!(
1448 r.config.read_file_line_numbers,
1449 "cc-parity must set `[core.tools.read_file] line_numbers`"
1450 );
1451 let dir = tmp_dir("readnum");
1452 std::fs::write(dir.join("sample.txt"), "alpha\nbeta\ngamma\n").unwrap();
1453 r.config.cwd = dir.clone();
1454
1455 let whole = run_tool(
1456 &r.config,
1457 "read_file",
1458 serde_json::json!({"path": "sample.txt"}),
1459 )
1460 .await
1461 .unwrap();
1462 assert_eq!(
1463 whole, " 1\talpha\n 2\tbeta\n 3\tgamma\n",
1464 "cc-parity read_file must emit a `cat -n` gutter"
1465 );
1466
1467 let sliced = run_tool(
1468 &r.config,
1469 "read_file",
1470 serde_json::json!({"path": "sample.txt", "offset": 2, "limit": 2}),
1471 )
1472 .await
1473 .unwrap();
1474 assert_eq!(
1475 sliced, " 2\tbeta\n 3\tgamma",
1476 "an `offset` read must number from the offset, not from 1"
1477 );
1478 }
1479
1480 #[tokio::test]
1484 async fn pi_core_read_file_keeps_the_raw_unnumbered_slice() {
1485 let mut r = resolved("pi-core");
1486 assert!(!r.config.read_file_line_numbers);
1487 let dir = tmp_dir("readraw");
1488 std::fs::write(dir.join("sample.txt"), "alpha\nbeta\n").unwrap();
1489 r.config.cwd = dir.clone();
1490 let out = run_tool(
1491 &r.config,
1492 "read_file",
1493 serde_json::json!({"path": "sample.txt"}),
1494 )
1495 .await
1496 .unwrap();
1497 assert_eq!(out, "alpha\nbeta\n");
1498 }
1499
1500 #[tokio::test]
1506 async fn cc_parity_read_file_renders_pdf_text_and_notebook_cells() {
1507 let mut r = resolved("cc-parity");
1508 assert!(r.config.read_file_multimodal);
1509 let dir = tmp_dir("multimodal");
1510 r.config.cwd = dir.clone();
1511
1512 std::fs::write(
1513 dir.join("doc.pdf"),
1514 b"%PDF-1.4\n1 0 obj\n<< /Type /Page >>\nendobj\n2 0 obj\n<< >>\nstream\nBT (parity page text) Tj ET\nendstream\nendobj\n%%EOF\n"
1515 .as_slice(),
1516 )
1517 .unwrap();
1518 let pdf = run_tool(
1519 &r.config,
1520 "read_file",
1521 serde_json::json!({"path": "doc.pdf"}),
1522 )
1523 .await
1524 .unwrap();
1525 assert!(pdf.contains("--- page 1 ---"), "{pdf}");
1526 assert!(pdf.contains("parity page text"), "{pdf}");
1527
1528 let nb = serde_json::json!({
1529 "metadata": {"kernelspec": {"display_name": "Python 3"}},
1530 "cells": [{
1531 "cell_type": "code", "execution_count": 2, "source": ["print('hi')\n"],
1532 "outputs": [{"output_type": "stream", "name": "stdout", "text": ["hi\n"]}]
1533 }]
1534 });
1535 std::fs::write(dir.join("nb.ipynb"), nb.to_string()).unwrap();
1536 let out = run_tool(
1537 &r.config,
1538 "read_file",
1539 serde_json::json!({"path": "nb.ipynb"}),
1540 )
1541 .await
1542 .unwrap();
1543 assert!(out.contains("--- cell 0 (code) [2] ---"), "{out}");
1544 assert!(out.contains("print('hi')"), "{out}");
1545 assert!(out.contains("[stdout]\nhi"), "{out}");
1546 }
1547
1548 #[test]
1553 fn both_parity_presets_arm_parallel_tool_calls() {
1554 for name in ["cc-parity", "cx-parity"] {
1555 assert!(
1556 resolved(name).config.parallel_tool_calls,
1557 "`{name}` must set `core.parallel_tool_calls`"
1558 );
1559 }
1560 assert!(
1561 !resolved("pi-core").config.parallel_tool_calls,
1562 "pi-core keeps the sequential path"
1563 );
1564 }
1565
1566 #[tokio::test]
1576 async fn cc_parity_edit_refuses_unread_and_stale_files_and_accepts_a_bash_view() {
1577 let mut r = resolved("cc-parity");
1578 assert!(r.config.edit_file_require_read_before_edit);
1579 let dir = tmp_dir("staleedit");
1580 r.config.cwd = dir.clone();
1581 let path = dir.join("code.txt");
1582 std::fs::write(&path, "alpha\n").unwrap();
1583
1584 let ctx = preset_ctx(&r.config);
1585 let registry = ToolRegistry::from_config(&r.config);
1586 let read = registry.get("read_file").unwrap();
1587 let edit = registry.get("edit_file").unwrap();
1588 let bash = registry.get("bash").unwrap();
1589 let edit_call = |old: &str, new: &str| serde_json::json!({"path": "code.txt", "old_string": old, "new_string": new});
1590
1591 let never = edit
1593 .execute(edit_call("alpha", "beta"), &ctx)
1594 .await
1595 .expect_err("an unread file must be refused");
1596 assert!(
1597 never.to_string().contains("must be read with `read_file`"),
1598 "{never}"
1599 );
1600
1601 read.execute(serde_json::json!({"path": "code.txt"}), &ctx)
1605 .await
1606 .unwrap();
1607 edit.execute(edit_call("alpha", "beta"), &ctx)
1608 .await
1609 .expect("a read file edits");
1610 edit.execute(edit_call("beta", "gamma"), &ctx)
1611 .await
1612 .expect("the model's own consecutive edit is not stale");
1613
1614 std::fs::write(&path, "one\ntwo\n").unwrap();
1616 read.execute(serde_json::json!({"path": "code.txt"}), &ctx)
1617 .await
1618 .unwrap();
1619 std::fs::write(&path, "one\ntwo\nthree (someone else)\n").unwrap();
1620 let stale = edit
1621 .execute(edit_call("one", "1"), &ctx)
1622 .await
1623 .expect_err("an edit against a changed file must be refused");
1624 assert!(
1625 stale
1626 .to_string()
1627 .contains("has changed on disk since it was read"),
1628 "{stale}"
1629 );
1630
1631 bash.execute(serde_json::json!({"command": "cat code.txt"}), &ctx)
1633 .await
1634 .unwrap();
1635 edit.execute(edit_call("two", "2"), &ctx)
1636 .await
1637 .expect("a `cat` view satisfies read-before-edit");
1638 }
1639
1640 #[test]
1643 fn bash_view_exemption_is_narrow() {
1644 use crate::tools::bash_view_target;
1645 assert_eq!(
1646 bash_view_target("cat src/lib.rs").as_deref(),
1647 Some("src/lib.rs")
1648 );
1649 assert_eq!(bash_view_target("/bin/cat x.txt").as_deref(), Some("x.txt"));
1650 assert_eq!(
1651 bash_view_target("head -n 20 x.txt").as_deref(),
1652 Some("x.txt")
1653 );
1654 assert_eq!(
1655 bash_view_target("sed -n '1,5p' x.txt").as_deref(),
1656 Some("x.txt")
1657 );
1658 assert_eq!(bash_view_target("grep foo x.txt").as_deref(), Some("x.txt"));
1659 assert_eq!(bash_view_target("cat x.txt | head -5"), None);
1662 assert_eq!(bash_view_target("cat x.txt > y.txt"), None);
1663 assert_eq!(bash_view_target("cat x.txt; rm x.txt"), None);
1664 assert_eq!(bash_view_target("cat a.txt b.txt"), None);
1665 assert_eq!(bash_view_target("echo hi"), None);
1666 assert_eq!(bash_view_target("sed 's/a/b/' x.txt"), None);
1667 assert_eq!(bash_view_target("cat $(ls)"), None);
1668 }
1669
1670 async fn one_shot_http(body: &str, content_type: &str) -> std::net::SocketAddr {
1673 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1674 let response = format!(
1675 "HTTP/1.1 200 OK\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
1676 body.len()
1677 );
1678 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1679 let addr = listener.local_addr().unwrap();
1680 tokio::spawn(async move {
1681 if let Ok((mut sock, _)) = listener.accept().await {
1682 let mut buf = [0u8; 4096];
1683 let _ = sock.read(&mut buf).await;
1684 let _ = sock.write_all(response.as_bytes()).await;
1685 let _ = sock.flush().await;
1686 }
1687 });
1688 addr
1689 }
1690
1691 static WEB_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1693
1694 #[tokio::test]
1701 async fn cc_parity_web_fetch_converts_html_to_markdown_and_caches_it() {
1702 let _guard = WEB_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1703 let cache = tmp_dir("webcache");
1704 std::env::set_var(crate::tools::WEB_CACHE_DIR_ENV, &cache);
1705
1706 let html = "<html><head><title>t</title><style>b{}</style></head><body>\
1707 <h1>Parity</h1><p>Fetched <strong>page</strong>.</p>\
1708 <a href=\"https://example.com/next\">next</a></body></html>";
1709 let addr = one_shot_http(html, "text/html; charset=utf-8").await;
1710 let url = format!("http://127.0.0.1:{}/page", addr.port());
1711
1712 let r = resolved("cc-parity");
1713 let first = run_tool(&r.config, "web_fetch", serde_json::json!({"url": url}))
1714 .await
1715 .unwrap();
1716 assert!(first.contains("markdown"), "{first}");
1717 assert!(first.contains("# Parity"), "{first}");
1718 assert!(first.contains("Fetched **page**."), "{first}");
1719 assert!(
1720 first.contains("[next](https://example.com/next)"),
1721 "{first}"
1722 );
1723 assert!(
1724 !first.contains("<h1>"),
1725 "raw markup reached the model: {first}"
1726 );
1727
1728 let second = run_tool(&r.config, "web_fetch", serde_json::json!({"url": url}))
1730 .await
1731 .expect("the second fetch must be served from the cache");
1732 assert!(second.starts_with("[web_fetch: cached "), "{second}");
1733 assert!(second.contains("# Parity"), "{second}");
1734
1735 std::env::remove_var(crate::tools::WEB_CACHE_DIR_ENV);
1736 }
1737
1738 #[tokio::test]
1747 async fn parity_presets_web_search_renders_results_from_its_backend() {
1748 let _guard = WEB_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1749 assert_eq!(
1750 crate::tools::DEFAULT_WEB_SEARCH_URL,
1751 "https://html.duckduckgo.com/html/",
1752 "the built-in backend must need no operator configuration"
1753 );
1754 let page = "<html><body><div class=\"result\">\
1755 <a class=\"result__a\" href=\"//duckduckgo.com/l/?uddg=https%3A%2F%2Fexample.com%2Fdoc\">Example doc</a>\
1756 <a class=\"result__snippet\">What the page says.</a></div></body></html>";
1757 let addr = one_shot_http(page, "text/html").await;
1758 std::env::set_var(
1759 crate::tools::WEB_SEARCH_URL_ENV,
1760 format!("http://127.0.0.1:{}/search", addr.port()),
1761 );
1762 let r = resolved("cx-parity");
1763 let out = run_tool(
1764 &r.config,
1765 "web_search",
1766 serde_json::json!({"query": "example"}),
1767 )
1768 .await
1769 .unwrap();
1770 std::env::remove_var(crate::tools::WEB_SEARCH_URL_ENV);
1771 assert!(out.contains("1 results"), "{out}");
1772 assert!(
1773 out.contains("1. Example doc — https://example.com/doc"),
1774 "{out}"
1775 );
1776 assert!(out.contains("What the page says."), "{out}");
1777 }
1778}