@vellumai/assistant 0.3.27 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/ARCHITECTURE.md +81 -4
  2. package/Dockerfile +2 -2
  3. package/bun.lock +4 -1
  4. package/docs/trusted-contact-access.md +9 -2
  5. package/package.json +6 -3
  6. package/scripts/ipc/generate-swift.ts +9 -5
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
  8. package/src/__tests__/agent-loop-thinking.test.ts +1 -1
  9. package/src/__tests__/agent-loop.test.ts +119 -0
  10. package/src/__tests__/approval-routes-http.test.ts +13 -5
  11. package/src/__tests__/asset-materialize-tool.test.ts +2 -0
  12. package/src/__tests__/asset-search-tool.test.ts +2 -0
  13. package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
  14. package/src/__tests__/attachments-store.test.ts +2 -0
  15. package/src/__tests__/browser-skill-endstate.test.ts +3 -3
  16. package/src/__tests__/bundled-asset.test.ts +107 -0
  17. package/src/__tests__/call-controller.test.ts +30 -29
  18. package/src/__tests__/call-routes-http.test.ts +34 -32
  19. package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
  20. package/src/__tests__/canonical-guardian-store.test.ts +636 -0
  21. package/src/__tests__/channel-approval-routes.test.ts +174 -1
  22. package/src/__tests__/channel-invite-transport.test.ts +6 -6
  23. package/src/__tests__/channel-reply-delivery.test.ts +19 -0
  24. package/src/__tests__/channel-retry-sweep.test.ts +130 -0
  25. package/src/__tests__/clarification-resolver.test.ts +2 -0
  26. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  27. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  28. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
  29. package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
  30. package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
  31. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
  32. package/src/__tests__/config-schema.test.ts +5 -5
  33. package/src/__tests__/config-watcher.test.ts +3 -1
  34. package/src/__tests__/connection-policy.test.ts +14 -5
  35. package/src/__tests__/contacts-tools.test.ts +3 -1
  36. package/src/__tests__/contradiction-checker.test.ts +2 -0
  37. package/src/__tests__/conversation-pairing.test.ts +10 -0
  38. package/src/__tests__/conversation-routes.test.ts +1 -1
  39. package/src/__tests__/credential-security-invariants.test.ts +16 -6
  40. package/src/__tests__/credential-vault-unit.test.ts +2 -2
  41. package/src/__tests__/credential-vault.test.ts +5 -4
  42. package/src/__tests__/daemon-lifecycle.test.ts +9 -0
  43. package/src/__tests__/daemon-server-session-init.test.ts +27 -0
  44. package/src/__tests__/elevenlabs-config.test.ts +2 -0
  45. package/src/__tests__/emit-signal-routing-intent.test.ts +43 -1
  46. package/src/__tests__/encrypted-store.test.ts +10 -5
  47. package/src/__tests__/followup-tools.test.ts +3 -1
  48. package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
  49. package/src/__tests__/gmail-integration.test.ts +0 -1
  50. package/src/__tests__/guardian-actions-endpoint.test.ts +205 -345
  51. package/src/__tests__/guardian-control-plane-policy.test.ts +19 -19
  52. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +599 -0
  53. package/src/__tests__/guardian-dispatch.test.ts +21 -19
  54. package/src/__tests__/guardian-grant-minting.test.ts +68 -1
  55. package/src/__tests__/guardian-outbound-http.test.ts +12 -9
  56. package/src/__tests__/guardian-routing-invariants.test.ts +1092 -0
  57. package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
  58. package/src/__tests__/handlers-slack-config.test.ts +3 -1
  59. package/src/__tests__/handlers-telegram-config.test.ts +3 -1
  60. package/src/__tests__/handlers-twilio-config.test.ts +3 -1
  61. package/src/__tests__/handlers-twitter-config.test.ts +3 -1
  62. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
  63. package/src/__tests__/heartbeat-service.test.ts +20 -0
  64. package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
  65. package/src/__tests__/ingress-reconcile.test.ts +3 -1
  66. package/src/__tests__/ingress-routes-http.test.ts +231 -4
  67. package/src/__tests__/intent-routing.test.ts +2 -0
  68. package/src/__tests__/ipc-snapshot.test.ts +13 -0
  69. package/src/__tests__/mcp-cli.test.ts +77 -0
  70. package/src/__tests__/media-generate-image.test.ts +21 -0
  71. package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
  72. package/src/__tests__/memory-regressions.test.ts +20 -20
  73. package/src/__tests__/non-member-access-request.test.ts +212 -36
  74. package/src/__tests__/notification-decision-fallback.test.ts +63 -3
  75. package/src/__tests__/notification-decision-strategy.test.ts +78 -0
  76. package/src/__tests__/notification-guardian-path.test.ts +15 -15
  77. package/src/__tests__/oauth-connect-handler.test.ts +3 -1
  78. package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
  79. package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
  80. package/src/__tests__/onboarding-template-contract.test.ts +116 -21
  81. package/src/__tests__/pairing-routes.test.ts +171 -0
  82. package/src/__tests__/playbook-execution.test.ts +3 -1
  83. package/src/__tests__/playbook-tools.test.ts +3 -1
  84. package/src/__tests__/provider-error-scenarios.test.ts +59 -8
  85. package/src/__tests__/proxy-approval-callback.test.ts +2 -0
  86. package/src/__tests__/recording-handler.test.ts +11 -0
  87. package/src/__tests__/recording-intent-handler.test.ts +15 -0
  88. package/src/__tests__/recording-state-machine.test.ts +13 -2
  89. package/src/__tests__/registry.test.ts +7 -3
  90. package/src/__tests__/relay-server.test.ts +148 -28
  91. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
  92. package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
  93. package/src/__tests__/runtime-events-sse.test.ts +4 -2
  94. package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
  95. package/src/__tests__/schedule-tools.test.ts +3 -1
  96. package/src/__tests__/secret-scanner-executor.test.ts +59 -0
  97. package/src/__tests__/secret-scanner.test.ts +8 -0
  98. package/src/__tests__/send-endpoint-busy.test.ts +4 -0
  99. package/src/__tests__/sensitive-output-placeholders.test.ts +208 -0
  100. package/src/__tests__/session-abort-tool-results.test.ts +23 -0
  101. package/src/__tests__/session-agent-loop.test.ts +16 -0
  102. package/src/__tests__/session-conflict-gate.test.ts +21 -0
  103. package/src/__tests__/session-load-history-repair.test.ts +27 -17
  104. package/src/__tests__/session-pre-run-repair.test.ts +23 -0
  105. package/src/__tests__/session-profile-injection.test.ts +21 -0
  106. package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
  107. package/src/__tests__/session-queue.test.ts +23 -0
  108. package/src/__tests__/session-runtime-assembly.test.ts +126 -59
  109. package/src/__tests__/session-skill-tools.test.ts +27 -5
  110. package/src/__tests__/session-slash-known.test.ts +23 -0
  111. package/src/__tests__/session-slash-queue.test.ts +23 -0
  112. package/src/__tests__/session-slash-unknown.test.ts +23 -0
  113. package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
  114. package/src/__tests__/session-workspace-injection.test.ts +21 -0
  115. package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
  116. package/src/__tests__/shell-credential-ref.test.ts +2 -0
  117. package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
  118. package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
  119. package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
  120. package/src/__tests__/skills.test.ts +8 -4
  121. package/src/__tests__/slack-channel-config.test.ts +3 -1
  122. package/src/__tests__/subagent-tools.test.ts +19 -0
  123. package/src/__tests__/swarm-recursion.test.ts +2 -0
  124. package/src/__tests__/swarm-session-integration.test.ts +2 -0
  125. package/src/__tests__/swarm-tool.test.ts +2 -0
  126. package/src/__tests__/system-prompt.test.ts +3 -1
  127. package/src/__tests__/task-compiler.test.ts +3 -1
  128. package/src/__tests__/task-management-tools.test.ts +3 -1
  129. package/src/__tests__/task-tools.test.ts +3 -1
  130. package/src/__tests__/terminal-sandbox.test.ts +13 -12
  131. package/src/__tests__/terminal-tools.test.ts +2 -0
  132. package/src/__tests__/tool-approval-handler.test.ts +15 -15
  133. package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
  134. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
  135. package/src/__tests__/tool-grant-request-escalation.test.ts +497 -0
  136. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
  137. package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
  138. package/src/__tests__/trusted-contact-verification.test.ts +91 -0
  139. package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
  140. package/src/__tests__/twitter-auth-handler.test.ts +3 -1
  141. package/src/__tests__/twitter-cli-routing.test.ts +3 -1
  142. package/src/__tests__/view-image-tool.test.ts +3 -1
  143. package/src/__tests__/voice-invite-redemption.test.ts +329 -0
  144. package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
  145. package/src/__tests__/voice-session-bridge.test.ts +10 -10
  146. package/src/__tests__/work-item-output.test.ts +3 -1
  147. package/src/__tests__/workspace-lifecycle.test.ts +13 -2
  148. package/src/agent/loop.ts +46 -3
  149. package/src/approvals/guardian-decision-primitive.ts +285 -0
  150. package/src/approvals/guardian-request-resolvers.ts +539 -0
  151. package/src/calls/call-controller.ts +26 -23
  152. package/src/calls/guardian-action-sweep.ts +10 -2
  153. package/src/calls/guardian-dispatch.ts +46 -40
  154. package/src/calls/relay-server.ts +358 -24
  155. package/src/calls/types.ts +1 -1
  156. package/src/calls/voice-session-bridge.ts +3 -3
  157. package/src/cli.ts +12 -0
  158. package/src/config/agent-schema.ts +14 -3
  159. package/src/config/calls-schema.ts +6 -6
  160. package/src/config/core-schema.ts +3 -3
  161. package/src/config/feature-flag-registry.json +8 -0
  162. package/src/config/mcp-schema.ts +1 -1
  163. package/src/config/memory-schema.ts +27 -19
  164. package/src/config/schema.ts +21 -21
  165. package/src/config/skills-schema.ts +7 -7
  166. package/src/config/system-prompt.ts +2 -1
  167. package/src/config/templates/BOOTSTRAP.md +47 -31
  168. package/src/config/templates/USER.md +5 -0
  169. package/src/config/update-bulletin-template-path.ts +4 -1
  170. package/src/config/vellum-skills/trusted-contacts/SKILL.md +149 -21
  171. package/src/daemon/handlers/config-inbox.ts +4 -4
  172. package/src/daemon/handlers/guardian-actions.ts +45 -66
  173. package/src/daemon/handlers/sessions.ts +148 -4
  174. package/src/daemon/ipc-contract/guardian-actions.ts +7 -0
  175. package/src/daemon/ipc-contract/messages.ts +16 -0
  176. package/src/daemon/ipc-contract-inventory.json +1 -0
  177. package/src/daemon/lifecycle.ts +22 -16
  178. package/src/daemon/pairing-store.ts +86 -3
  179. package/src/daemon/server.ts +18 -0
  180. package/src/daemon/session-agent-loop-handlers.ts +5 -4
  181. package/src/daemon/session-agent-loop.ts +33 -6
  182. package/src/daemon/session-lifecycle.ts +25 -17
  183. package/src/daemon/session-memory.ts +2 -2
  184. package/src/daemon/session-process.ts +68 -326
  185. package/src/daemon/session-runtime-assembly.ts +119 -25
  186. package/src/daemon/session-tool-setup.ts +3 -2
  187. package/src/daemon/session.ts +4 -3
  188. package/src/home-base/prebuilt/seed.ts +2 -1
  189. package/src/hooks/templates.ts +2 -1
  190. package/src/memory/canonical-guardian-store.ts +586 -0
  191. package/src/memory/channel-guardian-store.ts +2 -0
  192. package/src/memory/conversation-crud.ts +7 -7
  193. package/src/memory/db-init.ts +20 -0
  194. package/src/memory/embedding-local.ts +257 -39
  195. package/src/memory/embedding-runtime-manager.ts +471 -0
  196. package/src/memory/guardian-action-store.ts +7 -60
  197. package/src/memory/guardian-approvals.ts +9 -4
  198. package/src/memory/guardian-bindings.ts +25 -1
  199. package/src/memory/indexer.ts +3 -3
  200. package/src/memory/ingress-invite-store.ts +45 -0
  201. package/src/memory/job-handlers/backfill.ts +16 -9
  202. package/src/memory/migrations/036-normalize-phone-identities.ts +289 -0
  203. package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
  204. package/src/memory/migrations/118-reminder-routing-intent.ts +3 -3
  205. package/src/memory/migrations/121-canonical-guardian-requests.ts +59 -0
  206. package/src/memory/migrations/122-canonical-guardian-requester-chat-id.ts +15 -0
  207. package/src/memory/migrations/123-canonical-guardian-deliveries-destination-index.ts +15 -0
  208. package/src/memory/migrations/index.ts +5 -0
  209. package/src/memory/migrations/registry.ts +5 -0
  210. package/src/memory/qdrant-client.ts +31 -22
  211. package/src/memory/schema-migration.ts +1 -0
  212. package/src/memory/schema.ts +56 -0
  213. package/src/notifications/copy-composer.ts +31 -4
  214. package/src/notifications/decision-engine.ts +57 -0
  215. package/src/permissions/defaults.ts +2 -0
  216. package/src/runtime/access-request-helper.ts +173 -0
  217. package/src/runtime/actor-trust-resolver.ts +221 -0
  218. package/src/runtime/channel-guardian-service.ts +12 -4
  219. package/src/runtime/channel-invite-transports/voice.ts +58 -0
  220. package/src/runtime/channel-retry-sweep.ts +18 -6
  221. package/src/runtime/guardian-context-resolver.ts +38 -71
  222. package/src/runtime/guardian-decision-types.ts +6 -0
  223. package/src/runtime/guardian-reply-router.ts +717 -0
  224. package/src/runtime/http-server.ts +8 -0
  225. package/src/runtime/ingress-service.ts +80 -3
  226. package/src/runtime/invite-redemption-service.ts +141 -2
  227. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +116 -0
  228. package/src/runtime/routes/channel-route-shared.ts +1 -1
  229. package/src/runtime/routes/channel-routes.ts +1 -1
  230. package/src/runtime/routes/conversation-routes.ts +20 -2
  231. package/src/runtime/routes/guardian-action-routes.ts +100 -109
  232. package/src/runtime/routes/guardian-approval-interception.ts +17 -6
  233. package/src/runtime/routes/inbound-message-handler.ts +205 -529
  234. package/src/runtime/routes/ingress-routes.ts +52 -4
  235. package/src/runtime/routes/pairing-routes.ts +3 -0
  236. package/src/runtime/tool-grant-request-helper.ts +195 -0
  237. package/src/tools/executor.ts +13 -1
  238. package/src/tools/guardian-control-plane-policy.ts +2 -2
  239. package/src/tools/sensitive-output-placeholders.ts +203 -0
  240. package/src/tools/tool-approval-handler.ts +53 -10
  241. package/src/tools/types.ts +13 -2
  242. package/src/util/bundled-asset.ts +31 -0
  243. package/src/util/canonicalize-identity.ts +52 -0
  244. package/src/util/logger.ts +20 -8
  245. package/src/util/platform.ts +10 -0
  246. package/src/util/voice-code.ts +29 -0
  247. package/src/daemon/guardian-invite-intent.ts +0 -124
@@ -42,6 +42,8 @@ mock.module('../memory/channel-guardian-store.js', () => ({
42
42
 
43
43
  mock.module('../config/loader.js', () => ({
44
44
  getConfig: () => ({
45
+ ui: {},
46
+
45
47
  calls: {
46
48
  userConsultTimeoutSeconds: 120,
47
49
  },
@@ -103,6 +105,8 @@ function ensureConversation(id: string): void {
103
105
 
104
106
  function resetTables(): void {
105
107
  const db = getDb();
108
+ db.run('DELETE FROM canonical_guardian_deliveries');
109
+ db.run('DELETE FROM canonical_guardian_requests');
106
110
  db.run('DELETE FROM guardian_action_deliveries');
107
111
  db.run('DELETE FROM guardian_action_requests');
108
112
  db.run('DELETE FROM call_pending_questions');
@@ -160,7 +164,7 @@ describe('guardian-dispatch', () => {
160
164
 
161
165
  const db = getDb();
162
166
  const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
163
- const request = raw.query('SELECT * FROM guardian_action_requests WHERE call_session_id = ?').get(session.id) as
167
+ const request = raw.query('SELECT * FROM canonical_guardian_requests WHERE call_session_id = ?').get(session.id) as
164
168
  | { id: string; status: string; question_text: string }
165
169
  | undefined;
166
170
  expect(request).toBeDefined();
@@ -168,7 +172,7 @@ describe('guardian-dispatch', () => {
168
172
  expect(request!.question_text).toBe('What is the gate code?');
169
173
 
170
174
  const vellumDelivery = raw.query(
171
- 'SELECT * FROM guardian_action_deliveries WHERE request_id = ? AND destination_channel = ?',
175
+ 'SELECT * FROM canonical_guardian_deliveries WHERE request_id = ? AND destination_channel = ?',
172
176
  ).get(request!.id, 'vellum') as { status: string; destination_conversation_id: string | null } | undefined;
173
177
  expect(vellumDelivery).toBeDefined();
174
178
  expect(vellumDelivery!.status).toBe('sent');
@@ -224,18 +228,17 @@ describe('guardian-dispatch', () => {
224
228
 
225
229
  const db = getDb();
226
230
  const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
227
- const request = raw.query('SELECT * FROM guardian_action_requests WHERE call_session_id = ?').get(session.id) as
231
+ const request = raw.query('SELECT * FROM canonical_guardian_requests WHERE call_session_id = ?').get(session.id) as
228
232
  | { id: string }
229
233
  | undefined;
230
234
  const telegramDelivery = raw.query(
231
- 'SELECT * FROM guardian_action_deliveries WHERE request_id = ? AND destination_channel = ?',
235
+ 'SELECT * FROM canonical_guardian_deliveries WHERE request_id = ? AND destination_channel = ?',
232
236
  ).get(request!.id, 'telegram') as
233
- | { status: string; destination_chat_id: string | null; destination_external_user_id: string | null }
237
+ | { status: string; destination_chat_id: string | null }
234
238
  | undefined;
235
239
  expect(telegramDelivery).toBeDefined();
236
240
  expect(telegramDelivery!.status).toBe('sent');
237
241
  expect(telegramDelivery!.destination_chat_id).toBe('tg-chat-999');
238
- expect(telegramDelivery!.destination_external_user_id).toBe('tg-user-888');
239
242
  });
240
243
 
241
244
  test('marks non-sent pipeline delivery results as failed', async () => {
@@ -275,15 +278,14 @@ describe('guardian-dispatch', () => {
275
278
 
276
279
  const db = getDb();
277
280
  const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
278
- const request = raw.query('SELECT * FROM guardian_action_requests WHERE call_session_id = ?').get(session.id) as
281
+ const request = raw.query('SELECT * FROM canonical_guardian_requests WHERE call_session_id = ?').get(session.id) as
279
282
  | { id: string }
280
283
  | undefined;
281
284
  const vellumDelivery = raw.query(
282
- 'SELECT * FROM guardian_action_deliveries WHERE request_id = ? AND destination_channel = ?',
283
- ).get(request!.id, 'vellum') as { status: string; last_error: string | null } | undefined;
285
+ 'SELECT * FROM canonical_guardian_deliveries WHERE request_id = ? AND destination_channel = ?',
286
+ ).get(request!.id, 'vellum') as { status: string } | undefined;
284
287
  expect(vellumDelivery).toBeDefined();
285
288
  expect(vellumDelivery!.status).toBe('failed');
286
- expect(vellumDelivery!.last_error).toContain('IPC unavailable');
287
289
  });
288
290
 
289
291
  test('uses onThreadCreated callback conversation when delivery result omits conversationId', async () => {
@@ -326,17 +328,17 @@ describe('guardian-dispatch', () => {
326
328
 
327
329
  const db = getDb();
328
330
  const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
329
- const request = raw.query('SELECT * FROM guardian_action_requests WHERE call_session_id = ?').get(session.id) as
331
+ const request = raw.query('SELECT * FROM canonical_guardian_requests WHERE call_session_id = ?').get(session.id) as
330
332
  | { id: string }
331
333
  | undefined;
332
334
  const vellumDelivery = raw.query(
333
- 'SELECT * FROM guardian_action_deliveries WHERE request_id = ? AND destination_channel = ?',
335
+ 'SELECT * FROM canonical_guardian_deliveries WHERE request_id = ? AND destination_channel = ?',
334
336
  ).get(request!.id, 'vellum') as { destination_conversation_id: string | null } | undefined;
335
337
  expect(vellumDelivery).toBeDefined();
336
338
  expect(vellumDelivery!.destination_conversation_id).toBe('conv-from-thread-created');
337
339
  });
338
340
 
339
- test('persists toolName and inputDigest on guardian action request for tool-approval dispatches', async () => {
341
+ test('persists toolName and inputDigest on canonical guardian request for tool-approval dispatches', async () => {
340
342
  const convId = 'conv-dispatch-5';
341
343
  ensureConversation(convId);
342
344
 
@@ -359,7 +361,7 @@ describe('guardian-dispatch', () => {
359
361
 
360
362
  const db = getDb();
361
363
  const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
362
- const request = raw.query('SELECT * FROM guardian_action_requests WHERE call_session_id = ?').get(session.id) as
364
+ const request = raw.query('SELECT * FROM canonical_guardian_requests WHERE call_session_id = ?').get(session.id) as
363
365
  | { id: string; tool_name: string | null; input_digest: string | null }
364
366
  | undefined;
365
367
  expect(request).toBeDefined();
@@ -388,7 +390,7 @@ describe('guardian-dispatch', () => {
388
390
 
389
391
  const db = getDb();
390
392
  const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
391
- const request = raw.query('SELECT * FROM guardian_action_requests WHERE call_session_id = ?').get(session.id) as
393
+ const request = raw.query('SELECT * FROM canonical_guardian_requests WHERE call_session_id = ?').get(session.id) as
392
394
  | { id: string; tool_name: string | null; input_digest: string | null }
393
395
  | undefined;
394
396
  expect(request).toBeDefined();
@@ -485,11 +487,11 @@ describe('guardian-dispatch', () => {
485
487
  pendingQuestion: pq2,
486
488
  });
487
489
 
488
- // Both dispatches should have created separate action requests
490
+ // Both dispatches should have created separate canonical requests
489
491
  const db = getDb();
490
492
  const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
491
493
  const requests = raw.query(
492
- 'SELECT * FROM guardian_action_requests WHERE call_session_id = ? ORDER BY created_at ASC',
494
+ 'SELECT * FROM canonical_guardian_requests WHERE call_session_id = ? ORDER BY created_at ASC',
493
495
  ).all(session.id) as Array<{ id: string; question_text: string }>;
494
496
  expect(requests).toHaveLength(2);
495
497
  expect(requests[0].question_text).toBe('What is the gate code?');
@@ -498,7 +500,7 @@ describe('guardian-dispatch', () => {
498
500
  // Each request should have its own delivery row, both pointing to the shared conversation
499
501
  for (const req of requests) {
500
502
  const delivery = raw.query(
501
- 'SELECT * FROM guardian_action_deliveries WHERE request_id = ? AND destination_channel = ?',
503
+ 'SELECT * FROM canonical_guardian_deliveries WHERE request_id = ? AND destination_channel = ?',
502
504
  ).get(req.id, 'vellum') as { status: string; destination_conversation_id: string | null } | undefined;
503
505
  expect(delivery).toBeDefined();
504
506
  expect(delivery!.status).toBe('sent');
@@ -507,7 +509,7 @@ describe('guardian-dispatch', () => {
507
509
 
508
510
  // Total delivery rows should be 2 (one per request), not 1
509
511
  const allDeliveries = raw.query(
510
- 'SELECT * FROM guardian_action_deliveries WHERE destination_conversation_id = ?',
512
+ 'SELECT * FROM canonical_guardian_deliveries WHERE destination_conversation_id = ?',
511
513
  ).all(sharedConversationId) as Array<{ request_id: string }>;
512
514
  expect(allDeliveries).toHaveLength(2);
513
515
 
@@ -137,7 +137,7 @@ function registerPendingInteraction(
137
137
 
138
138
  function makeGuardianContext(): GuardianContext {
139
139
  return {
140
- actorRole: 'guardian',
140
+ trustClass: 'guardian',
141
141
  denialReason: undefined,
142
142
  };
143
143
  }
@@ -530,3 +530,70 @@ describe('guardian grant minting on tool-approval decisions', () => {
530
530
  composeSpy.mockRestore();
531
531
  });
532
532
  });
533
+
534
+ describe('approval interception trust-class regression coverage', () => {
535
+ let deliverSpy: ReturnType<typeof spyOn>;
536
+ let composeSpy: ReturnType<typeof spyOn>;
537
+
538
+ beforeEach(() => {
539
+ resetTables();
540
+ deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
541
+ composeSpy = spyOn(approvalMessageComposer, 'composeApprovalMessageGenerative')
542
+ .mockResolvedValue('test message');
543
+ });
544
+
545
+ test('identity-known unknown sender does not auto-deny pending approval', async () => {
546
+ const requestId = 'req-unknown-no-auto-deny-1';
547
+ const sessionMock = registerPendingInteraction(requestId, CONVERSATION_ID, TOOL_NAME, TOOL_INPUT);
548
+ createTestGuardianApproval(requestId);
549
+
550
+ const result = await handleApprovalInterception({
551
+ conversationId: CONVERSATION_ID,
552
+ content: 'approve',
553
+ externalChatId: REQUESTER_CHAT,
554
+ sourceChannel: 'telegram',
555
+ senderExternalUserId: 'intruder-user-1',
556
+ replyCallbackUrl: 'https://gateway.test/deliver',
557
+ guardianCtx: {
558
+ trustClass: 'unknown',
559
+ },
560
+ assistantId: ASSISTANT_ID,
561
+ });
562
+
563
+ expect(result.handled).toBe(true);
564
+ expect(result.type).toBe('assistant_turn');
565
+ expect(sessionMock).not.toHaveBeenCalled();
566
+
567
+ deliverSpy.mockRestore();
568
+ composeSpy.mockRestore();
569
+ });
570
+
571
+ test('legacy unverified sender still auto-denies pending approval', async () => {
572
+ const requestId = 'req-unknown-auto-deny-1';
573
+ const sessionMock = registerPendingInteraction(requestId, CONVERSATION_ID, TOOL_NAME, TOOL_INPUT);
574
+ createTestGuardianApproval(requestId);
575
+
576
+ const result = await handleApprovalInterception({
577
+ conversationId: CONVERSATION_ID,
578
+ content: 'approve',
579
+ externalChatId: REQUESTER_CHAT,
580
+ sourceChannel: 'telegram',
581
+ senderExternalUserId: undefined,
582
+ replyCallbackUrl: 'https://gateway.test/deliver',
583
+ guardianCtx: {
584
+ trustClass: 'unknown',
585
+ denialReason: 'no_identity',
586
+ },
587
+ assistantId: ASSISTANT_ID,
588
+ });
589
+
590
+ expect(result.handled).toBe(true);
591
+ expect(result.type).toBe('decision_applied');
592
+ expect(sessionMock).toHaveBeenCalled();
593
+ expect(sessionMock.mock.calls[0]?.[0]).toBe(requestId);
594
+ expect(sessionMock.mock.calls[0]?.[1]).toBe('deny');
595
+
596
+ deliverSpy.mockRestore();
597
+ composeSpy.mockRestore();
598
+ });
599
+ });
@@ -364,15 +364,16 @@ describe('HTTP route: handleStartOutbound', () => {
364
364
  const req = jsonRequest({ destination: '+15551234567' });
365
365
  const resp = await handleStartOutbound(req);
366
366
  expect(resp.status).toBe(400);
367
- const body = await resp.json() as Record<string, unknown>;
368
- expect(body.error).toBe('missing_channel');
367
+ const body = await resp.json() as { error: { message: string; code: string } };
368
+ expect(body.error.code).toBe('BAD_REQUEST');
369
+ expect(body.error.message).toContain('channel');
369
370
  });
370
371
 
371
372
  test('returns 400 for missing destination (SMS)', async () => {
372
373
  const req = jsonRequest({ channel: 'sms' });
373
374
  const resp = await handleStartOutbound(req);
374
375
  expect(resp.status).toBe(400);
375
- const body = await resp.json() as Record<string, unknown>;
376
+ const body = await resp.json() as { error?: string };
376
377
  expect(body.error).toBe('missing_destination');
377
378
  });
378
379
 
@@ -391,15 +392,16 @@ describe('HTTP route: handleResendOutbound', () => {
391
392
  const req = jsonRequest({});
392
393
  const resp = await handleResendOutbound(req);
393
394
  expect(resp.status).toBe(400);
394
- const body = await resp.json() as Record<string, unknown>;
395
- expect(body.error).toBe('missing_channel');
395
+ const body = await resp.json() as { error: { message: string; code: string } };
396
+ expect(body.error.code).toBe('BAD_REQUEST');
397
+ expect(body.error.message).toContain('channel');
396
398
  });
397
399
 
398
400
  test('returns 400 for no_active_session', async () => {
399
401
  const req = jsonRequest({ channel: 'sms', assistantId: 'resend-no-session' });
400
402
  const resp = await handleResendOutbound(req);
401
403
  expect(resp.status).toBe(400);
402
- const body = await resp.json() as Record<string, unknown>;
404
+ const body = await resp.json() as { error?: string };
403
405
  expect(body.error).toBe('no_active_session');
404
406
  });
405
407
 
@@ -432,15 +434,16 @@ describe('HTTP route: handleCancelOutbound', () => {
432
434
  const req = jsonRequest({});
433
435
  const resp = await handleCancelOutbound(req);
434
436
  expect(resp.status).toBe(400);
435
- const body = await resp.json() as Record<string, unknown>;
436
- expect(body.error).toBe('missing_channel');
437
+ const body = await resp.json() as { error: { message: string; code: string } };
438
+ expect(body.error.code).toBe('BAD_REQUEST');
439
+ expect(body.error.message).toContain('channel');
437
440
  });
438
441
 
439
442
  test('returns 400 for no_active_session', async () => {
440
443
  const req = jsonRequest({ channel: 'sms', assistantId: 'cancel-no-session' });
441
444
  const resp = await handleCancelOutbound(req);
442
445
  expect(resp.status).toBe(400);
443
- const body = await resp.json() as Record<string, unknown>;
446
+ const body = await resp.json() as { error?: string };
444
447
  expect(body.error).toBe('no_active_session');
445
448
  });
446
449