Skip to content

feat(engine): module substrate wave 3 — assistant auth, drafts API, draft-approval (HT-70) - #80

Merged
zaridan merged 5 commits into
mainfrom
feat/ht-70-drafts-approval
Jul 19, 2026
Merged

feat(engine): module substrate wave 3 — assistant auth, drafts API, draft-approval (HT-70)#80
zaridan merged 5 commits into
mainfrom
feat/ht-70-drafts-approval

Conversation

@zaridan

@zaridan zaridan commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Wave 3 — the final substrate wave (HT-70; spec §3/§6/§7): assistant principals, the drafts API, and the approval path. Rebased onto main with waves 1+2 in.

  • Assistant auth: ht_asst_<id>_<secret> tokens (SHA-256-hashed at rest — sound for server-generated 256-bit secrets; reviewer-endorsed), constant-time verification before routing, admin CRUD + rotate-token (shown once), fixed capability set enforced at ONE gate: read conversations, create drafts, create notes — nothing else, with soft-deleted indistinguishability on every assistant path.
  • Drafts API: create (draft:-scoped Idempotency-Key; reply handler now rejects draft:* caller keys closing the cross-namespace hole), review queue (keyset, soft-deleted excluded), approve-with-edits (audited; body validated identically to the reply path), discard.
  • Approval orchestration — the sacred part: token/Message-ID minted for the draft's existing thread id, §4a envelope derivation via a shared reply-headers.ts extraction (sendReply behavior provably identical — its tests unmodified), HT-32 pixel semantics, conversation locked and status re-checked INSIDE the resolve transaction (closed → reopens per the reply rule; deleted/spam → refused even under race), then the unchanged delivery worker. Fixture-asserted: an unedited approved draft's RFC 5322 wire output is byte-identical to sendReply's (both pixel configs; only the Message-ID token and the per-call MIME boundary normalized).
  • Events: draft.created/draft.resolved emitted inside the store transactions; author identity now recorded on replies/notes/resolutions via the acting-agent header (nullable degrade).
  • Wire amendments to agent-inbox-v1.md land here: ThreadView authorKind/draftStatus, widened deliveryStatus invariant, preview/threadCount draft exclusions.

Review trail (three independent layers)

  1. Sonnet-authored → Opus adversarial review: FIX-FIRST — 1 MAJOR (approve-on-closed didn't reopen; spec-mandated invariant) + 1 MINOR, both fixed.
  2. Codex independent pass (auth/threading-critical ground rule): found 2 more both prior layers missed — a TOCTOU race (conversation deleted/spammed mid-approval could still send mail) and approve-with-edits bypassing body validation. Both fixed; the race now dies inside the locked resolve transaction.
  3. Orchestrator integration: rebase conflicts union-merged (routes/dispatch/deps), cross-wave test fixtures completed.

Gates

typecheck 0 · lint 0 · test 0 — 1339 passed (full integrated suite, waves 1+2+3).

Note for reviewer: after merge this needs a MANUAL vercel deploy --prod — the project's git auto-deploy is not firing (observed on #77 and #79; see session notes). No new migrations (all DDL shipped in #77, already on prod).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added Assistant management APIs (create/list/update/disable/rotate tokens) and Assistant bearer-token authentication.
    • Implemented capability-gated access for Assistant callers.
    • Added conversation drafts endpoints (create, list, approve with edits, discard) with idempotent creation and a drafts review queue.
  • Bug Fixes
    • Conversation previews and counts now exclude unresolved/discarded drafts, while the full timeline still shows draft entries.
    • Improved handling for draft-related idempotency keys and restricted unauthorized/forbidden access.
  • Documentation
    • Updated Agent Inbox API specification for HT-70 draft and assistant credential contract changes.
  • Tests
    • Added/expanded end-to-end test coverage for Assistant auth, capability gating, and draft lifecycle flows.

zaridan and others added 4 commits July 19, 2026 09:48
…raft-approval orchestration (HT-70)

Adds the per-Assistant bearer-token credential class (ht_asst_<id>_<secret>,
constant-time verification alongside the service Bearer token), the
Assistants admin API, the drafts API (create/list/approve/discard), and the
draft-approval orchestration that mints a reply token for the draft's
existing thread, derives the envelope exactly as sendReply does, applies
HT-32 pixel injection when configured, and hands off to the unchanged
delivery path. Wires author-identity forward-carry into replies/notes and
emits draft.created/draft.resolved from the wave-1 event outbox inside the
appendDraft/resolveDraft transactions. A wire-level equivalence fixture
(src/mail/draft-equivalence.test.ts) proves assistant-draft-then-approve
produces byte-identical RFC 5322 output to sendReply, modulo the Message-ID
token, in both pixel-on and pixel-off configs — the mail-semantics
acceptance bar for this change (CHARTER.md invariant #5).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- resolveDraft's approve branch reopens a closed conversation to active
  in the same transaction (spec §6's reply-reopen invariant; pending
  stays pending, spam unreachable via the API's 409). Tests: closed
  reopens, active untouched, pending stays.
- Reply handler rejects caller Idempotency-Keys spelled draft:* (400),
  closing the cross-namespace collision; store comment now states the
  actual guarantee. Test for the 400.

Gates re-verified on this tree: typecheck 0, lint 0, test 0 (1208/1208).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… (HT-70 review)

- resolveDraft's approve branch now locks the parent conversation row
  (FOR UPDATE OF) inside the same transaction as the thread write, and
  is the AUTHORITATIVE check: 'deleted'/'spam' refuse outright (draft
  row untouched, returned as a typed sentinel the API maps to its
  existing 404/409 shapes), 'closed' still reopens. The API's own
  preflight read is now documented as a fast path only, since a
  concurrent delete/spam-mark between that read and the write could
  otherwise let mail be armed from a conversation an Agent already
  pulled out of the deliverable set. Tests simulate the race with a
  stale pre-mutation snapshot at both the approveDraft and full-API
  layers, plus store-level tests for the two new refusals.
- approve-with-edits' bodyText override now gets the same 1-5000
  length bound parseDraftBody/parseReplyBody already enforce (bodyHtml
  stays type-only, matching those same two paths). Tests: empty string
  400, oversize 400, boundary 5000 accepted.

Gates on this tree: typecheck 0, lint 0; targeted suites (conversations,
approve-draft, drafts, draft-equivalence, send) 162/162.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… (HT-70)

Rebased onto main (waves 1+2 merged): union-merged the add-add conflicts
in router/index/composition (function-ownership kept conversations.ts
conflict-free), and each wave's test fixtures gain the OTHER wave's now-
required InboxApiDeps member (dummy, never-invoked posture matching
index.test.ts's precedent). Comment paths follow the Modules vocabulary.

Gates on the integrated tree: typecheck 0, lint 0, test 0 (1339/1339).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6ace9ef0-00a6-4b07-b64a-3a719fab9767

📥 Commits

Reviewing files that changed from the base of the PR and between 0696df2 and b9dacb4.

📒 Files selected for processing (7)
  • src/api/assistant-auth.ts
  • src/api/assistants.test.ts
  • src/api/drafts.test.ts
  • src/api/index.ts
  • src/api/webhooks.test.ts
  • src/store/assistants.test.ts
  • src/store/assistants.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/api/assistant-auth.ts
  • src/api/webhooks.test.ts
  • src/api/assistants.test.ts
  • src/api/index.ts
  • src/api/drafts.test.ts

📝 Walkthrough

Walkthrough

The PR adds Assistant token authentication and admin APIs, introduces draft creation and review workflows, updates conversation/thread contracts for authorship and draft status, and makes draft persistence, approval, delivery, events, previews, and counts transactional and lifecycle-aware.

Changes

Assistant credentials and administration

Layer / File(s) Summary
Token authentication and Assistant administration
src/auth/*, src/api/assistant-auth.ts, src/api/assistants.ts, src/store/assistants.ts, src/composition/root.ts
Adds hashed Assistant bearer tokens, active-status authentication, admin CRUD and token rotation handlers, explicit Assistant IDs, and dependency wiring.

Routing and API integration

Layer / File(s) Summary
Assistant and draft routing
src/api/router.ts, src/api/index.ts, src/api/*test.ts
Adds Assistant and draft routes, caller classification, capability gates, and author identity forwarding.

Conversation and mail contracts

Layer / File(s) Summary
Thread views, authorship, and derivations
src/api/conversations.ts, src/api/cursor.ts, src/mail/reply-headers.ts, src/mail/send.ts, specs/api/agent-inbox-v1.md
Adds authorKind and draftStatus, filters unresolved/discarded drafts from previews and counts, adds draft cursors, centralizes reply-header derivation, and separates draft/reply idempotency keys.

Draft persistence and delivery

Layer / File(s) Summary
Draft review and approval flow
src/api/drafts.ts, src/mail/approve-draft.ts, src/store/conversations.ts
Adds draft creation, listing, approval, discard, edits, transactional status checks, lifecycle events, delivery failure handling, and closed-conversation reopening.

Validation

Layer / File(s) Summary
End-to-end lifecycle coverage
src/api/drafts.test.ts, src/mail/*draft*.test.ts, src/store/conversations.test.ts
Covers authentication, capability restrictions, idempotency, lifecycle transitions, race handling, event emission, delivery outcomes, and wire-level equivalence.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 67.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main additions: assistant auth, drafts API, and draft approval for HT-70.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ht-70-drafts-approval

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/api/assistant-auth.ts`:
- Around line 45-49: Update the assistant authentication flow in createInboxApi
to catch failures from the assistant-store awaits, including store.get and
store.getTokenHash, and convert them into the API’s controlled server-error
response. Preserve the existing null return for missing or inactive assistants
and ensure authentication never rejects under the function’s “Never throws”
contract.
- Around line 45-52: Update the authentication flow around assistant lookup and
token validation to fetch the Assistant’s active status and token hash
atomically in a single store query. Replace the separate store.get and
store.getTokenHash calls while preserving rejection of missing or inactive
Assistants, then compare the provided hash against the hash returned by that
combined query.

In `@src/api/assistants.test.ts`:
- Line 10: Fix the Biome formatting violations in assistants.test.ts by
organizing the Vitest imports according to repository conventions and applying
the formatter to the webhooks fixture layout around the referenced fixture
section. Make only formatting and import-order changes.

In `@src/api/drafts.test.ts`:
- Line 92: Reformat the inline webhooks object in the test fixture to Biome’s
multi-line style, including its store and queue properties while preserving the
WebhooksApiDeps satisfies constraint. Apply the same formatting change to the
webhooks object inside racyApi at the other occurrence.
- Around line 25-26: Reorder the imports in drafts.test.ts so the createInboxApi
import from ./index.js precedes the type-only WebhooksApiDeps import from
./webhooks.js, matching Biome organizeImports ordering.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a9ed2abf-f280-436a-9981-bb725b2481f8

📥 Commits

Reviewing files that changed from the base of the PR and between bcf8dba and 0696df2.

📒 Files selected for processing (28)
  • specs/api/agent-inbox-v1.md
  • src/api/agents.test.ts
  • src/api/assistant-auth.test.ts
  • src/api/assistant-auth.ts
  • src/api/assistants.test.ts
  • src/api/assistants.ts
  • src/api/conversations.ts
  • src/api/cursor.ts
  • src/api/drafts.test.ts
  • src/api/drafts.ts
  • src/api/index.test.ts
  • src/api/index.ts
  • src/api/router.test.ts
  • src/api/router.ts
  • src/api/webhooks.test.ts
  • src/auth/assistant-token.test.ts
  • src/auth/assistant-token.ts
  • src/composition/root.ts
  • src/mail/approve-draft.test.ts
  • src/mail/approve-draft.ts
  • src/mail/draft-equivalence.test.ts
  • src/mail/reply-headers.test.ts
  • src/mail/reply-headers.ts
  • src/mail/send.ts
  • src/store/assistants.test.ts
  • src/store/assistants.ts
  • src/store/conversations.test.ts
  • src/store/conversations.ts

Comment thread src/api/assistant-auth.ts Outdated
Comment on lines +45 to +49
const assistant = await store.get(parsed.assistantId)
if (assistant === null || assistant.status !== 'active') return null

const tokenHash = await store.getTokenHash(parsed.assistantId)
if (tokenHash === null) return null

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Contain Assistant-store failures.

These awaits can reject. createInboxApi performs Assistant authentication before its response-shaping try, so a database failure escapes as an uncontrolled 500 despite this function’s “Never throws” contract. Handle the failure at the API boundary as a controlled server error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/assistant-auth.ts` around lines 45 - 49, Update the assistant
authentication flow in createInboxApi to catch failures from the assistant-store
awaits, including store.get and store.getTokenHash, and convert them into the
API’s controlled server-error response. Preserve the existing null return for
missing or inactive assistants and ensure authentication never rejects under the
function’s “Never throws” contract.

Comment thread src/api/assistant-auth.ts Outdated
Comment thread src/api/assistants.test.ts
Comment thread src/api/drafts.test.ts Outdated
Comment thread src/api/drafts.test.ts Outdated
…iome fixes (HT-70 review)

CodeRabbit round 1 on #80 (5 actionables):
- getForAuth replaces getTokenHash: status + token_hash from ONE row
  read, so disable/rotation can never interleave between separate
  queries and validate stale credentials
- assistant auth's pre-try await is contained at the call site — a store
  failure returns the controlled server_error envelope (with no-store),
  never an uncontrolled host-runtime 500; new end-to-end test proves it
- biome import-order/format violations from the rebase integration
  patch fixed (the Quality gate failure; local lint had been misread
  through a piped exit code — re-verified bare)

Gates: typecheck 0, lint 0, test 0 (1340/1340).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@zaridan
zaridan merged commit 475b60e into main Jul 19, 2026
5 checks passed
@zaridan
zaridan deleted the feat/ht-70-drafts-approval branch July 19, 2026 17:28
zaridan added a commit that referenced this pull request Jul 19, 2026
Adds docs/modules/{README,webhooks,assistants-and-drafts}.md, written and
verified against the shipped code rather than the spec alone: curl examples
cross-checked against src/api/router.ts's actual patterns and validation
(src/api/webhooks.ts, src/store/webhook-endpoints.ts), and the signature
sample verified byte-for-byte against both the engine's signer
(src/webhooks/delivery.ts) and the reference module's verifier
(module-draft-assistant/src/verify.ts) via a throwaway script (exit 0).

Assistants/drafts sections document PR #80 (HT-70), not yet on main —
flagged at the top of assistants-and-drafts.md; merge this after #80.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
zaridan added a commit that referenced this pull request Jul 19, 2026
- assistants-and-drafts.md: drop the pre-merge branch/PR-#80 status note
  now that #80 is merged and deployed; content documents merged main.
- assistants-and-drafts.md: stop claiming every Assistant write is a
  draft — notes (POST .../notes) are direct writes with no draft/
  approval step, only customer-facing replies are draft-gated. Verified
  against handlePostNote (src/api/conversations.ts on merged main).
- README.md: "draft-writing Assistant", not "draft-writing AI" — vocabulary
  rule (Assistants, never generic "AI").
- webhooks.md: the signature-verification sample now requires an exact
  64-hex-char match before Buffer.from(..., 'hex') decoding — that call
  otherwise silently truncates at the first non-hex character instead of
  rejecting trailing garbage after a valid-length prefix. Re-verified with
  an extended throwaway script (adds the trailing-garbage negative case
  to the existing engine-signer/reference-module cross-check); exit 0.
- webhooks.md: split the delivery-guarantees table's "anything else
  retries" row — an SsrfRefusedError dead-letters immediately on the
  first attempt and is never retried, unlike an ordinary HTTP failure/
  timeout/connection error. Verified against src/webhooks/delivery.ts's
  catch branch on merged main.

Rebased onto origin/main (PR #80 merged as 475b60e) so both the docs and
the verification below reflect merged main, not the pre-merge branch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
zaridan added a commit that referenced this pull request Jul 19, 2026
* docs(modules): operator guide for the module substrate (HT-71)

Adds docs/modules/{README,webhooks,assistants-and-drafts}.md, written and
verified against the shipped code rather than the spec alone: curl examples
cross-checked against src/api/router.ts's actual patterns and validation
(src/api/webhooks.ts, src/store/webhook-endpoints.ts), and the signature
sample verified byte-for-byte against both the engine's signer
(src/webhooks/delivery.ts) and the reference module's verifier
(module-draft-assistant/src/verify.ts) via a throwaway script (exit 0).

Assistants/drafts sections document PR #80 (HT-70), not yet on main —
flagged at the top of assistants-and-drafts.md; merge this after #80.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(modules): address CodeRabbit review on PR #81

- assistants-and-drafts.md: drop the pre-merge branch/PR-#80 status note
  now that #80 is merged and deployed; content documents merged main.
- assistants-and-drafts.md: stop claiming every Assistant write is a
  draft — notes (POST .../notes) are direct writes with no draft/
  approval step, only customer-facing replies are draft-gated. Verified
  against handlePostNote (src/api/conversations.ts on merged main).
- README.md: "draft-writing Assistant", not "draft-writing AI" — vocabulary
  rule (Assistants, never generic "AI").
- webhooks.md: the signature-verification sample now requires an exact
  64-hex-char match before Buffer.from(..., 'hex') decoding — that call
  otherwise silently truncates at the first non-hex character instead of
  rejecting trailing garbage after a valid-length prefix. Re-verified with
  an extended throwaway script (adds the trailing-garbage negative case
  to the existing engine-signer/reference-module cross-check); exit 0.
- webhooks.md: split the delivery-guarantees table's "anything else
  retries" row — an SsrfRefusedError dead-letters immediately on the
  first attempt and is never retried, unlike an ordinary HTTP failure/
  timeout/connection error. Verified against src/webhooks/delivery.ts's
  catch branch on merged main.

Rebased onto origin/main (PR #80 merged as 475b60e) so both the docs and
the verification below reflect merged main, not the pre-merge branch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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