Skip to content

fix: close preview, knowledge and delegation release blockers - #135

Merged
Broccolito merged 5 commits into
mainfrom
codex/fix-subagent-live-steer-retry
Aug 26, 2026
Merged

fix: close preview, knowledge and delegation release blockers#135
Broccolito merged 5 commits into
mainfrom
codex/fix-subagent-live-steer-retry

Conversation

@Broccolito

Copy link
Copy Markdown
Collaborator

Closes eleven confirmed release blockers ahead of 1.89.7, plus the defects found while fixing them. Every fix carries a regression that was demonstrated to fail without it — several lanes reverted their own change in place to prove the new test could go red.

Why this is larger than a normal patch

Three of the eleven blockers had no fix written at all when this work was picked up, and two of them destroy user data:

  • Any parseable manifest.yaml was read as legacy and deleted. Every field is #[serde(default)] with no deny_unknown_fields, so a manifest that merely lost its schema_version line defaulted to the legacy generation and the base was removed — git history included — and the deletion logged as success. No concurrency required. A base is now retired only on a positive pre-OKF signal, confirmed under the same locks that authorize the deletion.
  • A validated headless artifact path was re-opened with a plain File::open. With the system temp directory in the root set, a symlink swapped in after validation could redirect the read. Opens are now no-follow and the opened descriptor's identity is verified before anything is read.

What changed

Delegation — accepted pre-start steering is durable across every failure path. The idempotency probe no longer skips when a message has no id, which the CLI always sends, so a steer is stored once rather than twice. The initialization queue settles only on confirmed durability and is bounded.

Capability containment — a SubAgent whose delegated runtime profile is missing or invalid now fails closed on both turn paths. Previously such a row could re-acquire workspace, whose tools include workspace_open — a route out of the delegated grant set entirely. Mutation testing printed the actual escalated tool list.

Preview — decoded raster dimensions and total pixels are bounded before the renderer sees bytes; revisions are a keyed digest of content rather than size+mtime, which collide on a same-size edit inside one mtime tick; HEIC sources are measured before conversion so a bomb is decoded by no process; embedded Office media and pdf.js source images are bounded before decode rather than only at the output canvas.

Prompt injection — one shared sanitizer for untrusted labels across Rust and the renderer, dropping control, bidi and zero-width characters instead of angle brackets alone. Page-controlled title, locator and revision moved inside the untrusted envelope, so a title containing newlines can no longer forge the fields describing it, and cannot reach composer prose unsanitized.

Knowledge — one damaged unrelated base no longer prevents startup, knowledge list reports the store without altering it, and a missing knowledge directory lints as a structural error rather than a clean report.

Verification

Gate Result
cargo test -p biorouter 3,491 passed, 0 failed
cargo test -p biorouter-server 1,210 passed, 0 failed
cargo test -p biorouter-mcp 1,600 passed, 0 failed
cargo test -p biorouter-cli 366 passed, 0 failed
npm run test:run 3,725 passed, 0 failed
./scripts/clippy-lint.sh pass
just check-everything pass
just preview-panel-e2e pass, 6 screenshots

Also fixed along the way: an ingest-stream test asserted a pre-OKF page directory the fixture had moved away from, so a working digest read as writing nothing; stage_import exceeded the too_many_lines baseline and is split so the archive budget is demonstrably accumulated before anything is written; and two test-isolation faults where recycled session ids met process-global registries.

Known limitations, stated rather than implied

  • A hard link to a credential store is a regular file at a permitted path and still passes the artifact checks. Documented in code.
  • A genuine legacy base missing its schema.md is now quarantined rather than purged — data kept, migration incomplete for that base.
  • Session ids are allocated per-database while several registries are process-global; three separate lanes hit this as test flakiness. Patched locally in three places; the general fix is a repo-level change and is not in this PR.

🤖 Generated with Claude Code

Eleven confirmed release blockers, plus the defects found while fixing them.
Every fix carries a regression that was shown to fail without it.

Delegation and steering
- Accepted pre-start child steering is durable across every failure path. The
  idempotency probe no longer skips when the message has no id, which the CLI
  always sends (`session send` posts `"id": null`), so a steer is stored exactly
  once instead of twice. Identity falls back to (role, content, provenance);
  `created` is excluded because the CLI restamps it on every retry.
- The initialization queue settles only once durability is confirmed, on both
  the success and error branches, and is bounded at 64 entries with dedup that
  no longer depends on a caller-supplied turn id.
- A transient read failure no longer kills a child turn that is already
  streaming; recovery never publishes a message it failed to persist.

Capability containment
- A SubAgent session whose delegated runtime profile is missing or invalid now
  fails closed on both the injected and direct turn paths, with a typed code,
  and the partially restored agent is evicted. Previously a pre-runtime-profile
  row could re-acquire `workspace`, whose tools include `workspace_open` — a
  route out of the delegated grant set entirely.

Preview panel
- Headless artifact reads open no-follow and verify the opened descriptor's
  identity, so a symlink swapped in after validation can no longer redirect the
  read; deny checks now apply to the file actually opened.
- Decoded raster dimensions and total pixels are bounded before the renderer
  sees the bytes, and the revision is a keyed digest of content rather than
  size and mtime, which collide on a same-size edit within one mtime tick.
- HEIC sources are measured before conversion, so a bomb is decoded by no
  process; embedded Office media and pdf.js source images are bounded before
  decode rather than only at the output canvas.
- Office documents are no longer labelled trusted content.

Prompt injection
- One sanitizer for untrusted labels, shared by Rust and the renderer, dropping
  control, bidi and zero-width characters instead of angle brackets alone.
- Page-controlled title, locator and revision moved inside the untrusted
  envelope, so a title containing newlines can no longer forge the fields that
  describe it, and cannot reach composer prose unsanitized.

Knowledge
- A base is retired only on a positive pre-OKF signal confirmed under the locks
  that authorize the deletion. A manifest that merely fails to state its
  generation is kept and reported: previously any parseable YAML defaulted to
  the legacy generation and was deleted, and the deletion logged as success.
- One damaged unrelated base no longer prevents startup, and `knowledge list`
  reports the store without altering it.
- A missing knowledge directory lints as a structural error rather than a clean
  report.

Also: an ingest-stream test asserted a pre-OKF page directory the fixture had
moved away from, reporting a working digest as writing nothing; `stage_import`
is split so the archive budget is demonstrably accumulated before anything is
written; and two test-isolation faults are fixed where recycled session ids met
process-global registries.
`test (windows-latest)` failed 25 tests across four crates; `test (ubuntu-latest)`
failed a streaming decoder test. Two independent Windows causes and one Linux
test-isolation bug.

The per-KB write lock lived inside the base it guards, at
`<kb_root>/.biorouter-knowledge/write.lock`. Windows refuses to rename or remove
a directory that still holds an open handle, so every staged deletion and every
base rename failed with "Access is denied" while the lock was held — and the
lock has to be held, because the legacy purge revalidates a base's format under
exactly that lock before destroying it. The lock now lives beside the tree
rather than inside it, at `<root>/.kb-locks/<id>.lock`, so nothing is ever open
under a base root and both operations succeed with every lock still held. The
revalidation window is narrower than releasing the lock would have made it, and
a rename carries the lock with the base so the two cannot separate.

The second cause is that a contended file lock was recognised by
`ErrorKind::WouldBlock`, which only Unix produces. Windows reports contention as
ERROR_LOCK_VIOLATION, which `std` leaves uncategorised, so every contended
acquisition returned an error where it should have waited — a queued macro
failed instead of queueing, and a cancelled operation reported a lock violation
instead of cancellation. Contention is now asked of the platform.

On Linux, two decoder tests that read more than one tool_use block were absent
from the serial group guarding `BIOROUTER_TOOL_CALL_BATCHING`, so a sibling test
turning batching off process-wide could make them observe unbatched output.
Reproduced 7 times in 8 runs; 10 of 10 green once they join the group.

Bases created before this change keep an inert `write.lock` inside them, which
the ignore rules already excluded. `.kb-locks` cannot be mistaken for a base:
its name fails id validation, which is what every root scan filters on.
@Broccolito
Broccolito merged commit ff6c6cf into main Aug 26, 2026
17 checks passed
@Broccolito
Broccolito deleted the codex/fix-subagent-live-steer-retry branch August 26, 2026 18:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant