@vellumai/assistant 0.4.9 → 0.4.11

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 (116) hide show
  1. package/ARCHITECTURE.md +24 -0
  2. package/Dockerfile +1 -1
  3. package/README.md +16 -9
  4. package/package.json +1 -1
  5. package/src/__tests__/account-registry.test.ts +1 -0
  6. package/src/__tests__/actor-token-service.test.ts +1 -0
  7. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  8. package/src/__tests__/asset-materialize-tool.test.ts +7 -0
  9. package/src/__tests__/asset-search-tool.test.ts +7 -0
  10. package/src/__tests__/browser-fill-credential.test.ts +1 -0
  11. package/src/__tests__/call-start-guardian-guard.test.ts +1 -0
  12. package/src/__tests__/channel-approval-routes.test.ts +29 -0
  13. package/src/__tests__/channel-guardian.test.ts +2143 -1546
  14. package/src/__tests__/channel-retry-sweep.test.ts +169 -14
  15. package/src/__tests__/claude-code-tool-profiles.test.ts +1 -0
  16. package/src/__tests__/computer-use-tools.test.ts +1 -0
  17. package/src/__tests__/contacts-tools.test.ts +1 -0
  18. package/src/__tests__/conversation-attention-telegram.test.ts +1 -0
  19. package/src/__tests__/credential-policy-validate.test.ts +97 -0
  20. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  21. package/src/__tests__/credential-vault-unit.test.ts +1 -0
  22. package/src/__tests__/credential-vault.test.ts +1 -0
  23. package/src/__tests__/delete-managed-skill-tool.test.ts +1 -0
  24. package/src/__tests__/file-edit-tool.test.ts +1 -0
  25. package/src/__tests__/file-read-tool.test.ts +1 -0
  26. package/src/__tests__/file-write-tool.test.ts +1 -0
  27. package/src/__tests__/followup-tools.test.ts +1 -0
  28. package/src/__tests__/gateway-only-guard.test.ts +1 -1
  29. package/src/__tests__/guardian-control-plane-policy.test.ts +5 -4
  30. package/src/__tests__/guardian-grant-minting.test.ts +3 -0
  31. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +4 -3
  32. package/src/__tests__/guardian-routing-state.test.ts +8 -0
  33. package/src/__tests__/headless-browser-interactions.test.ts +1 -0
  34. package/src/__tests__/headless-browser-navigate.test.ts +1 -0
  35. package/src/__tests__/headless-browser-read-tools.test.ts +1 -0
  36. package/src/__tests__/headless-browser-snapshot.test.ts +1 -0
  37. package/src/__tests__/host-file-edit-tool.test.ts +1 -0
  38. package/src/__tests__/host-file-read-tool.test.ts +1 -0
  39. package/src/__tests__/host-file-write-tool.test.ts +1 -0
  40. package/src/__tests__/host-shell-tool.test.ts +1 -0
  41. package/src/__tests__/lifecycle-docs-guard.test.ts +207 -0
  42. package/src/__tests__/managed-skill-lifecycle.test.ts +1 -0
  43. package/src/__tests__/media-reuse-story.e2e.test.ts +8 -0
  44. package/src/__tests__/messaging-send-tool.test.ts +1 -0
  45. package/src/__tests__/playbook-execution.test.ts +1 -0
  46. package/src/__tests__/playbook-tools.test.ts +1 -0
  47. package/src/__tests__/relay-server.test.ts +4 -0
  48. package/src/__tests__/scaffold-managed-skill-tool.test.ts +1 -0
  49. package/src/__tests__/schedule-tools.test.ts +1 -0
  50. package/src/__tests__/secret-onetime-send.test.ts +4 -0
  51. package/src/__tests__/secret-scanner-executor.test.ts +2 -0
  52. package/src/__tests__/send-notification-tool.test.ts +2 -0
  53. package/src/__tests__/shell-credential-ref.test.ts +1 -0
  54. package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -0
  55. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  56. package/src/__tests__/skill-load-tool.test.ts +1 -0
  57. package/src/__tests__/skill-script-runner-host.test.ts +1 -0
  58. package/src/__tests__/skill-script-runner-sandbox.test.ts +1 -0
  59. package/src/__tests__/skill-script-runner.test.ts +1 -0
  60. package/src/__tests__/skill-tool-factory.test.ts +1 -0
  61. package/src/__tests__/subagent-tools.test.ts +1 -1
  62. package/src/__tests__/swarm-recursion.test.ts +1 -0
  63. package/src/__tests__/swarm-session-integration.test.ts +1 -0
  64. package/src/__tests__/swarm-tool.test.ts +1 -0
  65. package/src/__tests__/task-management-tools.test.ts +1 -0
  66. package/src/__tests__/task-tools.test.ts +1 -0
  67. package/src/__tests__/terminal-tools.test.ts +1 -0
  68. package/src/__tests__/tool-approval-handler.test.ts +2 -2
  69. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  70. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
  71. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
  72. package/src/__tests__/tool-executor-shell-integration.test.ts +1 -0
  73. package/src/__tests__/tool-executor.test.ts +1 -0
  74. package/src/__tests__/trust-context-guards.test.ts +218 -0
  75. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +6 -0
  76. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +6 -0
  77. package/src/__tests__/trusted-contact-multichannel.test.ts +1 -0
  78. package/src/__tests__/trusted-contact-verification.test.ts +1 -0
  79. package/src/__tests__/view-image-tool.test.ts +1 -0
  80. package/src/calls/guardian-dispatch.ts +4 -4
  81. package/src/cli/mcp.ts +183 -3
  82. package/src/config/bundled-skills/agentmail/SKILL.md +4 -4
  83. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +1 -0
  84. package/src/config/bundled-skills/phone-calls/SKILL.md +17 -119
  85. package/src/config/system-prompt.ts +4 -2
  86. package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
  87. package/src/daemon/computer-use-session.ts +1 -0
  88. package/src/daemon/session-agent-loop.ts +1 -1
  89. package/src/daemon/session-memory.ts +2 -2
  90. package/src/daemon/session-runtime-assembly.ts +2 -2
  91. package/src/daemon/session-tool-setup.ts +1 -1
  92. package/src/mcp/client.ts +55 -6
  93. package/src/mcp/manager.ts +9 -0
  94. package/src/mcp/mcp-oauth-provider.ts +347 -0
  95. package/src/memory/channel-delivery-store.ts +1 -0
  96. package/src/memory/db-init.ts +4 -0
  97. package/src/memory/delivery-status.ts +43 -0
  98. package/src/memory/guardian-bindings.ts +3 -3
  99. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +108 -0
  100. package/src/memory/migrations/index.ts +1 -0
  101. package/src/memory/migrations/registry.ts +6 -0
  102. package/src/memory/schema.ts +1 -1
  103. package/src/runtime/actor-trust-resolver.ts +13 -4
  104. package/src/runtime/channel-retry-sweep.ts +31 -14
  105. package/src/runtime/guardian-context-resolver.ts +25 -64
  106. package/src/runtime/guardian-outbound-actions.ts +399 -108
  107. package/src/runtime/guardian-vellum-migration.ts +1 -23
  108. package/src/runtime/guardian-verification-templates.ts +66 -30
  109. package/src/runtime/local-actor-identity.ts +4 -6
  110. package/src/runtime/middleware/actor-token.ts +2 -8
  111. package/src/runtime/routes/channel-route-shared.ts +0 -1
  112. package/src/runtime/routes/inbound-message-handler.ts +3 -4
  113. package/src/runtime/tool-grant-request-helper.ts +1 -1
  114. package/src/tools/credentials/policy-validate.ts +22 -0
  115. package/src/tools/guardian-control-plane-policy.ts +2 -2
  116. package/src/tools/types.ts +1 -1
package/ARCHITECTURE.md CHANGED
@@ -2085,3 +2085,27 @@ The daemon uses a single fixed internal scope constant — `DAEMON_INTERNAL_ASSI
2085
2085
  |------|---------|
2086
2086
  | `src/runtime/assistant-scope.ts` | Exports `DAEMON_INTERNAL_ASSISTANT_ID` constant |
2087
2087
  | `src/__tests__/assistant-id-boundary-guard.test.ts` | Guard tests enforcing the identity boundary |
2088
+
2089
+ ### Canonical Trust-Context Model
2090
+
2091
+ The guardian trust system uses a three-valued `TrustClass` — `'guardian'`, `'trusted_contact'`, or `'unknown'` — as the single vocabulary for actor trust classification across all channels and runtime paths. There is no legacy `actorRole` concept; all trust decisions flow through `TrustClass`.
2092
+
2093
+ **`GuardianRuntimeContext`** (in `src/daemon/session-runtime-assembly.ts`) is the single runtime carrier for trust state on channel-originated turns. It carries `trustClass`, guardian identity fields, and requester metadata. The `guardianPrincipalId` field is typed as `?: string` (optional but non-nullable) — a principal ID is present when a guardian binding exists but is never `null`.
2094
+
2095
+ **Explicit trust gates:** `guardianTrustClass` is a **required** field in `ToolContext` (in `src/tools/types.ts`). Every tool execution must carry a trust classification — the field is not optional. This ensures trust-gated tool policies (guardian control-plane restrictions, host-tool blocking for untrusted actors) cannot be bypassed by omitting the classification.
2096
+
2097
+ **Guardian bindings** (in `src/memory/guardian-bindings.ts`) always carry `guardianPrincipalId: string` as a required, non-null field. A binding without a principal ID is invalid and cannot be created.
2098
+
2099
+ **Strict retry sweep parsing:** The channel retry sweep (`src/runtime/channel-retry-sweep.ts`) uses `parseGuardianRuntimeContext()` which validates `trustClass` against the canonical three-value set. There is no fallback to a legacy `actorRole` field — stored payloads that lack a valid `trustClass` are rejected deterministically to prevent silent privilege escalation. When `guardianCtx` is entirely absent from a stored payload (pre-guardian events), the sweep synthesizes an explicit `trustClass: 'unknown'` context so that replay never proceeds without a trust classification.
2100
+
2101
+ **Rollout note — legacy `actorRole` payloads:** Previously failed events stored with only `actorRole` (no `trustClass`) will be marked as failed on each retry attempt and eventually dead-lettered after exhausting `RETRY_MAX_ATTEMPTS`. This is an intentional security tradeoff: replaying these events with inferred trust would violate the explicit-trust model. If legacy events need to be recovered, they should be repaired (adding a canonical `trustClass` to the stored payload) before replay via `replayDeadLetters()`.
2102
+
2103
+ **Key files:**
2104
+
2105
+ | File | Purpose |
2106
+ |------|---------|
2107
+ | `src/daemon/session-runtime-assembly.ts` | `GuardianRuntimeContext` type definition |
2108
+ | `src/tools/types.ts` | `ToolContext.guardianTrustClass` (required trust gate) |
2109
+ | `src/runtime/channel-retry-sweep.ts` | Strict `trustClass` parser for retry sweep |
2110
+ | `src/memory/guardian-bindings.ts` | `GuardianBinding` with required `guardianPrincipalId` |
2111
+ | `src/__tests__/trust-context-guards.test.ts` | Guard tests enforcing trust-context type invariants |
package/Dockerfile CHANGED
@@ -87,7 +87,7 @@ RUN echo 'Dir::State "/data/dpkg";' > /etc/apt/apt.conf.d/99data-dir && \
87
87
  chown -R assistant:assistant /data/apt /data/dpkg
88
88
 
89
89
  ENV PATH="/data/usr/bin:/data/usr/sbin:${PATH}"
90
- ENV LD_LIBRARY_PATH="/data/usr/lib:/data/usr/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH}"
90
+ ENV LD_LIBRARY_PATH="/data/usr/lib:/data/usr/lib/x86_64-linux-gnu:/data/usr/lib/aarch64-linux-gnu:${LD_LIBRARY_PATH}"
91
91
 
92
92
  USER root
93
93
 
package/README.md CHANGED
@@ -58,30 +58,37 @@ When a release includes relevant updates, the daemon materializes release notes
58
58
 
59
59
  ## Usage
60
60
 
61
- ### Start the daemon
61
+ ### Lifecycle management (recommended)
62
+
63
+ Use the `vellum` CLI to manage daemon and gateway processes:
62
64
 
63
65
  ```bash
64
- bun run src/index.ts daemon start
66
+ vellum wake # start daemon + gateway from current checkout
67
+ vellum ps # list assistants and per-assistant process status
68
+ vellum sleep # stop daemon + gateway (directory-agnostic)
65
69
  ```
66
70
 
67
- ### Interactive CLI
71
+ > **Note:** `vellum wake` requires a hatched assistant. Run `vellum hatch` first, or launch the macOS app which handles hatching automatically.
68
72
 
69
- ```bash
70
- bun run src/index.ts
71
- ```
73
+ ### Development: raw bun commands
72
74
 
73
- ### Dev mode (auto-restart on file changes)
75
+ For low-level development (e.g., working on the daemon itself):
74
76
 
75
77
  ```bash
76
- bun run src/index.ts dev
78
+ bun run src/index.ts daemon start # start daemon only
79
+ bun run src/index.ts # interactive CLI session
80
+ bun run src/index.ts dev # dev mode (auto-restart on file changes)
77
81
  ```
78
82
 
79
83
  ### CLI commands
80
84
 
81
85
  | Command | Description |
82
86
  |---------|-------------|
87
+ | `vellum wake` | Start daemon + gateway from current checkout |
88
+ | `vellum sleep` | Stop daemon + gateway processes |
89
+ | `vellum ps` | List assistants and per-assistant process status |
83
90
  | `vellum` | Launch interactive CLI session |
84
- | `vellum daemon start\|stop\|restart\|status` | Manage the daemon process |
91
+ | `vellum daemon start\|stop\|restart\|status` | Manage the daemon process (low-level) |
85
92
  | `vellum dev` | Run daemon with auto-restart on file changes |
86
93
  | `vellum sessions list\|new\|export\|clear` | Manage conversation sessions |
87
94
  | `vellum config set\|get\|list` | Manage configuration |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "vellum": "./src/index.ts"
@@ -45,6 +45,7 @@ const _ctx: ToolContext = {
45
45
  workingDir: '/tmp',
46
46
  sessionId: 'test-session',
47
47
  conversationId: 'test-conv',
48
+ guardianTrustClass: 'guardian',
48
49
  };
49
50
 
50
51
  afterAll(() => {
@@ -340,6 +340,7 @@ describe('guardian vellum migration', () => {
340
340
  channel: 'telegram',
341
341
  guardianExternalUserId: 'tg-user-123',
342
342
  guardianDeliveryChatId: 'tg-chat-456',
343
+ guardianPrincipalId: 'tg-user-123',
343
344
  verifiedVia: 'challenge',
344
345
  });
345
346
 
@@ -53,6 +53,7 @@ function makeContext(overrides: Partial<ToolContext> = {}): ToolContext {
53
53
  workingDir: '/tmp',
54
54
  sessionId: 'session-1',
55
55
  conversationId: 'conv-1',
56
+ guardianTrustClass: 'guardian',
56
57
  ...overrides,
57
58
  };
58
59
  }
@@ -69,6 +69,7 @@ const dummyContext: ToolContext = {
69
69
  workingDir: sandboxDir,
70
70
  sessionId: 'sess-test',
71
71
  conversationId: 'conv-test',
72
+ guardianTrustClass: 'guardian',
72
73
  };
73
74
 
74
75
  // ---------------------------------------------------------------------------
@@ -322,6 +323,7 @@ describe('AssetMaterializeTool visibility policy', () => {
322
323
  workingDir: sandboxDir,
323
324
  sessionId: 'sess-test',
324
325
  conversationId: otherConv.id,
326
+ guardianTrustClass: 'guardian',
325
327
  };
326
328
 
327
329
  const result = await assetMaterializeTool.execute(
@@ -344,6 +346,7 @@ describe('AssetMaterializeTool visibility policy', () => {
344
346
  workingDir: sandboxDir,
345
347
  sessionId: 'sess-test',
346
348
  conversationId: privateConv.id,
349
+ guardianTrustClass: 'guardian',
347
350
  };
348
351
 
349
352
  const result = await assetMaterializeTool.execute(
@@ -367,6 +370,7 @@ describe('AssetMaterializeTool visibility policy', () => {
367
370
  workingDir: sandboxDir,
368
371
  sessionId: 'sess-test',
369
372
  conversationId: otherConv.id,
373
+ guardianTrustClass: 'guardian',
370
374
  };
371
375
 
372
376
  const result = await assetMaterializeTool.execute(
@@ -391,6 +395,7 @@ describe('AssetMaterializeTool visibility policy', () => {
391
395
  workingDir: sandboxDir,
392
396
  sessionId: 'sess-test',
393
397
  conversationId: standardConv.id,
398
+ guardianTrustClass: 'guardian',
394
399
  };
395
400
 
396
401
  const result = await assetMaterializeTool.execute(
@@ -417,6 +422,7 @@ describe('AssetMaterializeTool visibility policy', () => {
417
422
  workingDir: sandboxDir,
418
423
  sessionId: 'sess-test',
419
424
  conversationId: privateConv2.id,
425
+ guardianTrustClass: 'guardian',
420
426
  };
421
427
 
422
428
  const result = await assetMaterializeTool.execute(
@@ -444,6 +450,7 @@ describe('AssetMaterializeTool visibility policy', () => {
444
450
  workingDir: sandboxDir,
445
451
  sessionId: 'sess-test',
446
452
  conversationId: otherConv.id,
453
+ guardianTrustClass: 'guardian',
447
454
  };
448
455
 
449
456
  const result = await assetMaterializeTool.execute(
@@ -88,6 +88,7 @@ const dummyContext: ToolContext = {
88
88
  workingDir: '/tmp',
89
89
  sessionId: 'sess-test',
90
90
  conversationId: 'conv-test',
91
+ guardianTrustClass: 'guardian',
91
92
  };
92
93
 
93
94
  // ---------------------------------------------------------------------------
@@ -378,6 +379,7 @@ describe('AssetSearchTool visibility policy', () => {
378
379
  workingDir: '/tmp',
379
380
  sessionId: 'sess-test',
380
381
  conversationId: otherConv.id,
382
+ guardianTrustClass: 'guardian',
381
383
  };
382
384
 
383
385
  const result = await assetSearchTool.execute({}, context);
@@ -396,6 +398,7 @@ describe('AssetSearchTool visibility policy', () => {
396
398
  workingDir: '/tmp',
397
399
  sessionId: 'sess-test',
398
400
  conversationId: privateConv.id,
401
+ guardianTrustClass: 'guardian',
399
402
  };
400
403
 
401
404
  const result = await assetSearchTool.execute({}, context);
@@ -415,6 +418,7 @@ describe('AssetSearchTool visibility policy', () => {
415
418
  workingDir: '/tmp',
416
419
  sessionId: 'sess-test',
417
420
  conversationId: otherPrivateConv.id,
421
+ guardianTrustClass: 'guardian',
418
422
  };
419
423
 
420
424
  const result = await assetSearchTool.execute({}, context);
@@ -434,6 +438,7 @@ describe('AssetSearchTool visibility policy', () => {
434
438
  workingDir: '/tmp',
435
439
  sessionId: 'sess-test',
436
440
  conversationId: standardConv.id,
441
+ guardianTrustClass: 'guardian',
437
442
  };
438
443
 
439
444
  const result = await assetSearchTool.execute({}, context);
@@ -457,6 +462,7 @@ describe('AssetSearchTool visibility policy', () => {
457
462
  workingDir: '/tmp',
458
463
  sessionId: 'sess-test',
459
464
  conversationId: otherConv.id,
465
+ guardianTrustClass: 'guardian',
460
466
  };
461
467
 
462
468
  const result = await assetSearchTool.execute({}, context);
@@ -472,6 +478,7 @@ describe('AssetSearchTool visibility policy', () => {
472
478
  workingDir: '/tmp',
473
479
  sessionId: 'sess-test',
474
480
  conversationId: conv.id,
481
+ guardianTrustClass: 'guardian',
475
482
  };
476
483
 
477
484
  const result = await assetSearchTool.execute({}, context);
@@ -81,6 +81,7 @@ const ctx: ToolContext = {
81
81
  sessionId: 'test-session',
82
82
  conversationId: 'test-conversation',
83
83
  workingDir: '/tmp',
84
+ guardianTrustClass: 'guardian',
84
85
  };
85
86
 
86
87
  function resetMockPage() {
@@ -45,6 +45,7 @@ function makeContext(): ToolContext {
45
45
  sessionId: 'session-1',
46
46
  conversationId: 'conversation-1',
47
47
  assistantId: 'self',
48
+ guardianTrustClass: 'guardian',
48
49
  };
49
50
  }
50
51
 
@@ -246,6 +246,7 @@ describe('inbound callback metadata triggers decision handling', () => {
246
246
  channel: 'telegram',
247
247
  guardianExternalUserId: 'telegram-user-default',
248
248
  guardianDeliveryChatId: 'chat-123',
249
+ guardianPrincipalId: 'telegram-user-default',
249
250
  });
250
251
  });
251
252
 
@@ -321,6 +322,7 @@ describe('inbound text matching approval phrases triggers decision handling', ()
321
322
  channel: 'telegram',
322
323
  guardianExternalUserId: 'telegram-user-default',
323
324
  guardianDeliveryChatId: 'chat-123',
325
+ guardianPrincipalId: 'telegram-user-default',
324
326
  });
325
327
  });
326
328
 
@@ -384,6 +386,7 @@ describe('non-decision messages during pending approval (legacy fallback)', () =
384
386
  channel: 'telegram',
385
387
  guardianExternalUserId: 'telegram-user-default',
386
388
  guardianDeliveryChatId: 'chat-123',
389
+ guardianPrincipalId: 'telegram-user-default',
387
390
  });
388
391
  });
389
392
 
@@ -458,6 +461,7 @@ describe('empty content with callbackData bypasses validation', () => {
458
461
  channel: 'telegram',
459
462
  guardianExternalUserId: 'telegram-user-default',
460
463
  guardianDeliveryChatId: 'chat-123',
464
+ guardianPrincipalId: 'telegram-user-default',
461
465
  });
462
466
  });
463
467
 
@@ -551,6 +555,7 @@ describe('callback requestId validation', () => {
551
555
  channel: 'telegram',
552
556
  guardianExternalUserId: 'telegram-user-default',
553
557
  guardianDeliveryChatId: 'chat-123',
558
+ guardianPrincipalId: 'telegram-user-default',
554
559
  });
555
560
  });
556
561
 
@@ -651,6 +656,7 @@ describe('no immediate reply after approval decision', () => {
651
656
  channel: 'telegram',
652
657
  guardianExternalUserId: 'telegram-user-default',
653
658
  guardianDeliveryChatId: 'chat-123',
659
+ guardianPrincipalId: 'telegram-user-default',
654
660
  });
655
661
  });
656
662
 
@@ -773,6 +779,7 @@ describe('SMS channel approval decisions', () => {
773
779
  channel: 'sms',
774
780
  guardianExternalUserId: 'sms-user-default',
775
781
  guardianDeliveryChatId: 'sms-chat-123',
782
+ guardianPrincipalId: 'sms-user-default',
776
783
  });
777
784
  });
778
785
 
@@ -1026,6 +1033,7 @@ describe('guardian decision scoping — multiple pending approvals', () => {
1026
1033
  channel: 'telegram',
1027
1034
  guardianExternalUserId: 'guardian-scope-user',
1028
1035
  guardianDeliveryChatId: 'guardian-scope-chat',
1036
+ guardianPrincipalId: 'guardian-scope-user',
1029
1037
  });
1030
1038
 
1031
1039
  const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
@@ -1099,6 +1107,7 @@ describe('ambiguous plain-text decision with multiple pending requests', () => {
1099
1107
  channel: 'telegram',
1100
1108
  guardianExternalUserId: 'guardian-ambig-user',
1101
1109
  guardianDeliveryChatId: 'guardian-ambig-chat',
1110
+ guardianPrincipalId: 'guardian-ambig-user',
1102
1111
  });
1103
1112
 
1104
1113
  const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
@@ -1548,6 +1557,7 @@ describe('conversational approval engine — standard path', () => {
1548
1557
  channel: 'telegram',
1549
1558
  guardianExternalUserId: 'telegram-user-default',
1550
1559
  guardianDeliveryChatId: 'chat-123',
1560
+ guardianPrincipalId: 'telegram-user-default',
1551
1561
  });
1552
1562
  });
1553
1563
 
@@ -1717,6 +1727,7 @@ describe('guardian conversational approval via conversation engine', () => {
1717
1727
  channel: 'telegram',
1718
1728
  guardianExternalUserId: 'guardian-conv-user',
1719
1729
  guardianDeliveryChatId: 'guardian-conv-chat',
1730
+ guardianPrincipalId: 'guardian-conv-user',
1720
1731
  });
1721
1732
 
1722
1733
  const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
@@ -1781,6 +1792,7 @@ describe('guardian conversational approval via conversation engine', () => {
1781
1792
  channel: 'telegram',
1782
1793
  guardianExternalUserId: 'guardian-nlp-user',
1783
1794
  guardianDeliveryChatId: 'guardian-nlp-chat',
1795
+ guardianPrincipalId: 'guardian-nlp-user',
1784
1796
  });
1785
1797
 
1786
1798
  const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
@@ -1843,6 +1855,7 @@ describe('guardian conversational approval via conversation engine', () => {
1843
1855
  channel: 'telegram',
1844
1856
  guardianExternalUserId: 'guardian-dg-user',
1845
1857
  guardianDeliveryChatId: 'guardian-dg-chat',
1858
+ guardianPrincipalId: 'guardian-dg-user',
1846
1859
  });
1847
1860
 
1848
1861
  const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
@@ -1892,6 +1905,7 @@ describe('guardian conversational approval via conversation engine', () => {
1892
1905
  channel: 'telegram',
1893
1906
  guardianExternalUserId: 'guardian-multi-user',
1894
1907
  guardianDeliveryChatId: 'guardian-multi-chat',
1908
+ guardianPrincipalId: 'guardian-multi-user',
1895
1909
  });
1896
1910
 
1897
1911
  const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
@@ -1981,6 +1995,7 @@ describe('keep_pending remains conversational — standard path', () => {
1981
1995
  channel: 'telegram',
1982
1996
  guardianExternalUserId: 'telegram-user-default',
1983
1997
  guardianDeliveryChatId: 'chat-123',
1998
+ guardianPrincipalId: 'telegram-user-default',
1984
1999
  });
1985
2000
  });
1986
2001
 
@@ -2029,6 +2044,7 @@ describe('keep_pending remains conversational — guardian path', () => {
2029
2044
  channel: 'telegram',
2030
2045
  guardianExternalUserId: 'guardian-user-fb',
2031
2046
  guardianDeliveryChatId: 'guardian-chat-fb',
2047
+ guardianPrincipalId: 'guardian-user-fb',
2032
2048
  });
2033
2049
 
2034
2050
  const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
@@ -2091,6 +2107,7 @@ describe('requester cancel of guardian-gated pending request', () => {
2091
2107
  channel: 'telegram',
2092
2108
  guardianExternalUserId: 'guardian-cancel',
2093
2109
  guardianDeliveryChatId: 'guardian-cancel-chat',
2110
+ guardianPrincipalId: 'guardian-cancel',
2094
2111
  });
2095
2112
  });
2096
2113
 
@@ -2346,6 +2363,7 @@ describe('engine decision race condition — standard path', () => {
2346
2363
  channel: 'telegram',
2347
2364
  guardianExternalUserId: 'telegram-user-default',
2348
2365
  guardianDeliveryChatId: 'chat-123',
2366
+ guardianPrincipalId: 'telegram-user-default',
2349
2367
  });
2350
2368
  });
2351
2369
 
@@ -2407,6 +2425,7 @@ describe('engine decision race condition — guardian path', () => {
2407
2425
  channel: 'telegram',
2408
2426
  guardianExternalUserId: 'guardian-race-user',
2409
2427
  guardianDeliveryChatId: 'guardian-race-chat',
2428
+ guardianPrincipalId: 'guardian-race-user',
2410
2429
  });
2411
2430
 
2412
2431
  const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
@@ -2482,12 +2501,14 @@ describe('non-decision status reply for different channels', () => {
2482
2501
  channel: 'telegram',
2483
2502
  guardianExternalUserId: 'telegram-user-default',
2484
2503
  guardianDeliveryChatId: 'chat-123',
2504
+ guardianPrincipalId: 'telegram-user-default',
2485
2505
  });
2486
2506
  createBinding({
2487
2507
  assistantId: 'self',
2488
2508
  channel: 'sms',
2489
2509
  guardianExternalUserId: 'telegram-user-default',
2490
2510
  guardianDeliveryChatId: 'chat-123',
2511
+ guardianPrincipalId: 'telegram-user-default',
2491
2512
  });
2492
2513
  });
2493
2514
 
@@ -2572,6 +2593,7 @@ describe('background channel processing approval prompts', () => {
2572
2593
  channel: 'telegram',
2573
2594
  guardianExternalUserId: 'telegram-user-default',
2574
2595
  guardianDeliveryChatId: 'chat-123',
2596
+ guardianPrincipalId: 'telegram-user-default',
2575
2597
  });
2576
2598
 
2577
2599
  const deliverPromptSpy = spyOn(gatewayClient, 'deliverApprovalPrompt').mockResolvedValue(undefined);
@@ -2625,6 +2647,7 @@ describe('background channel processing approval prompts', () => {
2625
2647
  channel: 'telegram',
2626
2648
  guardianExternalUserId: ' telegram-user-default ',
2627
2649
  guardianDeliveryChatId: 'chat-123',
2650
+ guardianPrincipalId: ' telegram-user-default ',
2628
2651
  });
2629
2652
 
2630
2653
  const deliverPromptSpy = spyOn(gatewayClient, 'deliverApprovalPrompt').mockResolvedValue(undefined);
@@ -2676,6 +2699,7 @@ describe('background channel processing approval prompts', () => {
2676
2699
  channel: 'telegram',
2677
2700
  guardianExternalUserId: 'guardian-user-other',
2678
2701
  guardianDeliveryChatId: 'guardian-chat-other',
2702
+ guardianPrincipalId: 'guardian-user-other',
2679
2703
  });
2680
2704
 
2681
2705
  const processCalls: Array<{ options?: Record<string, unknown> }> = [];
@@ -2779,6 +2803,8 @@ describe('NL approval routing via destination-scoped canonical requests', () =>
2779
2803
  channel: 'telegram',
2780
2804
  guardianExternalUserId: guardianUserId,
2781
2805
  guardianDeliveryChatId: guardianChatId,
2806
+
2807
+ guardianPrincipalId: guardianUserId,
2782
2808
  });
2783
2809
 
2784
2810
  // Create canonical tool_approval request WITHOUT guardianExternalUserId
@@ -2835,6 +2861,8 @@ describe('NL approval routing via destination-scoped canonical requests', () =>
2835
2861
  channel: 'telegram',
2836
2862
  guardianExternalUserId: guardianUserId,
2837
2863
  guardianDeliveryChatId: differentChatId,
2864
+
2865
+ guardianPrincipalId: guardianUserId,
2838
2866
  });
2839
2867
 
2840
2868
  // Create canonical pending_question WITHOUT guardianExternalUserId
@@ -2890,6 +2918,7 @@ describe('trusted-contact self-approval blocked before guardian approval row exi
2890
2918
  channel: 'telegram',
2891
2919
  guardianExternalUserId: 'guardian-tc-selfapproval',
2892
2920
  guardianDeliveryChatId: 'guardian-tc-selfapproval-chat',
2921
+ guardianPrincipalId: 'guardian-tc-selfapproval',
2893
2922
  });
2894
2923
  });
2895
2924