feat(approvals): render integrity-bound change previews - #405
Conversation
PeterGuy326
left a comment
There was a problem hiding this comment.
Requesting changes — this trips the packaging invariant and CI is red.
Blocking:
- runtime-layout not updated. The new shared module
packages/shared/src/approval-preview.tsis added to the shared barrelindex.tsbutdist/approval-preview.jsis never added toSHARED_RUNTIME_FILESinapps/desktop/packaging/runtime-layout.cjs.package-layout.test.mjsfails ("shared runtime inventory must be updated explicitly" + "packages/shared/dist/index.js imports unpackaged dist/approval-preview.js"), and bothUnpacked staging smokejobs die with "control plane ready line was not observed" — the packaged control plane does not boot. Add the dist entry (same pattern #313 got right). - CI is red and the signal is incomplete. Node 24 ubuntu + macos fail; because the verify chain runs
test:scriptsbeforetypecheck:renderer/test:renderer, the early failure means the newapproval.test.ts/approvals-center.test.ts/approval-queue.test.tsxcases never ran in CI — only locally. Re-run green after the fix.
Design concerns:
3. actionDigest is an unkeyed SHA-256 over the preview + action fields. It binds a preview to an action but authenticates nothing — any engine can recompute it for forged content, so "integrity-bound" over-promises against this repo's untrusted-engine threat model. Either rename to something honest (e.g. previewFingerprint) or HMAC it with a client-held secret if the goal is tamper-evidence.
4. The digest input is JSON.stringify of a rest-spread object, so it's key-order sensitive — not canonical, despite the "Stable JSON input" comment. Use a canonical/sorted serialization.
5. store.ts valid() now rejects any persisted record whose preview text isn't already redaction-idempotent, which can silently drop durable approvals. Confirm this can't discard previously-valid persisted state (or migrate it).
PeterGuy326
left a comment
There was a problem hiding this comment.
Re-review of head 557c96a: the original packaging, naming, canonicalization, legacy-store, and red-CI concerns are resolved, and the full GitHub check matrix is green. I also ran the focused server and renderer verification locally. One contract/security blocker remains.
Blocking: the displayed preview is not re-bound to the authoritative action
ApprovalStore.valid() validates action.preview and context.preview independently with isApprovalChangePreview(), but that helper only checks shape and the sha256: format. It never recomputes either fingerprint there, and never asserts that context.preview equals the redacted projection of action.preview. The renderer displays context.preview.
A persisted record with a correctly bound action.preview and a different, forged context.preview (including an arbitrary syntactically valid fingerprint) is accepted by both put() and list(), and the forged context is then returned/rendered. I reproduced this against the built PR: action says after: safe, context says before: FORGED DISPLAY, and the store accepts it. This violates issue #400's acceptance criterion that tampered or mismatched previews are rejected rather than rendered.
Please make the durable store enforce one source of truth: either do not persist the display projection and derive/redact it from validated action.preview on every public view, or recompute the action binding and require the context preview to equal the deterministic redacted projection. Add a regression test that persists mismatched action/context previews and expects approval_storage_failed.
Verification performed
- All required GitHub checks: green, including packaging and unpacked staging smoke.
npm run build: pass.- Focused server tests: 23 pass, 1 optional engine integration skipped.
- Renderer approval queue: 18/18 pass after wiring the required local design-system dependency.
typecheck:renderer: pass.build:renderer: pass.
Not ready to merge until the mismatched durable display-preview path fails closed.
PeterGuy326
left a comment
There was a problem hiding this comment.
Re-reviewed head 1847c64. The remaining durable preview-projection blocker is resolved.
Verified in code and by execution:
ApprovalStore.valid()recomputes the action preview fingerprint.context.previewmust deep-equal the deterministic redacted projection ofaction.preview.- The new persisted mismatch regression test rejects the record.
- My previous forged action/context reproducer now fails closed with
approval_storage_failed. npm run buildpasses.- Focused approval suites: 24 passed, 1 optional engine integration skipped.
- The complete GitHub check matrix is green.
No remaining blocking findings. Approved.
waterbro-8
left a comment
There was a problem hiding this comment.
Review — Approve (code, not 产品验收)
Exact head 1847c644327edaebd7aebfc3994fd6ab9993f3ce. MERGEABLE. CI 11/11 green.
PeterGuy326's blocker on 557c96a is addressed on this head:
hasBoundPreviewrecomputesapprovalPreviewFingerprintInputagainstaction.previewand rejects a mismatched fingerprint.- Durable
valid()requirescontext.previewtoisDeepStrictEqualthe deterministicprojectApprovalPreview(action.preview)(or the honest unavailable placeholder). - Regression test persists a forged context file list and expects
store.putto reject.
CLI ingress still binds the engine fingerprint; the drawer still renders context.preview only after that projection check.
Not product验收. Not auto-merge. PeterGuy326's REQUEST_CHANGES is still on 557c96a and will keep GitHub CHANGES_REQUESTED until he re-reviews this head.
Refs #306. Base the tree on current main so approval-preview.js exists, then overlay attachment files. Keep preview fingerprinting and attachment validation.
The previous merge dropped turn.send's key and appended attachment strings after the locale object closer, which broke tsc.
* feat(turns): P0 attachment support for images and PDFs (#306) Add image (PNG/JPEG/WebP) and PDF attachment support to conversation turns. Users can paste from clipboard or select files via the composer. - Shared types and validation constants (packages/shared) - Server attachment store with atomic writes under session directory - PDF text extraction via pdfjs-dist (dynamic import, graceful fallback) - Attachment upload/read HTTP endpoints + IPC bridge - Attachment context injected into engine input (Decision A2: path manifest + extracted text, no envelope schema change) - Composer UI: paperclip button, paste handler, attachment card strip - Turn history displays saved attachment cards - Additive optional `attachments` field on TurnRecord (backward compat) - i18n for zh/en, CSS for attachment strip and cards Deferred to follow-up: drag-drop, OCR, reference links, lifecycle UI. * fix(turns): bind attachments to sessions and harden storage Refs #306. Reject symlinks, require sessionStore.get on upload/read, cap PDF text/time, chunk renderer base64, refuse attachmentIds on bare /turns, and ship attachment modules in the runtime manifest. * fix(turns): sort attachment runtime inventory and changelog Keep #313 Draft. Rebase onto current main and put attachment modules in the explicit runtime allowlist in walkFiles order so package-layout CI passes. task_ref: #313 * fix(turns): import attachment constants from browser-safe subpath Value-importing @roleweave/shared pulled position-id/turns createRequire into the Vite renderer bundle and failed smoke/installer builds. task_ref: #313 * fix(turns): drop undeclared PDF extract and align attachment budgets P0 keeps PDF as a stored attachment but does not extract text (no pdfjs-dist). Engine context lists file paths only and must fit the 256 KiB input budget. Server turn creation now enforces total size. Adds validate, context, route, and cross-session tests. task_ref: #313 * fix(ui): keep #405 approval preview locale keys with attachments The previous merge dropped turn.send's key and appended attachment strings after the locale object closer, which broke tsc.
Closes #400
Summary
approval-change-preview.v1contract and canonical action-binding digest inputVerification
npm run buildnode --test --test-timeout=120000 apps/server/dist/test/approval.test.js apps/server/dist/test/approvals-center.test.jsnpm run test:renderer -- approval-queue.test.tsxnpm run typecheck:renderernpm run build:renderer