@vellumai/assistant 0.4.2 → 0.4.4

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 (221) hide show
  1. package/.env.example +3 -0
  2. package/ARCHITECTURE.md +124 -10
  3. package/README.md +43 -35
  4. package/docs/trusted-contact-access.md +20 -0
  5. package/package.json +1 -1
  6. package/scripts/ipc/generate-swift.ts +1 -0
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
  8. package/src/__tests__/access-request-decision.test.ts +0 -1
  9. package/src/__tests__/actor-token-service.test.ts +1099 -0
  10. package/src/__tests__/agent-loop.test.ts +51 -0
  11. package/src/__tests__/approval-routes-http.test.ts +2 -0
  12. package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
  13. package/src/__tests__/assistant-id-boundary-guard.test.ts +415 -0
  14. package/src/__tests__/call-controller.test.ts +49 -0
  15. package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
  16. package/src/__tests__/call-pointer-messages.test.ts +93 -3
  17. package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
  18. package/src/__tests__/call-routes-http.test.ts +0 -25
  19. package/src/__tests__/callback-handoff-copy.test.ts +186 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +133 -12
  21. package/src/__tests__/channel-guardian.test.ts +0 -86
  22. package/src/__tests__/channel-readiness-service.test.ts +10 -16
  23. package/src/__tests__/checker.test.ts +33 -12
  24. package/src/__tests__/config-schema.test.ts +6 -0
  25. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
  26. package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
  27. package/src/__tests__/conversation-routes.test.ts +12 -3
  28. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  29. package/src/__tests__/daemon-server-session-init.test.ts +4 -0
  30. package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
  31. package/src/__tests__/guardian-actions-endpoint.test.ts +39 -13
  32. package/src/__tests__/guardian-dispatch.test.ts +8 -0
  33. package/src/__tests__/guardian-outbound-http.test.ts +4 -5
  34. package/src/__tests__/guardian-question-mode.test.ts +200 -0
  35. package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
  36. package/src/__tests__/guardian-routing-state.test.ts +525 -0
  37. package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
  38. package/src/__tests__/handlers-telegram-config.test.ts +0 -83
  39. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
  40. package/src/__tests__/headless-browser-navigate.test.ts +2 -0
  41. package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
  42. package/src/__tests__/ingress-routes-http.test.ts +55 -0
  43. package/src/__tests__/ipc-snapshot.test.ts +18 -51
  44. package/src/__tests__/non-member-access-request.test.ts +159 -9
  45. package/src/__tests__/notification-decision-fallback.test.ts +129 -4
  46. package/src/__tests__/notification-decision-strategy.test.ts +106 -2
  47. package/src/__tests__/notification-guardian-path.test.ts +3 -0
  48. package/src/__tests__/recording-intent-handler.test.ts +1 -0
  49. package/src/__tests__/relay-server.test.ts +1475 -33
  50. package/src/__tests__/send-endpoint-busy.test.ts +5 -0
  51. package/src/__tests__/session-agent-loop.test.ts +1 -0
  52. package/src/__tests__/session-confirmation-signals.test.ts +523 -0
  53. package/src/__tests__/session-init.benchmark.test.ts +0 -2
  54. package/src/__tests__/session-runtime-assembly.test.ts +4 -1
  55. package/src/__tests__/session-surfaces-task-progress.test.ts +44 -1
  56. package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
  57. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
  58. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
  59. package/src/__tests__/tool-executor.test.ts +21 -2
  60. package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
  61. package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
  62. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -0
  63. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +11 -1
  64. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  65. package/src/__tests__/trusted-contact-verification.test.ts +0 -1
  66. package/src/__tests__/twilio-config.test.ts +2 -13
  67. package/src/__tests__/twilio-routes.test.ts +4 -3
  68. package/src/__tests__/update-bulletin.test.ts +0 -1
  69. package/src/agent/loop.ts +1 -1
  70. package/src/approvals/guardian-decision-primitive.ts +12 -3
  71. package/src/approvals/guardian-request-resolvers.ts +169 -11
  72. package/src/calls/call-constants.ts +29 -0
  73. package/src/calls/call-controller.ts +11 -3
  74. package/src/calls/call-domain.ts +33 -11
  75. package/src/calls/call-pointer-message-composer.ts +154 -0
  76. package/src/calls/call-pointer-messages.ts +106 -27
  77. package/src/calls/guardian-dispatch.ts +4 -2
  78. package/src/calls/relay-server.ts +921 -112
  79. package/src/calls/twilio-config.ts +4 -11
  80. package/src/calls/twilio-routes.ts +4 -6
  81. package/src/calls/types.ts +3 -1
  82. package/src/calls/voice-session-bridge.ts +4 -3
  83. package/src/cli/core-commands.ts +7 -4
  84. package/src/cli.ts +5 -4
  85. package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
  86. package/src/config/bundled-skills/app-builder/SKILL.md +309 -10
  87. package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
  88. package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
  89. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
  90. package/src/config/bundled-skills/messaging/SKILL.md +61 -12
  91. package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
  92. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
  93. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
  94. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
  95. package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
  96. package/src/config/bundled-skills/twitter/SKILL.md +3 -3
  97. package/src/config/bundled-skills/vercel-token-setup/SKILL.md +215 -0
  98. package/src/config/calls-schema.ts +36 -0
  99. package/src/config/env.ts +22 -0
  100. package/src/config/feature-flag-registry.json +8 -8
  101. package/src/config/schema.ts +2 -2
  102. package/src/config/skills.ts +11 -0
  103. package/src/config/system-prompt.ts +11 -1
  104. package/src/config/templates/SOUL.md +2 -0
  105. package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
  106. package/src/config/vellum-skills/trusted-contacts/SKILL.md +8 -1
  107. package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
  108. package/src/daemon/call-pointer-generators.ts +59 -0
  109. package/src/daemon/computer-use-session.ts +2 -5
  110. package/src/daemon/handlers/apps.ts +76 -20
  111. package/src/daemon/handlers/config-channels.ts +9 -61
  112. package/src/daemon/handlers/config-inbox.ts +11 -3
  113. package/src/daemon/handlers/config-ingress.ts +28 -3
  114. package/src/daemon/handlers/config-telegram.ts +12 -0
  115. package/src/daemon/handlers/config.ts +2 -6
  116. package/src/daemon/handlers/index.ts +2 -1
  117. package/src/daemon/handlers/pairing.ts +2 -0
  118. package/src/daemon/handlers/publish.ts +11 -46
  119. package/src/daemon/handlers/sessions.ts +59 -5
  120. package/src/daemon/handlers/shared.ts +17 -2
  121. package/src/daemon/ipc-contract/apps.ts +1 -0
  122. package/src/daemon/ipc-contract/inbox.ts +4 -0
  123. package/src/daemon/ipc-contract/integrations.ts +1 -97
  124. package/src/daemon/ipc-contract/messages.ts +47 -1
  125. package/src/daemon/ipc-contract/notifications.ts +11 -0
  126. package/src/daemon/ipc-contract-inventory.json +2 -4
  127. package/src/daemon/lifecycle.ts +17 -0
  128. package/src/daemon/server.ts +16 -2
  129. package/src/daemon/session-agent-loop-handlers.ts +20 -0
  130. package/src/daemon/session-agent-loop.ts +24 -12
  131. package/src/daemon/session-lifecycle.ts +1 -1
  132. package/src/daemon/session-process.ts +11 -1
  133. package/src/daemon/session-runtime-assembly.ts +6 -1
  134. package/src/daemon/session-surfaces.ts +32 -3
  135. package/src/daemon/session.ts +88 -1
  136. package/src/daemon/tool-side-effects.ts +22 -0
  137. package/src/home-base/prebuilt/brain-graph.html +1483 -0
  138. package/src/home-base/prebuilt/index.html +40 -0
  139. package/src/inbound/platform-callback-registration.ts +157 -0
  140. package/src/memory/canonical-guardian-store.ts +1 -1
  141. package/src/memory/conversation-crud.ts +2 -1
  142. package/src/memory/conversation-title-service.ts +16 -2
  143. package/src/memory/db-init.ts +8 -0
  144. package/src/memory/delivery-crud.ts +2 -1
  145. package/src/memory/guardian-action-store.ts +2 -1
  146. package/src/memory/guardian-approvals.ts +3 -2
  147. package/src/memory/ingress-invite-store.ts +12 -2
  148. package/src/memory/ingress-member-store.ts +4 -3
  149. package/src/memory/migrations/038-actor-token-records.ts +39 -0
  150. package/src/memory/migrations/124-voice-invite-display-metadata.ts +14 -0
  151. package/src/memory/migrations/index.ts +2 -0
  152. package/src/memory/schema.ts +26 -5
  153. package/src/messaging/provider-types.ts +24 -0
  154. package/src/messaging/provider.ts +7 -0
  155. package/src/messaging/providers/gmail/adapter.ts +127 -0
  156. package/src/messaging/providers/sms/adapter.ts +40 -37
  157. package/src/notifications/adapters/macos.ts +45 -2
  158. package/src/notifications/broadcaster.ts +16 -0
  159. package/src/notifications/copy-composer.ts +50 -2
  160. package/src/notifications/decision-engine.ts +22 -9
  161. package/src/notifications/destination-resolver.ts +16 -2
  162. package/src/notifications/emit-signal.ts +18 -9
  163. package/src/notifications/guardian-question-mode.ts +419 -0
  164. package/src/notifications/signal.ts +14 -3
  165. package/src/permissions/checker.ts +13 -1
  166. package/src/permissions/prompter.ts +14 -0
  167. package/src/providers/anthropic/client.ts +20 -0
  168. package/src/providers/provider-send-message.ts +15 -3
  169. package/src/runtime/access-request-helper.ts +82 -4
  170. package/src/runtime/actor-token-service.ts +234 -0
  171. package/src/runtime/actor-token-store.ts +236 -0
  172. package/src/runtime/actor-trust-resolver.ts +2 -2
  173. package/src/runtime/assistant-scope.ts +10 -0
  174. package/src/runtime/channel-approvals.ts +5 -3
  175. package/src/runtime/channel-readiness-service.ts +23 -64
  176. package/src/runtime/channel-readiness-types.ts +3 -4
  177. package/src/runtime/channel-retry-sweep.ts +4 -1
  178. package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
  179. package/src/runtime/guardian-action-followup-executor.ts +1 -1
  180. package/src/runtime/guardian-context-resolver.ts +82 -0
  181. package/src/runtime/guardian-outbound-actions.ts +5 -7
  182. package/src/runtime/guardian-reply-router.ts +67 -30
  183. package/src/runtime/guardian-vellum-migration.ts +57 -0
  184. package/src/runtime/http-server.ts +75 -31
  185. package/src/runtime/http-types.ts +13 -0
  186. package/src/runtime/ingress-service.ts +14 -0
  187. package/src/runtime/invite-redemption-service.ts +10 -1
  188. package/src/runtime/local-actor-identity.ts +76 -0
  189. package/src/runtime/middleware/actor-token.ts +271 -0
  190. package/src/runtime/middleware/twilio-validation.ts +2 -4
  191. package/src/runtime/routes/approval-routes.ts +82 -7
  192. package/src/runtime/routes/brain-graph-routes.ts +222 -0
  193. package/src/runtime/routes/call-routes.ts +2 -1
  194. package/src/runtime/routes/channel-readiness-routes.ts +71 -0
  195. package/src/runtime/routes/channel-route-shared.ts +3 -3
  196. package/src/runtime/routes/conversation-attention-routes.ts +2 -1
  197. package/src/runtime/routes/conversation-routes.ts +142 -53
  198. package/src/runtime/routes/events-routes.ts +22 -8
  199. package/src/runtime/routes/guardian-action-routes.ts +45 -3
  200. package/src/runtime/routes/guardian-approval-interception.ts +29 -0
  201. package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
  202. package/src/runtime/routes/inbound-conversation.ts +4 -3
  203. package/src/runtime/routes/inbound-message-handler.ts +147 -5
  204. package/src/runtime/routes/ingress-routes.ts +2 -0
  205. package/src/runtime/routes/integration-routes.ts +7 -15
  206. package/src/runtime/routes/pairing-routes.ts +163 -0
  207. package/src/runtime/routes/twilio-routes.ts +934 -0
  208. package/src/runtime/tool-grant-request-helper.ts +3 -1
  209. package/src/security/oauth2.ts +27 -2
  210. package/src/security/token-manager.ts +46 -10
  211. package/src/tools/browser/browser-execution.ts +4 -3
  212. package/src/tools/browser/browser-handoff.ts +10 -18
  213. package/src/tools/browser/browser-manager.ts +80 -25
  214. package/src/tools/browser/browser-screencast.ts +35 -119
  215. package/src/tools/calls/call-start.ts +2 -1
  216. package/src/tools/permission-checker.ts +15 -4
  217. package/src/tools/terminal/parser.ts +12 -0
  218. package/src/tools/tool-approval-handler.ts +244 -19
  219. package/src/workspace/git-service.ts +19 -0
  220. package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
  221. package/src/daemon/handlers/config-twilio.ts +0 -1082
@@ -1319,11 +1319,12 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
1319
1319
  deliverSpy.mockRestore();
1320
1320
  });
1321
1321
 
1322
- test('verification code with explicit assistantId resolves against that assistant', async () => {
1322
+ test('verification code with explicit assistantId resolves against canonical scope', async () => {
1323
1323
  const { createVerificationChallenge } = await import('../runtime/channel-guardian-service.js');
1324
1324
  const { getGuardianBinding } = await import('../runtime/channel-guardian-service.js');
1325
1325
 
1326
- const { secret } = createVerificationChallenge('asst-route-X', 'telegram');
1326
+ // All assistant IDs canonicalize to 'self' in the single-tenant daemon
1327
+ const { secret } = createVerificationChallenge('self', 'telegram');
1327
1328
 
1328
1329
  const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
1329
1330
 
@@ -1338,17 +1339,18 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
1338
1339
  expect(body.accepted).toBe(true);
1339
1340
  expect(body.guardianVerification).toBe('verified');
1340
1341
 
1341
- const bindingX = getGuardianBinding('asst-route-X', 'telegram');
1342
+ const bindingX = getGuardianBinding('self', 'telegram');
1342
1343
  expect(bindingX).not.toBeNull();
1343
1344
  expect(bindingX!.guardianExternalUserId).toBe('user-for-asst-x');
1344
1345
 
1345
1346
  deliverSpy.mockRestore();
1346
1347
  });
1347
1348
 
1348
- test('cross-assistant challenge code does not verify against a different assistant scope', async () => {
1349
+ test('all assistant IDs share canonical scope for verification', async () => {
1349
1350
  const { createVerificationChallenge } = await import('../runtime/channel-guardian-service.js');
1350
1351
 
1351
- const { secret } = createVerificationChallenge('asst-A-cross', 'telegram');
1352
+ // Both IDs canonicalize to 'self', so the challenge is found
1353
+ const { secret } = createVerificationChallenge('self', 'telegram');
1352
1354
 
1353
1355
  const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
1354
1356
 
@@ -1361,19 +1363,19 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
1361
1363
  const body = await res.json() as Record<string, unknown>;
1362
1364
 
1363
1365
  expect(body.accepted).toBe(true);
1364
- expect(body.guardianVerification).toBeUndefined();
1366
+ expect(body.guardianVerification).toBe('verified');
1365
1367
 
1366
1368
  deliverSpy.mockRestore();
1367
1369
  });
1368
1370
 
1369
- test('non-self assistant inbound does not mutate assistant-agnostic external bindings', async () => {
1371
+ test('inbound with explicit assistantId does not mutate existing external bindings', async () => {
1370
1372
  const db = getDb();
1371
1373
  const now = Date.now();
1372
1374
  ensureConversation('conv-existing-binding');
1373
1375
  db.insert(externalConversationBindings).values({
1374
1376
  conversationId: 'conv-existing-binding',
1375
1377
  sourceChannel: 'telegram',
1376
- externalChatId: 'chat-123',
1378
+ externalChatId: 'chat-existing-999',
1377
1379
  externalUserId: 'existing-user',
1378
1380
  createdAt: now,
1379
1381
  updatedAt: now,
@@ -1385,7 +1387,7 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
1385
1387
  senderExternalUserId: 'incoming-user',
1386
1388
  });
1387
1389
 
1388
- const res = await handleChannelInbound(req, undefined, 'token', 'asst-non-self');
1390
+ const res = await handleChannelInbound(req, noopProcessMessage, 'token', 'asst-non-self');
1389
1391
  expect(res.status).toBe(200);
1390
1392
 
1391
1393
  const binding = db
@@ -2665,8 +2667,10 @@ describe('background channel processing approval prompts', () => {
2665
2667
  deliverPromptSpy.mockRestore();
2666
2668
  });
2667
2669
 
2668
- test('non-guardian channel turns are not interactive to prevent self-approval', async () => {
2669
- // Set up a guardian binding for a DIFFERENT user so the sender is non-guardian
2670
+ test('trusted-contact channel turns with resolvable guardian route are interactive', async () => {
2671
+ // Set up a guardian binding for a DIFFERENT user so the sender is a
2672
+ // trusted contact (not the guardian). The guardian route is resolvable
2673
+ // because the binding exists — approval notifications can be delivered.
2670
2674
  createBinding({
2671
2675
  assistantId: 'self',
2672
2676
  channel: 'telegram',
@@ -2701,7 +2705,9 @@ describe('background channel processing approval prompts', () => {
2701
2705
  await new Promise((resolve) => setTimeout(resolve, 300));
2702
2706
 
2703
2707
  expect(processCalls.length).toBeGreaterThan(0);
2704
- expect(processCalls[0].options?.isInteractive).toBe(false);
2708
+ // Trusted contacts with a resolvable guardian route should be interactive
2709
+ // so approval prompts can be routed to the guardian for decision.
2710
+ expect(processCalls[0].options?.isInteractive).toBe(true);
2705
2711
  });
2706
2712
 
2707
2713
  test('unverified channel turns never broadcast approval prompts', async () => {
@@ -2869,3 +2875,118 @@ describe('NL approval routing via destination-scoped canonical requests', () =>
2869
2875
  expect(unchanged!.status).toBe('pending');
2870
2876
  });
2871
2877
  });
2878
+
2879
+ // ═══════════════════════════════════════════════════════════════════════════
2880
+ // Trusted-contact self-approval guard (pre-row)
2881
+ // ═══════════════════════════════════════════════════════════════════════════
2882
+
2883
+ describe('trusted-contact self-approval blocked before guardian approval row exists', () => {
2884
+ beforeEach(() => {
2885
+ // Create a guardian binding so the requester resolves as trusted_contact
2886
+ createBinding({
2887
+ assistantId: 'self',
2888
+ channel: 'telegram',
2889
+ guardianExternalUserId: 'guardian-tc-selfapproval',
2890
+ guardianDeliveryChatId: 'guardian-tc-selfapproval-chat',
2891
+ });
2892
+ });
2893
+
2894
+ test('trusted contact cannot self-approve via conversational engine when no guardian approval row exists', async () => {
2895
+ const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
2896
+
2897
+ // Create the requester conversation (different user than guardian)
2898
+ const initReq = makeInboundRequest({
2899
+ content: 'init',
2900
+ externalChatId: 'tc-selfapproval-chat',
2901
+ senderExternalUserId: 'tc-selfapproval-user',
2902
+ });
2903
+ await handleChannelInbound(initReq, noopProcessMessage, 'token');
2904
+
2905
+ const db = getDb();
2906
+ const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
2907
+ const conversationId = events[0]?.conversation_id;
2908
+ ensureConversation(conversationId!);
2909
+
2910
+ // Register a pending interaction — but do NOT create a guardian approval
2911
+ // row in channelGuardianApprovalRequests. This simulates the window
2912
+ // between the pending confirmation being created (isInteractive=true)
2913
+ // and the guardian approval prompt being delivered.
2914
+ const sessionMock = registerPendingInteraction('req-tc-selfapproval-1', conversationId!, 'shell');
2915
+
2916
+ deliverSpy.mockClear();
2917
+
2918
+ // The conversational engine would normally classify "yes" as approve_once,
2919
+ // but the guard should intercept before the engine runs.
2920
+ const mockConversationGenerator = mock(async (_ctx: unknown) => ({
2921
+ disposition: 'approve_once' as const,
2922
+ replyText: 'Approved!',
2923
+ }));
2924
+
2925
+ // Trusted contact sends "yes" to try to self-approve
2926
+ const req = makeInboundRequest({
2927
+ content: 'yes',
2928
+ externalChatId: 'tc-selfapproval-chat',
2929
+ senderExternalUserId: 'tc-selfapproval-user',
2930
+ });
2931
+ const res = await handleChannelInbound(
2932
+ req, noopProcessMessage, 'token', 'self', undefined,
2933
+ undefined, mockConversationGenerator,
2934
+ );
2935
+ const body = await res.json() as Record<string, unknown>;
2936
+
2937
+ expect(body.accepted).toBe(true);
2938
+ // Should be blocked with assistant_turn (pending guardian notice),
2939
+ // NOT decision_applied
2940
+ expect(body.approval).toBe('assistant_turn');
2941
+ // The session should NOT have been resolved
2942
+ expect(sessionMock).not.toHaveBeenCalled();
2943
+
2944
+ // The pending interaction should still be registered (not consumed)
2945
+ const stillPending = pendingInteractions.get('req-tc-selfapproval-1');
2946
+ expect(stillPending).toBeDefined();
2947
+
2948
+ deliverSpy.mockRestore();
2949
+ });
2950
+
2951
+ test('trusted contact cannot self-approve via legacy parser when no guardian approval row exists', async () => {
2952
+ const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
2953
+
2954
+ const initReq = makeInboundRequest({
2955
+ content: 'init',
2956
+ externalChatId: 'tc-selfapproval-chat',
2957
+ senderExternalUserId: 'tc-selfapproval-user',
2958
+ });
2959
+ await handleChannelInbound(initReq, noopProcessMessage, 'token');
2960
+
2961
+ const db = getDb();
2962
+ const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
2963
+ const conversationId = events[0]?.conversation_id;
2964
+ ensureConversation(conversationId!);
2965
+
2966
+ // Register pending interaction without guardian approval row
2967
+ const sessionMock = registerPendingInteraction('req-tc-selfapproval-2', conversationId!, 'shell');
2968
+
2969
+ deliverSpy.mockClear();
2970
+
2971
+ // No conversational engine — falls through to legacy parser path.
2972
+ // "approve" would normally be parsed as an approval decision.
2973
+ const req = makeInboundRequest({
2974
+ content: 'approve',
2975
+ externalChatId: 'tc-selfapproval-chat',
2976
+ senderExternalUserId: 'tc-selfapproval-user',
2977
+ });
2978
+ const res = await handleChannelInbound(req, noopProcessMessage, 'token');
2979
+ const body = await res.json() as Record<string, unknown>;
2980
+
2981
+ expect(body.accepted).toBe(true);
2982
+ // Should be blocked, not decision_applied
2983
+ expect(body.approval).toBe('assistant_turn');
2984
+ expect(sessionMock).not.toHaveBeenCalled();
2985
+
2986
+ // Pending interaction should still exist
2987
+ const stillPending = pendingInteractions.get('req-tc-selfapproval-2');
2988
+ expect(stillPending).toBeDefined();
2989
+
2990
+ deliverSpy.mockRestore();
2991
+ });
2992
+ });
@@ -22,7 +22,6 @@ mock.module('../util/platform.js', () => ({
22
22
  getDbPath: () => join(testDir, 'test.db'),
23
23
  getLogPath: () => join(testDir, 'test.log'),
24
24
  ensureDataDir: () => {},
25
- normalizeAssistantId: (id: string) => id === 'self' ? 'self' : id,
26
25
  readHttpToken: () => 'test-bearer-token',
27
26
  }));
28
27
 
@@ -1339,7 +1338,6 @@ describe('IPC handler channel-aware guardian status', () => {
1339
1338
  type: 'guardian_verification',
1340
1339
  action: 'status',
1341
1340
  channel: 'telegram',
1342
- assistantId: 'self',
1343
1341
  };
1344
1342
 
1345
1343
  handleGuardianVerification(msg, mockSocket, ctx);
@@ -1359,7 +1357,6 @@ describe('IPC handler channel-aware guardian status', () => {
1359
1357
  type: 'guardian_verification',
1360
1358
  action: 'status',
1361
1359
  channel: 'sms',
1362
- assistantId: 'self',
1363
1360
  };
1364
1361
 
1365
1362
  handleGuardianVerification(msg, mockSocket, ctx);
@@ -1385,7 +1382,6 @@ describe('IPC handler channel-aware guardian status', () => {
1385
1382
  type: 'guardian_verification',
1386
1383
  action: 'status',
1387
1384
  channel: 'telegram',
1388
- assistantId: 'self',
1389
1385
  };
1390
1386
 
1391
1387
  handleGuardianVerification(msg, mockSocket, ctx);
@@ -1414,7 +1410,6 @@ describe('IPC handler channel-aware guardian status', () => {
1414
1410
  type: 'guardian_verification',
1415
1411
  action: 'status',
1416
1412
  channel: 'telegram',
1417
- assistantId: 'self',
1418
1413
  };
1419
1414
 
1420
1415
  handleGuardianVerification(msg, mockSocket, ctx);
@@ -1458,34 +1453,6 @@ describe('IPC handler channel-aware guardian status', () => {
1458
1453
  expect(resp!.channel).toBe('sms');
1459
1454
  });
1460
1455
 
1461
- test('status action with custom assistantId returns correct value', () => {
1462
- createBinding({
1463
- assistantId: 'asst-custom',
1464
- channel: 'telegram',
1465
- guardianExternalUserId: 'user-77',
1466
- guardianDeliveryChatId: 'chat-77',
1467
- });
1468
-
1469
- const { ctx, lastResponse } = createMockCtx();
1470
- const msg: GuardianVerificationRequest = {
1471
- type: 'guardian_verification',
1472
- action: 'status',
1473
- channel: 'telegram',
1474
- assistantId: 'asst-custom',
1475
- };
1476
-
1477
- handleGuardianVerification(msg, mockSocket, ctx);
1478
-
1479
- const resp = lastResponse();
1480
- expect(resp).not.toBeNull();
1481
- expect(resp!.success).toBe(true);
1482
- expect(resp!.bound).toBe(true);
1483
- expect(resp!.assistantId).toBe('asst-custom');
1484
- expect(resp!.channel).toBe('telegram');
1485
- expect(resp!.guardianExternalUserId).toBe('user-77');
1486
- expect(resp!.guardianDeliveryChatId).toBe('chat-77');
1487
- });
1488
-
1489
1456
  test('status action for unbound sms does not return guardianDeliveryChatId', () => {
1490
1457
  const { ctx, lastResponse } = createMockCtx();
1491
1458
  const msg: GuardianVerificationRequest = {
@@ -1511,7 +1478,6 @@ describe('IPC handler channel-aware guardian status', () => {
1511
1478
  type: 'guardian_verification',
1512
1479
  action: 'status',
1513
1480
  channel: 'voice',
1514
- assistantId: 'self',
1515
1481
  };
1516
1482
 
1517
1483
  handleGuardianVerification(msg, mockSocket, ctx);
@@ -1528,7 +1494,6 @@ describe('IPC handler channel-aware guardian status', () => {
1528
1494
  type: 'guardian_verification',
1529
1495
  action: 'status',
1530
1496
  channel: 'voice',
1531
- assistantId: 'self',
1532
1497
  };
1533
1498
 
1534
1499
  handleGuardianVerification(msg, mockSocket, ctx);
@@ -1943,7 +1908,6 @@ describe('IPC handler voice guardian verification', () => {
1943
1908
  type: 'guardian_verification',
1944
1909
  action: 'create_challenge',
1945
1910
  channel: 'voice',
1946
- assistantId: 'self',
1947
1911
  };
1948
1912
 
1949
1913
  handleGuardianVerification(msg, mockSocket, ctx);
@@ -1964,7 +1928,6 @@ describe('IPC handler voice guardian verification', () => {
1964
1928
  type: 'guardian_verification',
1965
1929
  action: 'status',
1966
1930
  channel: 'voice',
1967
- assistantId: 'self',
1968
1931
  };
1969
1932
 
1970
1933
  handleGuardianVerification(msg, mockSocket, ctx);
@@ -1990,7 +1953,6 @@ describe('IPC handler voice guardian verification', () => {
1990
1953
  type: 'guardian_verification',
1991
1954
  action: 'status',
1992
1955
  channel: 'voice',
1993
- assistantId: 'self',
1994
1956
  };
1995
1957
 
1996
1958
  handleGuardianVerification(msg, mockSocket, ctx);
@@ -2017,7 +1979,6 @@ describe('IPC handler voice guardian verification', () => {
2017
1979
  type: 'guardian_verification',
2018
1980
  action: 'revoke',
2019
1981
  channel: 'voice',
2020
- assistantId: 'self',
2021
1982
  };
2022
1983
 
2023
1984
  handleGuardianVerification(msg, mockSocket, ctx);
@@ -2050,7 +2011,6 @@ describe('IPC handler voice guardian verification', () => {
2050
2011
  type: 'guardian_verification',
2051
2012
  action: 'revoke',
2052
2013
  channel: 'voice',
2053
- assistantId: 'self',
2054
2014
  };
2055
2015
 
2056
2016
  handleGuardianVerification(msg, mockSocket, ctx);
@@ -2515,7 +2475,6 @@ describe('outbound SMS verification', () => {
2515
2475
  type: 'guardian_verification',
2516
2476
  action: 'start_outbound',
2517
2477
  channel: 'sms',
2518
- assistantId: 'self',
2519
2478
  destination: '+15551234567',
2520
2479
  };
2521
2480
 
@@ -2552,7 +2511,6 @@ describe('outbound SMS verification', () => {
2552
2511
  type: 'guardian_verification',
2553
2512
  action: 'start_outbound',
2554
2513
  channel: 'sms',
2555
- assistantId: 'self',
2556
2514
  destination: '+15559876543',
2557
2515
  rebind: false,
2558
2516
  };
@@ -2579,7 +2537,6 @@ describe('outbound SMS verification', () => {
2579
2537
  type: 'guardian_verification',
2580
2538
  action: 'start_outbound',
2581
2539
  channel: 'sms',
2582
- assistantId: 'self',
2583
2540
  destination: '+15559876543',
2584
2541
  rebind: true,
2585
2542
  };
@@ -2599,7 +2556,6 @@ describe('outbound SMS verification', () => {
2599
2556
  type: 'guardian_verification',
2600
2557
  action: 'start_outbound',
2601
2558
  channel: 'sms',
2602
- assistantId: 'self',
2603
2559
  destination: '+15551234567',
2604
2560
  }, mockSocket, startCtx);
2605
2561
 
@@ -2609,7 +2565,6 @@ describe('outbound SMS verification', () => {
2609
2565
  type: 'guardian_verification',
2610
2566
  action: 'resend_outbound',
2611
2567
  channel: 'sms',
2612
- assistantId: 'self',
2613
2568
  }, mockSocket, ctx);
2614
2569
 
2615
2570
  const resp = lastResponse();
@@ -2625,7 +2580,6 @@ describe('outbound SMS verification', () => {
2625
2580
  type: 'guardian_verification',
2626
2581
  action: 'start_outbound',
2627
2582
  channel: 'sms',
2628
- assistantId: 'self',
2629
2583
  destination: '+15551234567',
2630
2584
  }, mockSocket, startCtx);
2631
2585
 
@@ -2643,7 +2597,6 @@ describe('outbound SMS verification', () => {
2643
2597
  type: 'guardian_verification',
2644
2598
  action: 'resend_outbound',
2645
2599
  channel: 'sms',
2646
- assistantId: 'self',
2647
2600
  }, mockSocket, ctx);
2648
2601
 
2649
2602
  const resp = lastResponse();
@@ -2660,7 +2613,6 @@ describe('outbound SMS verification', () => {
2660
2613
  type: 'guardian_verification',
2661
2614
  action: 'start_outbound',
2662
2615
  channel: 'sms',
2663
- assistantId: 'self',
2664
2616
  destination: '+15551234567',
2665
2617
  }, mockSocket, startCtx);
2666
2618
 
@@ -2680,7 +2632,6 @@ describe('outbound SMS verification', () => {
2680
2632
  type: 'guardian_verification',
2681
2633
  action: 'resend_outbound',
2682
2634
  channel: 'sms',
2683
- assistantId: 'self',
2684
2635
  }, mockSocket, ctx);
2685
2636
 
2686
2637
  const resp = lastResponse();
@@ -2696,7 +2647,6 @@ describe('outbound SMS verification', () => {
2696
2647
  type: 'guardian_verification',
2697
2648
  action: 'start_outbound',
2698
2649
  channel: 'sms',
2699
- assistantId: 'self',
2700
2650
  destination: '+15551234567',
2701
2651
  }, mockSocket, startCtx);
2702
2652
 
@@ -2710,7 +2660,6 @@ describe('outbound SMS verification', () => {
2710
2660
  type: 'guardian_verification',
2711
2661
  action: 'cancel_outbound',
2712
2662
  channel: 'sms',
2713
- assistantId: 'self',
2714
2663
  }, mockSocket, ctx);
2715
2664
 
2716
2665
  const resp = lastResponse();
@@ -2802,7 +2751,6 @@ describe('outbound SMS verification', () => {
2802
2751
  type: 'guardian_verification',
2803
2752
  action: 'start_outbound',
2804
2753
  channel: 'slack',
2805
- assistantId: 'self',
2806
2754
  destination: '@some_user',
2807
2755
  }, mockSocket, ctx);
2808
2756
 
@@ -2818,7 +2766,6 @@ describe('outbound SMS verification', () => {
2818
2766
  type: 'guardian_verification',
2819
2767
  action: 'start_outbound',
2820
2768
  channel: 'sms',
2821
- assistantId: 'self',
2822
2769
  // no destination
2823
2770
  }, mockSocket, ctx);
2824
2771
 
@@ -2834,7 +2781,6 @@ describe('outbound SMS verification', () => {
2834
2781
  type: 'guardian_verification',
2835
2782
  action: 'start_outbound',
2836
2783
  channel: 'sms',
2837
- assistantId: 'self',
2838
2784
  destination: 'not-a-phone',
2839
2785
  }, mockSocket, ctx);
2840
2786
 
@@ -2850,7 +2796,6 @@ describe('outbound SMS verification', () => {
2850
2796
  type: 'guardian_verification',
2851
2797
  action: 'start_outbound',
2852
2798
  channel: 'sms',
2853
- assistantId: 'self',
2854
2799
  destination: '(555) 123-4567',
2855
2800
  }, mockSocket, ctx);
2856
2801
 
@@ -2891,7 +2836,6 @@ describe('outbound SMS verification', () => {
2891
2836
  type: 'guardian_verification',
2892
2837
  action: 'cancel_outbound',
2893
2838
  channel: 'sms',
2894
- assistantId: 'self',
2895
2839
  }, mockSocket, ctx);
2896
2840
 
2897
2841
  const resp = lastResponse();
@@ -2916,7 +2860,6 @@ describe('outbound Telegram verification', () => {
2916
2860
  type: 'guardian_verification',
2917
2861
  action: 'start_outbound',
2918
2862
  channel: 'telegram',
2919
- assistantId: 'self',
2920
2863
  destination: '@someuser',
2921
2864
  }, mockSocket, ctx);
2922
2865
 
@@ -2946,7 +2889,6 @@ describe('outbound Telegram verification', () => {
2946
2889
  type: 'guardian_verification',
2947
2890
  action: 'start_outbound',
2948
2891
  channel: 'telegram',
2949
- assistantId: 'self',
2950
2892
  destination: 'someuser',
2951
2893
  }, mockSocket, ctx);
2952
2894
 
@@ -2963,7 +2905,6 @@ describe('outbound Telegram verification', () => {
2963
2905
  type: 'guardian_verification',
2964
2906
  action: 'start_outbound',
2965
2907
  channel: 'telegram',
2966
- assistantId: 'self',
2967
2908
  destination: '123456789',
2968
2909
  }, mockSocket, ctx);
2969
2910
 
@@ -3001,7 +2942,6 @@ describe('outbound Telegram verification', () => {
3001
2942
  type: 'guardian_verification',
3002
2943
  action: 'start_outbound',
3003
2944
  channel: 'telegram',
3004
- assistantId: 'self',
3005
2945
  destination: '@someuser',
3006
2946
  }, mockSocket, ctx);
3007
2947
 
@@ -3024,7 +2964,6 @@ describe('outbound Telegram verification', () => {
3024
2964
  type: 'guardian_verification',
3025
2965
  action: 'start_outbound',
3026
2966
  channel: 'telegram',
3027
- assistantId: 'self',
3028
2967
  destination: '@newuser',
3029
2968
  rebind: false,
3030
2969
  }, mockSocket, ctx);
@@ -3195,7 +3134,6 @@ describe('outbound Telegram verification', () => {
3195
3134
  type: 'guardian_verification',
3196
3135
  action: 'start_outbound',
3197
3136
  channel: 'telegram',
3198
- assistantId: 'self',
3199
3137
  destination: '123456789',
3200
3138
  }, mockSocket, startCtx);
3201
3139
 
@@ -3209,7 +3147,6 @@ describe('outbound Telegram verification', () => {
3209
3147
  type: 'guardian_verification',
3210
3148
  action: 'resend_outbound',
3211
3149
  channel: 'telegram',
3212
- assistantId: 'self',
3213
3150
  }, mockSocket, ctx);
3214
3151
 
3215
3152
  const resp = lastResponse();
@@ -3230,7 +3167,6 @@ describe('outbound Telegram verification', () => {
3230
3167
  type: 'guardian_verification',
3231
3168
  action: 'start_outbound',
3232
3169
  channel: 'telegram',
3233
- assistantId: 'self',
3234
3170
  destination: '@someuser',
3235
3171
  }, mockSocket, startCtx);
3236
3172
 
@@ -3239,7 +3175,6 @@ describe('outbound Telegram verification', () => {
3239
3175
  type: 'guardian_verification',
3240
3176
  action: 'resend_outbound',
3241
3177
  channel: 'telegram',
3242
- assistantId: 'self',
3243
3178
  }, mockSocket, ctx);
3244
3179
 
3245
3180
  const resp = lastResponse();
@@ -3255,7 +3190,6 @@ describe('outbound Telegram verification', () => {
3255
3190
  type: 'guardian_verification',
3256
3191
  action: 'start_outbound',
3257
3192
  channel: 'telegram',
3258
- assistantId: 'self',
3259
3193
  destination: '123456789',
3260
3194
  }, mockSocket, startCtx);
3261
3195
 
@@ -3267,7 +3201,6 @@ describe('outbound Telegram verification', () => {
3267
3201
  type: 'guardian_verification',
3268
3202
  action: 'cancel_outbound',
3269
3203
  channel: 'telegram',
3270
- assistantId: 'self',
3271
3204
  }, mockSocket, ctx);
3272
3205
 
3273
3206
  const resp = lastResponse();
@@ -3313,7 +3246,6 @@ describe('outbound Telegram verification', () => {
3313
3246
  type: 'guardian_verification',
3314
3247
  action: 'start_outbound',
3315
3248
  channel: 'telegram',
3316
- assistantId: 'self',
3317
3249
  }, mockSocket, ctx);
3318
3250
 
3319
3251
  const resp = lastResponse();
@@ -3329,7 +3261,6 @@ describe('outbound Telegram verification', () => {
3329
3261
  type: 'guardian_verification',
3330
3262
  action: 'start_outbound',
3331
3263
  channel: 'telegram',
3332
- assistantId: 'self',
3333
3264
  destination: '123456789',
3334
3265
  }, mockSocket, startCtx);
3335
3266
 
@@ -3349,7 +3280,6 @@ describe('outbound Telegram verification', () => {
3349
3280
  type: 'guardian_verification',
3350
3281
  action: 'resend_outbound',
3351
3282
  channel: 'telegram',
3352
- assistantId: 'self',
3353
3283
  }, mockSocket, ctx);
3354
3284
 
3355
3285
  const resp = lastResponse();
@@ -3365,7 +3295,6 @@ describe('outbound Telegram verification', () => {
3365
3295
  type: 'guardian_verification',
3366
3296
  action: 'start_outbound',
3367
3297
  channel: 'telegram',
3368
- assistantId: 'self',
3369
3298
  destination: '123456789',
3370
3299
  }, mockSocket, startCtx);
3371
3300
 
@@ -3375,7 +3304,6 @@ describe('outbound Telegram verification', () => {
3375
3304
  type: 'guardian_verification',
3376
3305
  action: 'resend_outbound',
3377
3306
  channel: 'telegram',
3378
- assistantId: 'self',
3379
3307
  }, mockSocket, ctx);
3380
3308
 
3381
3309
  const resp = lastResponse();
@@ -3400,7 +3328,6 @@ describe('outbound voice verification', () => {
3400
3328
  type: 'guardian_verification',
3401
3329
  action: 'start_outbound',
3402
3330
  channel: 'voice',
3403
- assistantId: 'self',
3404
3331
  destination: '+15551234567',
3405
3332
  }, mockSocket, ctx);
3406
3333
 
@@ -3439,7 +3366,6 @@ describe('outbound voice verification', () => {
3439
3366
  type: 'guardian_verification',
3440
3367
  action: 'start_outbound',
3441
3368
  channel: 'voice',
3442
- assistantId: 'self',
3443
3369
  destination: 'not-a-phone',
3444
3370
  }, mockSocket, ctx);
3445
3371
 
@@ -3455,7 +3381,6 @@ describe('outbound voice verification', () => {
3455
3381
  type: 'guardian_verification',
3456
3382
  action: 'start_outbound',
3457
3383
  channel: 'voice',
3458
- assistantId: 'self',
3459
3384
  destination: '555-123-4567',
3460
3385
  }, mockSocket, ctx);
3461
3386
 
@@ -3495,7 +3420,6 @@ describe('outbound voice verification', () => {
3495
3420
  type: 'guardian_verification',
3496
3421
  action: 'start_outbound',
3497
3422
  channel: 'voice',
3498
- assistantId: 'self',
3499
3423
  destination: '+15559876543',
3500
3424
  rebind: false,
3501
3425
  }, mockSocket, ctx);
@@ -3513,7 +3437,6 @@ describe('outbound voice verification', () => {
3513
3437
  type: 'guardian_verification',
3514
3438
  action: 'start_outbound',
3515
3439
  channel: 'voice',
3516
- assistantId: 'self',
3517
3440
  destination: '+15551234567',
3518
3441
  }, mockSocket, startCtx);
3519
3442
 
@@ -3523,7 +3446,6 @@ describe('outbound voice verification', () => {
3523
3446
  type: 'guardian_verification',
3524
3447
  action: 'resend_outbound',
3525
3448
  channel: 'voice',
3526
- assistantId: 'self',
3527
3449
  }, mockSocket, ctx);
3528
3450
 
3529
3451
  const resp = lastResponse();
@@ -3539,7 +3461,6 @@ describe('outbound voice verification', () => {
3539
3461
  type: 'guardian_verification',
3540
3462
  action: 'start_outbound',
3541
3463
  channel: 'voice',
3542
- assistantId: 'self',
3543
3464
  destination: '+15551234567',
3544
3465
  }, mockSocket, startCtx);
3545
3466
 
@@ -3549,7 +3470,6 @@ describe('outbound voice verification', () => {
3549
3470
  type: 'guardian_verification',
3550
3471
  action: 'cancel_outbound',
3551
3472
  channel: 'voice',
3552
- assistantId: 'self',
3553
3473
  }, mockSocket, ctx);
3554
3474
 
3555
3475
  const resp = lastResponse();
@@ -3587,7 +3507,6 @@ describe('outbound voice verification', () => {
3587
3507
  type: 'guardian_verification',
3588
3508
  action: 'start_outbound',
3589
3509
  channel: 'voice',
3590
- assistantId: 'self',
3591
3510
  destination: '+15551234567',
3592
3511
  }, mockSocket, ctx);
3593
3512
 
@@ -3603,7 +3522,6 @@ describe('outbound voice verification', () => {
3603
3522
  type: 'guardian_verification',
3604
3523
  action: 'start_outbound',
3605
3524
  channel: 'voice',
3606
- assistantId: 'self',
3607
3525
  }, mockSocket, ctx);
3608
3526
 
3609
3527
  const resp = lastResponse();
@@ -3636,7 +3554,6 @@ describe('M1–M4 hardening coverage', () => {
3636
3554
  type: 'guardian_verification',
3637
3555
  action: 'start_outbound',
3638
3556
  channel: 'sms',
3639
- assistantId: 'self',
3640
3557
  destination: '+15551234567',
3641
3558
  }, mockSocket, ctx);
3642
3559
 
@@ -3657,7 +3574,6 @@ describe('M1–M4 hardening coverage', () => {
3657
3574
  type: 'guardian_verification',
3658
3575
  action: 'start_outbound',
3659
3576
  channel: 'sms',
3660
- assistantId: 'self',
3661
3577
  destination: '+15551234567',
3662
3578
  }, mockSocket, startCtx);
3663
3579
 
@@ -3672,7 +3588,6 @@ describe('M1–M4 hardening coverage', () => {
3672
3588
  type: 'guardian_verification',
3673
3589
  action: 'resend_outbound',
3674
3590
  channel: 'sms',
3675
- assistantId: 'self',
3676
3591
  }, mockSocket, ctx);
3677
3592
 
3678
3593
  const resp = lastResponse();
@@ -3691,7 +3606,6 @@ describe('M1–M4 hardening coverage', () => {
3691
3606
  type: 'guardian_verification',
3692
3607
  action: 'start_outbound',
3693
3608
  channel: 'telegram',
3694
- assistantId: 'self',
3695
3609
  destination: '@someuser',
3696
3610
  }, mockSocket, ctx);
3697
3611