Merge remote-tracking branch 'origin/master' into codex/pr202-merge-review

This commit is contained in:
Tianyi Cui
2026-07-14 23:42:36 +08:00
56 changed files with 2327 additions and 122 deletions

View File

@@ -31,6 +31,10 @@ Cooperative listeners usually mutate a shared request or decision object and the
For single-decision events, short-circuiting is the design. A policy listener can return without `next()` when it owns the decision, while a listener that only annotates or observes must delegate.
## Loader Configuration
`@cordisjs/plugin-include` parses `!!js` into expression nodes, but the Loader interpolates only an entry's `config` before mounting the plugin. Entry metadata (`id`, `name`, `group`, `disabled`, `inject`, `intercept`, and `isolate`) remains literal; `disabled: !!js ...` is therefore a truthy object that always disables the entry. Use explicit config overlays when environment selection changes which plugins are mounted.
## Practical Rules
Encapsulate behavior into plugins: a tool pipeline event belongs to `ctx.tools`, model streaming belongs to `ctx.llm`, and live agent coordination belongs to `ctx.agents`. Prefer events for interception and policy; prefer service methods for direct capability calls.

View File

@@ -0,0 +1,45 @@
# Post-mortem 0002: Filesystem snapshot tools were permanently disabled
Status: resolved
## Executive summary
The ACP example attempted to enable filesystem plugins conditionally with `disabled: !!js ...`, but Cordis evaluates JavaScript expressions only inside plugin `config`. The raw expression object was truthy, so the filesystem stack was always disabled. Snapshot refresh then accepted `UNKNOWN_TOOL` results as new goldens. The fix uses an explicit filesystem overlay and adds static-config and snapshot-result guards.
## Summary
The default ACP composition is intentionally bash-only because its sandbox cannot confine in-process filesystem providers. Filesystem snapshot scenarios still need `read`, `write`, and `edit`, so their plugins were placed in the default `cordis.yml` with a `disabled` expression intended to enable them only for full-access launches and snapshots.
Cordis Include parsed each `!!js` scalar into an expression object. The Loader recursively interpolated the plugin's `config`, but consumed entry metadata such as `disabled` directly. Every filesystem entry therefore saw a truthy object and remained disabled in every mode.
## Impact
Seven filesystem scenarios and the mixed workspace-edit scenario called tools that were absent from the registry. Their structured session logs carried `ToolNotFoundError` with code `UNKNOWN_TOOL`, while stdout rendered generic failed tool cards. The snapshot suite passed because both surfaces matched the refreshed fixtures; it proved deterministic replay of the regression rather than successful filesystem behavior.
The live confined default did not gain unintended filesystem access. A naive interpolation fix would have created that risk: permission presets update bash sandbox and approval state at runtime, but cannot mount, unmount, or confine the filesystem stack.
## Timeline
- PR #261 consolidated ACP compositions and refreshed the filesystem snapshots while introducing conditional filesystem entries.
- All unit, coverage, snapshot, documentation, build, and hygiene checks passed.
- Review of the refreshed filesystem goldens found generic failed cards and structured `UNKNOWN_TOOL` results.
- A real Loader boot confirmed that every `disabled` value remained an expression object and every filesystem fiber was absent.
## Root cause
The implementation assumed `!!js` applied to an entire Loader entry. Its actual boundary is narrower: `Entry._resolveConfig()` interpolates only `entry.options.config`; `Entry.disabled` tests `entry.options.disabled` without interpolation. The YAML tag was syntactically valid, so loading produced no diagnostic.
The snapshot framework treated any deterministic transcript as valid behavior. Header pins verified the composed tool schemas, but the filesystem scenarios shared a pin from the default composition and therefore did not independently prove that their required tools were registered. Refresh rewrote the expected stdout and session logs before any semantic assertion rejected missing tools.
## Guardrails added
- Filesystem scenarios boot `fs.cordis.yml`, an explicit fixed full-access overlay with a paired replay config and its own request-header class.
- [`AGENTS.md`](../../AGENTS.md) and the [Cordis primer](../cordis-primer.md#loader-configuration) state that `!!js` is valid only under plugin `config` and conditional composition uses overlays.
- `verify-cordis-config` parses repository Cordis YAML and rejects expression nodes in Loader entry metadata, including include patches and inserted entries.
- `dsh-acp-snapshot` rejects structured `UNKNOWN_TOOL` results in fresh runs and committed session fixtures before they can become accepted goldens.
## Lessons
- A syntactically accepted configuration value is not necessarily evaluated at that location; document and verify interpolation boundaries.
- A snapshot refresh is fixture production, not correctness review. Semantic impossibilities such as a missing registered tool need assertions independent of the golden.
- Permission controls must describe only the capabilities they actually govern. Composition-time filesystem access cannot follow a runtime bash-only preset safely.

View File

@@ -11,3 +11,4 @@ Every post-mortem opens with an **Executive summary**: one short paragraph a bus
| # | Title |
|---|---|
| [0001](0001-acp-default-export-drops-inject.md) | ACP server crashed on connect: `export default` dropped the plugin's `inject` |
| [0002](0002-js-expression-disabled-filesystem-tools.md) | Filesystem snapshot tools were permanently disabled by a literal `!!js` object |

View File

@@ -8,11 +8,11 @@ An ACP snapshot suite needs to prove the exact composed system prompt and tool-s
## Decision
Exactly one scenario per header-composition class is flagged `pinsHeader`. Its directory splits the pin by review format: `system-prompt.golden.md` contains the normalized composed prompt as ordinary Markdown, while `session.jsonl` keeps the full tool-schema list, config, and reason but stores `header.system` as `"{{system}}"`. Every other JSONL stores both the system prompt and tool list as `"{{system}}"` / `"{{tools}}"`. The pin mechanics live in [`dsh-acp-snapshot`](../../../../packages/support/acp-snapshot/README.md), whose suite factory enforces one pin per class.
Exactly one scenario per header-composition class is flagged `pinsHeader`. Its directory splits the pin by review format: `system-prompt.golden.md` contains the normalized composed prompt as ordinary Markdown, `tool-schemas.golden.json` contains the complete initial schemas and later schema edits as structured JSON, and `session.jsonl` retains config, reason, and any model-visible prefix while storing `header.system` and `header.tools` as `"{{system}}"` / `"{{tools}}"`. Every other JSONL uses the same prompt and tool tokens and also tokenizes session-prefix content. The pin mechanics live in [`dsh-acp-snapshot`](../../../../packages/support/acp-snapshot/README.md), whose suite factory enforces one pin per class.
The pure `scrubSystemPrompts` normalizer applies to every stored session fixture and tokenizes both an initial header's prompt and a header delta's inserted prompt lines. `scrubRequestHeaders` additionally tokenizes tool schemas and session-prefix content for non-pinning scenarios while retaining structural facts: system-delta positions and arity, added/removed/changed tool names, prefix message count, field presence, config, and reason. Record and refresh write-back apply the appropriate scrub before writing JSONL and regenerate the Markdown prompt from the normalized live header, so neither path can reintroduce prompt text into JSONL or leave the readable snapshot stale.
The pure `scrubSystemPrompts` and `scrubToolSchemas` normalizers apply to every stored session fixture and independently tokenize initial-header content plus header-delta bulk. `scrubRequestHeaders` also tokenizes session-prefix content for non-pinning scenarios while retaining structural facts: system-delta positions and arity, added/removed/changed tool names, prefix message count, field presence, config, and reason. Record and refresh write-back apply the appropriate scrub before writing JSONL and regenerate both sidecars from the normalized live header and deltas, so neither path can reintroduce prompt/schema bulk into JSONL or leave a review artifact stale.
Guards make the split self-enforcing. On disk, every `session*.jsonl` is a fixed point of `scrubSystemPrompts`, only non-pinning fixtures are fixed points of the full header scrub, `system-prompt.golden.md` exists exactly beside pinning fixtures, and each class has one pin. Live, every `request/header` produced by a parent, spawn child, fork child, initial request, or resume must match both halves of its class's pin after volatile-value normalization. A header without a string prompt or any `request/header-delta` fails loud because the two static pin artifacts cannot represent it.
Guards make the split self-enforcing. On disk, every `session*.jsonl` is a fixed point of both prompt and schema scrubbers, only non-pinning fixtures must be fixed points of the full header scrub, both sidecars exist exactly beside pinning fixtures in canonical newline-terminated formats, and each class has one pin. Live, every `request/header` produced by a parent, spawn child, fork child, initial request, or resume must match the reconstructed pin after volatile-value normalization; the pinning run's prompt and schema deltas must also match their sidecars. A header without a string prompt, without an array-valued tool list, or with an undeclared `request/header-delta` fails loud.
One pin covers the whole suite because every session — parent, spawn child, fork child — composes the identical tool list and the identical prompt modulo cwd, and the uniformity guard fails the suite the moment that stops holding. If header composition ever becomes session-dependent by design (a restricted subagent toolset, say), the divergent shape gets its own pinning scenario.
@@ -21,13 +21,13 @@ One pin covers the whole suite because every session — parent, spawn child, fo
- **Re-record or hand-edit every fixture per change** — preserves exact headers but buries behavioral diffs under duplicated prompt and schema content.
- **Scrub at compare time only, keeping fixtures raw** — lets compares pass while committed fixtures retain stale duplicate content and rewrite wholesale on the next recording. Stored tokens state honestly what each JSONL does not pin.
- **Scrub everywhere, pin nowhere** — loses the only end-to-end record of the composed header as actually sent (prompt assembly, registered-tool order, full schemas). The generated tool catalog documents each tool in isolation; only a real fixture pins the composed set.
- **Keep the one full pin entirely in JSONL** — removes suite-wide duplication but leaves system-prompt changes as an escaped one-line diff entangled with the tool list. Markdown gives prompt prose its natural review format without weakening the header assertion.
- **Keep the one full pin entirely in JSONL** — removes suite-wide duplication but leaves prompt and schema changes as one escaped line. Markdown and structured JSON give each surface its natural review format without weakening the reconstructed-header assertion.
- **Slim the session log itself (log a content digest, store the header elsewhere)** — violates the reconstructability contract: the product log must reproduce each request bit-for-bit ([reconstructable-requests RFC](../architecture/2026-07-05-reconstructable-requests.md)). Header bulk is a test-artifact concern, solved in test normalization; the live log is untouched.
## Verification
The suite replays every scenario against the split pins. Unit coverage exercises both scrub levels, Markdown formatting, record/refresh regeneration, normalized prompt extraction, fixed-point enforcement, required-file symmetry, header uniformity, and delta rejection.
The suite replays every scenario against the split pins. Unit coverage exercises the independent and full scrubbers, both sidecar formats, record/refresh regeneration, normalized prompt/schema extraction, fixed-point enforcement, required-file symmetry, reconstructed-header uniformity, and delta rejection.
## Consequences
A system-prompt change produces a normal line-oriented Markdown diff in one file per affected composition class; a tool-description change produces one pinned JSONL line per class; ordinary behavioral fixtures remain untouched. Session fixtures display tokens for omitted content, and the live uniformity guard makes each split pin authoritative for every session in its class. The pinning scenario carries one extra generated artifact whose terminal newline is canonicalized for repository hygiene.
A system-prompt change produces a line-oriented Markdown diff in one file per affected composition class; a tool-description change produces a structured JSON diff in one file per class; ordinary behavioral fixtures remain untouched. Session fixtures display tokens for omitted content, and the live uniformity guard makes each split pin authoritative for every session in its class. Each pinning scenario carries two generated, newline-canonicalized sidecars.