Skip to content

fix(chat-routes): redact persisted parts, surface run-failure, close manifest hole (audit P1)#201

Merged
drewstone merged 1 commit into
mainfrom
fix/audit-p1-framework
Jul 17, 2026
Merged

fix(chat-routes): redact persisted parts, surface run-failure, close manifest hole (audit P1)#201
drewstone merged 1 commit into
mainfrom
fix/audit-p1-framework

Conversation

@drewstone

Copy link
Copy Markdown
Contributor

Confirmed audit findings in the assembled chat vertical, each with a test proving the fix. New seams are optional — #194/#200 back-compat preserved.

P1

  • Persisted-parts redaction (PII-at-rest leak). transformFinalText used to touch only the scalar finalText; createSandboxChatProducer's persisted message.parts streamed straight off the raw producer, so a legal product's redaction left raw PII in the stored text parts. The transform now runs over every persisted assistant TEXT part before persistence, consistent with the scalar. Test: a redactor for SSN 123 — the persisted text PART is redacted, not just finalText.
  • Run-failure surfaced to the lifecycle. A terminal error / session.run.failed event (model 402, rate-limit) reached handleChatTurn as a clean completion, so onTurnComplete billed an empty turn and marked it done. onTurnComplete now receives failed: boolean + failureReason?, so products skip the deduct and render an error row. Test: a producer emitting a terminal error → onTurnComplete fires with failed:true + reason; a clean turn reports failed:false.
  • Browser-safe manifest hole. The manifest walked a top-level *-react readdir, missing nested browser entries and reopening the #256/#372 client-bundle-crash class. It now enumerates every browser-intended entry from package.json exports (react family + /lazy splits + pure-mechanism subpaths), explicitly covering web-react/terminal, design-canvas-react/engine, and every *-react/lazy. All enumerated entries pass the node:/agent-runtime import-graph walk. Test: enumeration + walk + a regression guard listing the nested entries.

P2

  • mergePersistedPart tool clobber. A normalized tool part always carries output/error keys (undefined when uncaptured); a plain spread overwrote a completed tool's output on a later empty update. The tool branch now overlays only defined incoming fields and never downgrades a settled tool back to running. Test: complete-then-empty-update keeps output; error-then-empty keeps the error message + status.
  • feat(chat-routes): product turn-lifecycle seams (heartbeat, beforeTurn, lifecycle, context-gate, turn-lock) #200 seams uncovered. Added a tests/vertical/ composition scenario wiring turnLock + lifecycle + contextGate over the fake-producer harness: a contextGate reject writes no assistant row and never runs the producer/lifecycle; the turnLock serializes a duplicate (refused 409, zero rows).

P3

  • withStreamHeartbeat clears its pending setTimeout in finally, so a rejected source never orphans a timer that keeps the runtime alive.
  • The terminal lifecycle hook is hoisted + idempotent and fires onTurnError (with lock release) even on a synchronous pre-stream throw — no dangling "started but unsettled" span. Test: a mocked engine throwing synchronously after onTurnStart['start','error'] + lock released.

Docs

  • CLAUDE.md/AGENTS.md /chat-routes row now lists the six seams (turnLock/contextGate/beforeTurn/lifecycle/heartbeat/onRawEvent) + transformFinalText + run-failure surfacing; ARCHITECTURE.md gains a chat-routes entry. createUploadRoute left intact.

Verification

Clean frozen-lockfile install → pnpm typecheck clean → pnpm test green (2501 passing; the one build-gated scaffolder suite passes after pnpm build) → NODE_OPTIONS=--max-old-space-size=8192 pnpm build green. Publishes on merge — legal/tax consume the redaction + run-failure fixes next.

…manifest hole (audit P1)

- transformFinalText now applies to every persisted assistant TEXT part, not
  just the scalar finalText — closes a PII-redaction-at-rest leak where
  message.parts kept raw PII the /redact transform had scrubbed from the scalar.
- onTurnComplete receives failed/failureReason derived from a terminal
  error/session.run.failed event, so products skip billing an errored turn and
  render an error row instead of marking an empty turn complete.
- browser-safe manifest is enumerated from package.json exports (react family +
  /lazy splits + pure-mechanism subpaths) instead of a top-level *-react
  readdir, covering web-react/terminal, design-canvas-react/engine, and every
  *-react/lazy — the #256/#372 client-bundle-crash blind spot.
- mergePersistedPart tool branch overlays only DEFINED incoming fields and never
  downgrades a settled tool to running, so a later empty update no longer
  clobbers captured tool output/error.
- lifecycle terminal hook is hoisted + idempotent and fires onTurnError on a
  synchronous pre-stream throw; withStreamHeartbeat clears its timer in finally
  so a rejected source never orphans a setTimeout.
- vertical suite gains a turnLock + lifecycle + contextGate composition scenario.

@tangletools tangletools left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Auto-approved drewstone PR — 1322af15

This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.

tangletools · auto-approval · reason: drewstone_author · 2026-07-17T17:40:43Z

@drewstone
drewstone merged commit ef7a313 into main Jul 17, 2026
1 check passed
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.

2 participants