Skip to content

feat(approvals): render integrity-bound change previews - #405

Merged
PeterGuy326 merged 4 commits into
mainfrom
feat/approval-change-preview-contract
Sep 20, 2026
Merged

PeterGuy326 merged 4 commits into
mainfrom
feat/approval-change-preview-contract

Conversation

@Bindy-lbb

Copy link
Copy Markdown
Collaborator

Closes #400

Summary

  • add the additive approval-change-preview.v1 contract and canonical action-binding digest input
  • fail closed at CLI ingress and durable turn-store reads when a preview is malformed or bound to another action
  • project only redacted, bounded preview text into approval API views and render create/modify/delete diffs in the drawer
  • keep the honest unavailable state for engines that do not send a preview

Verification

  • npm run build
  • node --test --test-timeout=120000 apps/server/dist/test/approval.test.js apps/server/dist/test/approvals-center.test.js
  • npm run test:renderer -- approval-queue.test.tsx
  • npm run typecheck:renderer
  • npm run build:renderer

@PeterGuy326 PeterGuy326 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes — this trips the packaging invariant and CI is red.

Blocking:

  1. runtime-layout not updated. The new shared module packages/shared/src/approval-preview.ts is added to the shared barrel index.ts but dist/approval-preview.js is never added to SHARED_RUNTIME_FILES in apps/desktop/packaging/runtime-layout.cjs. package-layout.test.mjs fails ("shared runtime inventory must be updated explicitly" + "packages/shared/dist/index.js imports unpackaged dist/approval-preview.js"), and both Unpacked staging smoke jobs 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).
  2. CI is red and the signal is incomplete. Node 24 ubuntu + macos fail; because the verify chain runs test:scripts before typecheck:renderer/test:renderer, the early failure means the new approval.test.ts / approvals-center.test.ts / approval-queue.test.tsx cases 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 PeterGuy326 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 PeterGuy326 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.preview must deep-equal the deterministic redacted projection of action.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 build passes.
  • Focused approval suites: 24 passed, 1 optional engine integration skipped.
  • The complete GitHub check matrix is green.

No remaining blocking findings. Approved.

@waterbro-8 waterbro-8 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review — Approve (code, not 产品验收)

Exact head 1847c644327edaebd7aebfc3994fd6ab9993f3ce. MERGEABLE. CI 11/11 green.

PeterGuy326's blocker on 557c96a is addressed on this head:

  • hasBoundPreview recomputes approvalPreviewFingerprintInput against action.preview and rejects a mismatched fingerprint.
  • Durable valid() requires context.preview to isDeepStrictEqual the deterministic projectApprovalPreview(action.preview) (or the honest unavailable placeholder).
  • Regression test persists a forged context file list and expects store.put to 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.

@PeterGuy326
PeterGuy326 merged commit 3a9d5d2 into main Sep 20, 2026
11 checks passed
@PeterGuy326
PeterGuy326 deleted the feat/approval-change-preview-contract branch September 20, 2026 05:50
sun-970 added a commit that referenced this pull request Sep 20, 2026
Refs #306.

Keep #405 approval-preview fingerprinting and locale keys, and retain
#313 attachment store/runtime/i18n. Merge main e53ab83 into 05daf49.
sun-970 added a commit that referenced this pull request Sep 20, 2026
Refs #306.

Base the tree on current main so approval-preview.js exists, then overlay
attachment files. Keep preview fingerprinting and attachment validation.
sun-970 added a commit that referenced this pull request Sep 20, 2026
Refs #306.

Base the tree on current main (#405 + #407) so approval-preview.js and
org reparent bindings remain. Overlay attachment files. Sort
SHARED_RUNTIME_FILES so attachments.js follows approvals.js. Keep both
#406 and #306 changelog entries.
sun-970 added a commit that referenced this pull request Sep 20, 2026
The previous merge dropped turn.send's key and appended attachment
strings after the locale object closer, which broke tsc.
PeterGuy326 pushed a commit that referenced this pull request Sep 20, 2026
* 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.
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.

feat(engine,approvals): define a redacted, integrity-bound change preview contract

3 participants