@vellumai/assistant 0.4.32 → 0.4.34

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 (186) hide show
  1. package/docs/architecture/memory.md +1 -1
  2. package/package.json +1 -1
  3. package/src/__tests__/access-request-decision.test.ts +85 -4
  4. package/src/__tests__/actor-token-service.test.ts +4 -12
  5. package/src/__tests__/approval-primitive.test.ts +0 -45
  6. package/src/__tests__/approval-routes-http.test.ts +0 -1
  7. package/src/__tests__/assistant-id-boundary-guard.test.ts +150 -0
  8. package/src/__tests__/call-controller.test.ts +0 -1
  9. package/src/__tests__/call-routes-http.test.ts +0 -1
  10. package/src/__tests__/callback-handoff-copy.test.ts +0 -1
  11. package/src/__tests__/channel-approval-routes.test.ts +5 -45
  12. package/src/__tests__/channel-guardian.test.ts +122 -346
  13. package/src/__tests__/channel-invite-transport.test.ts +52 -40
  14. package/src/__tests__/commit-message-enrichment-service.test.ts +4 -38
  15. package/src/__tests__/computer-use-session-working-dir.test.ts +0 -1
  16. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +4 -3
  17. package/src/__tests__/contacts-tools.test.ts +4 -5
  18. package/src/__tests__/conversation-attention-store.test.ts +2 -65
  19. package/src/__tests__/conversation-attention-telegram.test.ts +0 -2
  20. package/src/__tests__/conversation-pairing.test.ts +0 -1
  21. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  22. package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -3
  23. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -7
  24. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -1
  25. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -74
  26. package/src/__tests__/guardian-action-late-reply.test.ts +1 -8
  27. package/src/__tests__/guardian-dispatch.test.ts +0 -1
  28. package/src/__tests__/guardian-grant-minting.test.ts +0 -1
  29. package/src/__tests__/guardian-outbound-http.test.ts +0 -1
  30. package/src/__tests__/guardian-routing-state.test.ts +0 -3
  31. package/src/__tests__/handlers-telegram-config.test.ts +0 -1
  32. package/src/__tests__/inbound-invite-redemption.test.ts +1 -7
  33. package/src/__tests__/ingress-reconcile.test.ts +3 -36
  34. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1
  35. package/src/__tests__/migration-export-http.test.ts +0 -1
  36. package/src/__tests__/migration-import-commit-http.test.ts +0 -1
  37. package/src/__tests__/migration-import-preflight-http.test.ts +0 -1
  38. package/src/__tests__/migration-validate-http.test.ts +0 -1
  39. package/src/__tests__/non-member-access-request.test.ts +0 -8
  40. package/src/__tests__/notification-broadcaster.test.ts +1 -2
  41. package/src/__tests__/notification-decision-fallback.test.ts +0 -2
  42. package/src/__tests__/notification-decision-strategy.test.ts +0 -1
  43. package/src/__tests__/notification-guardian-path.test.ts +0 -1
  44. package/src/__tests__/notification-telegram-adapter.test.ts +0 -4
  45. package/src/__tests__/relay-server.test.ts +151 -80
  46. package/src/__tests__/sandbox-host-parity.test.ts +5 -2
  47. package/src/__tests__/scoped-approval-grants.test.ts +9 -40
  48. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -36
  49. package/src/__tests__/send-endpoint-busy.test.ts +0 -1
  50. package/src/__tests__/send-notification-tool.test.ts +0 -1
  51. package/src/__tests__/session-init.benchmark.test.ts +0 -2
  52. package/src/__tests__/slack-channel-config.test.ts +0 -1
  53. package/src/__tests__/slack-inbound-verification.test.ts +2 -5
  54. package/src/__tests__/sms-messaging-provider.test.ts +0 -4
  55. package/src/__tests__/terminal-tools.test.ts +5 -2
  56. package/src/__tests__/thread-seed-composer.test.ts +0 -1
  57. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  58. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -4
  59. package/src/__tests__/trusted-contact-approval-notifier.test.ts +65 -77
  60. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  61. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +1 -18
  62. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -14
  63. package/src/__tests__/trusted-contact-verification.test.ts +3 -16
  64. package/src/__tests__/twilio-routes.test.ts +2 -3
  65. package/src/__tests__/update-bulletin.test.ts +0 -2
  66. package/src/__tests__/user-reference.test.ts +47 -1
  67. package/src/__tests__/voice-invite-redemption.test.ts +0 -1
  68. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -38
  69. package/src/__tests__/workspace-git-service.test.ts +2 -2
  70. package/src/approvals/approval-primitive.ts +0 -15
  71. package/src/approvals/guardian-decision-primitive.ts +0 -3
  72. package/src/approvals/guardian-request-resolvers.ts +0 -5
  73. package/src/calls/call-domain.ts +0 -3
  74. package/src/calls/call-store.ts +0 -3
  75. package/src/calls/guardian-action-sweep.ts +2 -1
  76. package/src/calls/guardian-dispatch.ts +1 -2
  77. package/src/calls/relay-access-wait.ts +0 -4
  78. package/src/calls/relay-server.ts +8 -66
  79. package/src/calls/relay-setup-router.ts +1 -2
  80. package/src/calls/relay-verification.ts +0 -1
  81. package/src/calls/twilio-routes.ts +0 -3
  82. package/src/calls/types.ts +0 -1
  83. package/src/calls/voice-session-bridge.ts +0 -1
  84. package/src/channels/config.ts +41 -2
  85. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -1
  86. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  87. package/src/config/bundled-skills/slack-digest-setup/SKILL.md +164 -0
  88. package/src/config/env.ts +0 -4
  89. package/src/config/feature-flag-registry.json +4 -4
  90. package/src/config/user-reference.ts +47 -9
  91. package/src/contacts/contact-store.ts +13 -88
  92. package/src/contacts/contacts-write.ts +3 -11
  93. package/src/contacts/types.ts +0 -1
  94. package/src/daemon/handlers/config-channels.ts +19 -44
  95. package/src/daemon/handlers/config-inbox.ts +6 -6
  96. package/src/daemon/handlers/contacts.ts +8 -12
  97. package/src/daemon/handlers/index.ts +0 -2
  98. package/src/daemon/lifecycle.ts +18 -26
  99. package/src/daemon/session-process.ts +0 -4
  100. package/src/memory/channel-delivery-store.ts +1 -0
  101. package/src/memory/conversation-attention-store.ts +4 -19
  102. package/src/memory/conversation-crud.ts +0 -2
  103. package/src/memory/db-init.ts +8 -0
  104. package/src/memory/delivery-crud.ts +13 -0
  105. package/src/memory/guardian-action-store.ts +0 -12
  106. package/src/memory/guardian-approvals.ts +35 -80
  107. package/src/memory/guardian-rate-limits.ts +1 -14
  108. package/src/memory/guardian-verification.ts +6 -34
  109. package/src/memory/invite-store.ts +76 -15
  110. package/src/memory/migrations/040-invite-code-hash-column.ts +16 -0
  111. package/src/memory/migrations/134-contacts-notes-column.ts +64 -45
  112. package/src/memory/migrations/136-drop-assistant-id-columns.ts +263 -0
  113. package/src/memory/migrations/index.ts +2 -0
  114. package/src/memory/migrations/registry.ts +14 -1
  115. package/src/memory/schema/calls.ts +0 -7
  116. package/src/memory/schema/contacts.ts +2 -8
  117. package/src/memory/schema/guardian.ts +0 -5
  118. package/src/memory/schema/infrastructure.ts +0 -2
  119. package/src/memory/schema/notifications.ts +3 -17
  120. package/src/memory/scoped-approval-grants.ts +2 -24
  121. package/src/notifications/adapters/sms.ts +2 -1
  122. package/src/notifications/broadcaster.ts +1 -6
  123. package/src/notifications/decision-engine.ts +3 -4
  124. package/src/notifications/deliveries-store.ts +0 -4
  125. package/src/notifications/destination-resolver.ts +4 -6
  126. package/src/notifications/deterministic-checks.ts +1 -6
  127. package/src/notifications/emit-signal.ts +4 -11
  128. package/src/notifications/events-store.ts +7 -17
  129. package/src/notifications/preference-summary.ts +2 -2
  130. package/src/notifications/preferences-store.ts +2 -9
  131. package/src/notifications/signal.ts +0 -1
  132. package/src/notifications/thread-candidates.ts +1 -11
  133. package/src/notifications/types.ts +0 -3
  134. package/src/runtime/access-request-helper.ts +3 -10
  135. package/src/runtime/actor-refresh-token-store.ts +0 -6
  136. package/src/runtime/actor-token-store.ts +3 -16
  137. package/src/runtime/actor-trust-resolver.ts +1 -4
  138. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -9
  139. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -3
  140. package/src/runtime/auth/credential-service.ts +1 -15
  141. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  142. package/src/runtime/auth/token-service.ts +50 -0
  143. package/src/runtime/channel-guardian-service.ts +16 -49
  144. package/src/runtime/channel-invite-transport.ts +129 -34
  145. package/src/runtime/channel-invite-transports/email.ts +54 -0
  146. package/src/runtime/channel-invite-transports/slack.ts +87 -0
  147. package/src/runtime/channel-invite-transports/sms.ts +74 -0
  148. package/src/runtime/channel-invite-transports/telegram.ts +35 -11
  149. package/src/runtime/channel-invite-transports/voice.ts +12 -12
  150. package/src/runtime/confirmation-request-guardian-bridge.ts +0 -1
  151. package/src/runtime/guardian-action-followup-executor.ts +3 -2
  152. package/src/runtime/guardian-action-grant-minter.ts +0 -1
  153. package/src/runtime/guardian-outbound-actions.ts +2 -12
  154. package/src/runtime/guardian-vellum-migration.ts +2 -3
  155. package/src/runtime/http-server.ts +0 -1
  156. package/src/runtime/invite-redemption-service.ts +191 -11
  157. package/src/runtime/invite-redemption-templates.ts +6 -6
  158. package/src/runtime/invite-service.ts +81 -11
  159. package/src/runtime/local-actor-identity.ts +2 -5
  160. package/src/runtime/routes/access-request-decision.ts +52 -7
  161. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -9
  162. package/src/runtime/routes/channel-readiness-routes.ts +29 -18
  163. package/src/runtime/routes/contact-routes.ts +48 -46
  164. package/src/runtime/routes/conversation-attention-routes.ts +0 -2
  165. package/src/runtime/routes/global-search-routes.ts +0 -2
  166. package/src/runtime/routes/guardian-bootstrap-routes.ts +6 -12
  167. package/src/runtime/routes/guardian-expiry-sweep.ts +3 -2
  168. package/src/runtime/routes/inbound-message-handler.ts +1 -6
  169. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +296 -47
  170. package/src/runtime/routes/inbound-stages/background-dispatch.ts +6 -42
  171. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -6
  172. package/src/runtime/routes/inbound-stages/edit-intercept.ts +10 -0
  173. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +0 -1
  174. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +0 -1
  175. package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -7
  176. package/src/runtime/routes/invite-routes.ts +1 -0
  177. package/src/runtime/routes/pairing-routes.ts +4 -4
  178. package/src/runtime/tool-grant-request-helper.ts +0 -1
  179. package/src/tools/browser/browser-manager.ts +22 -12
  180. package/src/tools/browser/runtime-check.ts +110 -3
  181. package/src/tools/calls/call-start.ts +1 -3
  182. package/src/tools/followups/followup_create.ts +1 -2
  183. package/src/tools/shared/shell-output.ts +7 -2
  184. package/src/tools/tool-approval-handler.ts +0 -2
  185. package/src/util/platform.ts +0 -4
  186. package/src/workspace/git-service.ts +10 -4
@@ -487,7 +487,7 @@ graph TB
487
487
 
488
488
  ### How it works
489
489
 
490
- 1. **Lazy initialization**: The git repository is created on first use, not at workspace creation. When `ensureInitialized()` is called, it checks for a `.git` directory. If absent, it runs `git init`, creates a `.gitignore` (excluding `data/`, `logs/`, `*.log`, `*.sock`, `*.pid`, `session-token`, `http-token`), sets the git identity to "Vellum Assistant", and creates an initial baseline commit capturing any pre-existing files. The baseline commit is intentional — it makes `git log`, `git diff`, and `git revert` work cleanly from the start. Both new and existing workspaces get the same treatment. For existing repos (e.g. created by older versions or external tools), `.gitignore` rules and git identity are set idempotently on each init, ensuring proper configuration regardless of how the repo was originally created.
490
+ 1. **Lazy initialization**: The git repository is created on first use, not at workspace creation. When `ensureInitialized()` is called, it checks for a `.git` directory. If absent, it runs `git init`, creates a `.gitignore` (excluding `data/`, `logs/`, `*.log`, `*.sock`, `*.pid`, `session-token`), sets the git identity to "Vellum Assistant", and creates an initial baseline commit capturing any pre-existing files. The baseline commit is intentional — it makes `git log`, `git diff`, and `git revert` work cleanly from the start. Both new and existing workspaces get the same treatment. For existing repos (e.g. created by older versions or external tools), `.gitignore` rules and git identity are set idempotently on each init, ensuring proper configuration regardless of how the repo was originally created.
491
491
 
492
492
  2. **Turn-boundary commits**: After each conversation turn (user message + assistant response cycle), `session.ts` commits workspace changes via `commitTurnChanges(workspaceDir, sessionId, turnNumber)`. The commit runs in the `finally` block of `runAgentLoop`, guarded by a `turnStarted` flag that is set once the agent loop begins executing. This guarantees a commit attempt even when post-processing (e.g. `resolveAssistantAttachments`) throws, or when the user cancels mid-turn. The commit is raced against a configurable timeout (`workspaceGit.turnCommitMaxWaitMs`, default 4s) via `Promise.race`. If the commit exceeds the timeout, the turn proceeds immediately while the commit continues in the background. Note: the background commit is NOT awaited before the next turn starts, so brief cross-turn file attribution windows are possible but accepted as a tradeoff for responsiveness. Commit outcomes are logged with structured fields (`sessionId`, `turnNumber`, `filesChanged`, `durationMs`) for observability.
493
493
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.4.32",
3
+ "version": "0.4.34",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "vellum": "./src/index.ts"
@@ -30,7 +30,6 @@ mock.module("../util/platform.js", () => ({
30
30
  getDbPath: () => join(testDir, "test.db"),
31
31
  getLogPath: () => join(testDir, "test.log"),
32
32
  ensureDataDir: () => {},
33
- readHttpToken: () => "test-bearer-token",
34
33
  }));
35
34
 
36
35
  mock.module("../util/logger.js", () => ({
@@ -100,7 +99,6 @@ function createTestApproval(overrides: Record<string, unknown> = {}) {
100
99
  return createApprovalRequest({
101
100
  runId: `ingress-access-request-${Date.now()}`,
102
101
  conversationId: `access-req-telegram-user-unknown-456`,
103
- assistantId: "self",
104
102
  channel: "telegram",
105
103
  requesterExternalUserId: "user-unknown-456",
106
104
  requesterChatId: "chat-123",
@@ -158,7 +156,7 @@ describe("access request decision handler", () => {
158
156
  expect(result.type).toBe("approved");
159
157
 
160
158
  // There should be an active session for this channel
161
- const session = findActiveSession("self", "telegram");
159
+ const session = findActiveSession("telegram");
162
160
  expect(session).not.toBeNull();
163
161
  expect(session!.expectedExternalUserId).toBe("user-unknown-456");
164
162
  expect(session!.expectedChatId).toBe("chat-123");
@@ -187,7 +185,7 @@ describe("access request decision handler", () => {
187
185
  expect(updated!.decidedByExternalUserId).toBe("guardian-user-789");
188
186
 
189
187
  // No verification session should be created
190
- const session = findActiveSession("self", "telegram");
188
+ const session = findActiveSession("telegram");
191
189
  expect(session).toBeNull();
192
190
  });
193
191
 
@@ -331,4 +329,87 @@ describe("access request notification delivery", () => {
331
329
  expect(text).toContain("unable to deliver");
332
330
  expect(text).toContain("try again");
333
331
  });
332
+
333
+ test("slack approval notification is sent as DM using requesterExternalUserId", async () => {
334
+ await notifyRequesterOfApproval({
335
+ replyCallbackUrl:
336
+ "http://localhost:7830/deliver/slack?threadTs=1234.5678",
337
+ requesterChatId: "C12345-channel",
338
+ requesterExternalUserId: "U98765-user",
339
+ channel: "slack",
340
+ assistantId: "self",
341
+ bearerToken: "test-token",
342
+ });
343
+
344
+ expect(deliverReplyCalls.length).toBe(1);
345
+ const call = deliverReplyCalls[0];
346
+ // Should target the user ID (DM) not the channel
347
+ expect(call.payload.chatId).toBe("U98765-user");
348
+ // threadTs should be stripped — it belongs to the guardian's channel thread
349
+ expect(call.url).not.toContain("threadTs");
350
+ });
351
+
352
+ test("slack denial notification is sent as DM using requesterExternalUserId", async () => {
353
+ await notifyRequesterOfDenial({
354
+ replyCallbackUrl:
355
+ "http://localhost:7830/deliver/slack?threadTs=1234.5678",
356
+ requesterChatId: "C12345-channel",
357
+ requesterExternalUserId: "U98765-user",
358
+ channel: "slack",
359
+ assistantId: "self",
360
+ bearerToken: "test-token",
361
+ });
362
+
363
+ expect(deliverReplyCalls.length).toBe(1);
364
+ const call = deliverReplyCalls[0];
365
+ expect(call.payload.chatId).toBe("U98765-user");
366
+ expect(call.url).not.toContain("threadTs");
367
+ });
368
+
369
+ test("slack delivery failure notification is sent as DM using requesterExternalUserId", async () => {
370
+ await notifyRequesterOfDeliveryFailure({
371
+ replyCallbackUrl:
372
+ "http://localhost:7830/deliver/slack?threadTs=1234.5678",
373
+ requesterChatId: "C12345-channel",
374
+ requesterExternalUserId: "U98765-user",
375
+ channel: "slack",
376
+ assistantId: "self",
377
+ bearerToken: "test-token",
378
+ });
379
+
380
+ expect(deliverReplyCalls.length).toBe(1);
381
+ const call = deliverReplyCalls[0];
382
+ expect(call.payload.chatId).toBe("U98765-user");
383
+ expect(call.url).not.toContain("threadTs");
384
+ });
385
+
386
+ test("non-slack channels still use requesterChatId and preserve threadTs", async () => {
387
+ await notifyRequesterOfApproval({
388
+ replyCallbackUrl:
389
+ "http://localhost:7830/deliver/telegram?threadTs=1234.5678",
390
+ requesterChatId: "chat-123",
391
+ requesterExternalUserId: "user-456",
392
+ channel: "telegram",
393
+ assistantId: "self",
394
+ bearerToken: "test-token",
395
+ });
396
+
397
+ expect(deliverReplyCalls.length).toBe(1);
398
+ expect(deliverReplyCalls[0].payload.chatId).toBe("chat-123");
399
+ // threadTs should be preserved for non-slack channels
400
+ expect(deliverReplyCalls[0].url).toContain("threadTs=1234.5678");
401
+ });
402
+
403
+ test("slack without requesterExternalUserId falls back to requesterChatId", async () => {
404
+ await notifyRequesterOfApproval({
405
+ replyCallbackUrl: "http://localhost:7830/deliver/slack",
406
+ requesterChatId: "C12345-channel",
407
+ channel: "slack",
408
+ assistantId: "self",
409
+ bearerToken: "test-token",
410
+ });
411
+
412
+ expect(deliverReplyCalls.length).toBe(1);
413
+ expect(deliverReplyCalls[0].payload.chatId).toBe("C12345-channel");
414
+ });
334
415
  });
@@ -39,7 +39,6 @@ mock.module("../config/env.js", () => ({
39
39
  isHttpAuthDisabled: () => false,
40
40
  getInternalGatewayTarget: () => "http://localhost:7822",
41
41
  getGatewayBaseUrl: () => "http://localhost:7822",
42
- getRuntimeProxyBearerToken: () => undefined,
43
42
  getRuntimeGatewayOriginSecret: () => undefined,
44
43
  isHttpAuthDisabledWithoutSafetyGate: () => false,
45
44
  getEnableMonitoring: () => false,
@@ -130,7 +129,6 @@ describe("actor-token store (hash-only)", () => {
130
129
 
131
130
  const record = createActorTokenRecord({
132
131
  tokenHash,
133
- assistantId: "self",
134
132
  guardianPrincipalId: "principal-store",
135
133
  hashedDeviceId: "hashed-dev-store",
136
134
  platform: "macos",
@@ -149,7 +147,6 @@ describe("actor-token store (hash-only)", () => {
149
147
 
150
148
  createActorTokenRecord({
151
149
  tokenHash,
152
- assistantId: "self",
153
150
  guardianPrincipalId: "principal-bind",
154
151
  hashedDeviceId: "hashed-dev-bind",
155
152
  platform: "ios",
@@ -157,7 +154,6 @@ describe("actor-token store (hash-only)", () => {
157
154
  });
158
155
 
159
156
  const found = findActiveByDeviceBinding(
160
- "self",
161
157
  "principal-bind",
162
158
  "hashed-dev-bind",
163
159
  );
@@ -170,7 +166,6 @@ describe("actor-token store (hash-only)", () => {
170
166
 
171
167
  createActorTokenRecord({
172
168
  tokenHash,
173
- assistantId: "self",
174
169
  guardianPrincipalId: "principal-revoke",
175
170
  hashedDeviceId: "hashed-dev-revoke",
176
171
  platform: "macos",
@@ -178,7 +173,6 @@ describe("actor-token store (hash-only)", () => {
178
173
  });
179
174
 
180
175
  const count = revokeByDeviceBinding(
181
- "self",
182
176
  "principal-revoke",
183
177
  "hashed-dev-revoke",
184
178
  );
@@ -193,7 +187,6 @@ describe("actor-token store (hash-only)", () => {
193
187
 
194
188
  createActorTokenRecord({
195
189
  tokenHash,
196
- assistantId: "self",
197
190
  guardianPrincipalId: "principal-single",
198
191
  hashedDeviceId: "hashed-dev-single",
199
192
  platform: "macos",
@@ -214,7 +207,7 @@ describe("guardian vellum migration", () => {
214
207
  const principalId = ensureVellumGuardianBinding("self");
215
208
  expect(principalId).toMatch(/^vellum-principal-/);
216
209
 
217
- const guardianResult = findGuardianForChannel("vellum", "self");
210
+ const guardianResult = findGuardianForChannel("vellum");
218
211
  expect(guardianResult).not.toBeNull();
219
212
  expect(guardianResult!.contact.principalId).toBe(principalId);
220
213
  expect(guardianResult!.channel.verifiedVia).toBe("startup-migration");
@@ -228,7 +221,6 @@ describe("guardian vellum migration", () => {
228
221
 
229
222
  test("ensureVellumGuardianBinding preserves existing bindings for other channels", () => {
230
223
  createGuardianBinding({
231
- assistantId: "self",
232
224
  channel: "telegram",
233
225
  guardianExternalUserId: "tg-user-123",
234
226
  guardianDeliveryChatId: "tg-chat-456",
@@ -238,11 +230,11 @@ describe("guardian vellum migration", () => {
238
230
 
239
231
  ensureVellumGuardianBinding("self");
240
232
 
241
- const tgGuardian = findGuardianForChannel("telegram", "self");
233
+ const tgGuardian = findGuardianForChannel("telegram");
242
234
  expect(tgGuardian).not.toBeNull();
243
235
  expect(tgGuardian!.channel.externalUserId).toBe("tg-user-123");
244
236
 
245
- const vGuardian = findGuardianForChannel("vellum", "self");
237
+ const vGuardian = findGuardianForChannel("vellum");
246
238
  expect(vGuardian).not.toBeNull();
247
239
  });
248
240
  });
@@ -440,7 +432,7 @@ describe("resolveLocalIpcAuthContext", () => {
440
432
 
441
433
  test("enriches actorPrincipalId from vellum guardian binding when present", () => {
442
434
  ensureVellumGuardianBinding("self");
443
- const guardianResult = findGuardianForChannel("vellum", "self");
435
+ const guardianResult = findGuardianForChannel("vellum");
444
436
  expect(guardianResult).toBeTruthy();
445
437
 
446
438
  const ctx = resolveLocalIpcAuthContext("session-123");
@@ -60,7 +60,6 @@ afterAll(() => {
60
60
  function mintParams(overrides: Partial<MintGrantParams> = {}): MintGrantParams {
61
61
  const futureExpiry = new Date(Date.now() + 60_000).toISOString();
62
62
  return {
63
- assistantId: "self",
64
63
  scopeMode: "request_id",
65
64
  requestChannel: "telegram",
66
65
  decisionChannel: "telegram",
@@ -177,7 +176,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
177
176
  toolName: "shell",
178
177
  inputDigest: computeToolApprovalDigest("shell", { command: "ls" }),
179
178
  consumingRequestId: "consumer-1",
180
- assistantId: "self",
181
179
  });
182
180
 
183
181
  expect(result.ok).toBe(true);
@@ -200,7 +198,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
200
198
  toolName: "shell",
201
199
  inputDigest: digest,
202
200
  consumingRequestId: "consumer-2",
203
- assistantId: "self",
204
201
  });
205
202
 
206
203
  expect(result.ok).toBe(true);
@@ -224,7 +221,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
224
221
  toolName: "shell",
225
222
  inputDigest: digest,
226
223
  consumingRequestId: "consumer-3",
227
- assistantId: "self",
228
224
  });
229
225
 
230
226
  expect(result.ok).toBe(true);
@@ -242,7 +238,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
242
238
  toolName: "shell",
243
239
  inputDigest: computeToolApprovalDigest("shell", { command: "ls" }),
244
240
  consumingRequestId: "consumer-miss",
245
- assistantId: "self",
246
241
  },
247
242
  { maxWaitMs: 0 },
248
243
  );
@@ -267,7 +262,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
267
262
  toolName: "file_write",
268
263
  inputDigest: digest,
269
264
  consumingRequestId: "consumer-mismatch-tool",
270
- assistantId: "self",
271
265
  },
272
266
  { maxWaitMs: 0 },
273
267
  );
@@ -293,32 +287,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
293
287
  command: "rm -rf /",
294
288
  }),
295
289
  consumingRequestId: "consumer-mismatch-input",
296
- assistantId: "self",
297
- },
298
- { maxWaitMs: 0 },
299
- );
300
-
301
- expect(result.ok).toBe(false);
302
- if (result.ok) return;
303
- expect(result.reason).toBe("no_match");
304
- });
305
-
306
- test("miss: assistant ID mismatch", async () => {
307
- mintGrantFromDecision(
308
- mintParams({
309
- scopeMode: "request_id",
310
- requestId: "req-assist",
311
- assistantId: "assistant-A",
312
- }),
313
- );
314
-
315
- const result = await consumeGrantForInvocation(
316
- {
317
- requestId: "req-assist",
318
- toolName: "shell",
319
- inputDigest: computeToolApprovalDigest("shell", {}),
320
- consumingRequestId: "consumer-assist-mismatch",
321
- assistantId: "assistant-B",
322
290
  },
323
291
  { maxWaitMs: 0 },
324
292
  );
@@ -344,7 +312,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
344
312
  toolName: "shell",
345
313
  inputDigest: computeToolApprovalDigest("shell", {}),
346
314
  consumingRequestId: "consumer-expired",
347
- assistantId: "self",
348
315
  },
349
316
  { maxWaitMs: 0 },
350
317
  );
@@ -368,7 +335,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
368
335
  toolName: "shell",
369
336
  inputDigest: computeToolApprovalDigest("shell", {}),
370
337
  consumingRequestId: "consumer-first",
371
- assistantId: "self",
372
338
  });
373
339
  expect(first.ok).toBe(true);
374
340
 
@@ -378,7 +344,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
378
344
  toolName: "shell",
379
345
  inputDigest: computeToolApprovalDigest("shell", {}),
380
346
  consumingRequestId: "consumer-second",
381
- assistantId: "self",
382
347
  },
383
348
  { maxWaitMs: 0 },
384
349
  );
@@ -401,7 +366,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
401
366
  toolName: "shell",
402
367
  inputDigest: digest,
403
368
  consumingRequestId: "consumer-sig-first",
404
- assistantId: "self",
405
369
  });
406
370
  expect(first.ok).toBe(true);
407
371
 
@@ -410,7 +374,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
410
374
  toolName: "shell",
411
375
  inputDigest: digest,
412
376
  consumingRequestId: "consumer-sig-second",
413
- assistantId: "self",
414
377
  },
415
378
  { maxWaitMs: 0 },
416
379
  );
@@ -439,7 +402,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
439
402
  toolName: "shell",
440
403
  inputDigest: digest,
441
404
  consumingRequestId: "consumer-ctx",
442
- assistantId: "self",
443
405
  conversationId: "conv-ctx",
444
406
  callSessionId: "call-ctx",
445
407
  });
@@ -463,7 +425,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
463
425
  toolName: "shell",
464
426
  inputDigest: digest,
465
427
  consumingRequestId: "consumer-ctx-mismatch",
466
- assistantId: "self",
467
428
  conversationId: "conv-B",
468
429
  },
469
430
  { maxWaitMs: 0 },
@@ -497,7 +458,6 @@ describe("approval-primitive / consumeGrantForInvocation retry", () => {
497
458
  toolName: "shell",
498
459
  inputDigest: digest,
499
460
  consumingRequestId: "consumer-async-immediate",
500
- assistantId: "self",
501
461
  });
502
462
  const elapsed = Date.now() - start;
503
463
 
@@ -528,7 +488,6 @@ describe("approval-primitive / consumeGrantForInvocation retry", () => {
528
488
  toolName: "shell",
529
489
  inputDigest: digest,
530
490
  consumingRequestId: "consumer-async-delayed",
531
- assistantId: "self",
532
491
  },
533
492
  { maxWaitMs: 5_000, intervalMs: 100 },
534
493
  );
@@ -553,7 +512,6 @@ describe("approval-primitive / consumeGrantForInvocation retry", () => {
553
512
  toolName: "shell",
554
513
  inputDigest: digest,
555
514
  consumingRequestId: "consumer-async-timeout",
556
- assistantId: "self",
557
515
  },
558
516
  { maxWaitMs: 500, intervalMs: 100 },
559
517
  );
@@ -580,7 +538,6 @@ describe("approval-primitive / consumeGrantForInvocation retry", () => {
580
538
  toolName: "shell",
581
539
  inputDigest: digest,
582
540
  consumingRequestId: "consumer-aborted",
583
- assistantId: "self",
584
541
  },
585
542
  { maxWaitMs: 2_000, intervalMs: 50, signal: controller.signal },
586
543
  );
@@ -606,7 +563,6 @@ describe("approval-primitive / consumeGrantForInvocation retry", () => {
606
563
  toolName: "shell",
607
564
  inputDigest: digest,
608
565
  consumingRequestId: "consumer-pre-aborted",
609
- assistantId: "self",
610
566
  },
611
567
  { maxWaitMs: 2_000, intervalMs: 50, signal: controller.signal },
612
568
  );
@@ -628,7 +584,6 @@ describe("approval-primitive / consumeGrantForInvocation retry", () => {
628
584
  toolName: "shell",
629
585
  inputDigest: digest,
630
586
  consumingRequestId: "consumer-no-retry",
631
- assistantId: "self",
632
587
  },
633
588
  { maxWaitMs: 0 },
634
589
  );
@@ -57,7 +57,6 @@ mock.module("../config/env.js", () => ({
57
57
  getGatewayPort: () => 7830,
58
58
  getRuntimeHttpPort: () => 7821,
59
59
  getRuntimeHttpHost: () => "127.0.0.1",
60
- getRuntimeProxyBearerToken: () => undefined,
61
60
  getRuntimeGatewayOriginSecret: () => undefined,
62
61
  getIngressPublicBaseUrl: () => undefined,
63
62
  setIngressPublicBaseUrl: () => {},
@@ -18,6 +18,8 @@ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
18
18
  * - No assistant-scoped route handlers in the daemon HTTP server
19
19
  * - No hardcoded `'self'` string for assistant scoping (use the constant)
20
20
  * - The constant itself equals `'self'`
21
+ * - No `assistantId` columns in daemon SQLite schema definitions
22
+ * - No `assistantId` parameter in daemon store function signatures
21
23
  */
22
24
 
23
25
  // ---------------------------------------------------------------------------
@@ -466,4 +468,152 @@ describe("assistant ID boundary", () => {
466
468
  ).not.toContain("assistantId");
467
469
  }
468
470
  });
471
+
472
+ // -------------------------------------------------------------------------
473
+ // Rule (f): No assistantId columns in daemon SQLite schema definitions
474
+ //
475
+ // The daemon is assistant-agnostic — it uses DAEMON_INTERNAL_ASSISTANT_ID
476
+ // implicitly. Schema files must not define assistantId columns, which would
477
+ // re-introduce assistant-scoped storage in the daemon layer.
478
+ // -------------------------------------------------------------------------
479
+
480
+ test("no assistantId columns in daemon SQLite schema definitions", () => {
481
+ const repoRoot = getRepoRoot();
482
+
483
+ // Scan all Drizzle schema files for assistantId column definitions.
484
+ // The Drizzle ORM pattern is `assistantId: text(` for defining a text
485
+ // column named assistantId.
486
+ const schemaGlobs = [
487
+ "assistant/src/memory/schema/*.ts",
488
+ "assistant/src/memory/schema/**/*.ts",
489
+ ];
490
+
491
+ let grepOutput = "";
492
+ try {
493
+ grepOutput = execFileSync(
494
+ "git",
495
+ ["grep", "-nE", "assistantId\\s*:\\s*text\\(", "--", ...schemaGlobs],
496
+ { encoding: "utf-8", cwd: repoRoot },
497
+ ).trim();
498
+ } catch (err) {
499
+ // Exit code 1 means no matches — happy path
500
+ if ((err as { status?: number }).status === 1) {
501
+ return;
502
+ }
503
+ throw err;
504
+ }
505
+
506
+ const lines = grepOutput.split("\n").filter((l) => l.length > 0);
507
+ const violations = lines.filter((line) => {
508
+ // Allow comments
509
+ const parts = line.split(":");
510
+ const content = parts.slice(2).join(":").trim();
511
+ if (
512
+ content.startsWith("//") ||
513
+ content.startsWith("*") ||
514
+ content.startsWith("/*")
515
+ ) {
516
+ return false;
517
+ }
518
+ return true;
519
+ });
520
+
521
+ if (violations.length > 0) {
522
+ const message = [
523
+ "Found `assistantId` column definitions in daemon SQLite schema files.",
524
+ "`assistantId` columns are not allowed in daemon schema — the daemon uses",
525
+ "`DAEMON_INTERNAL_ASSISTANT_ID` implicitly and is assistant-agnostic.",
526
+ "",
527
+ "Violations:",
528
+ ...violations.map((v) => ` - ${v}`),
529
+ ].join("\n");
530
+
531
+ expect(violations, message).toEqual([]);
532
+ }
533
+ });
534
+
535
+ // -------------------------------------------------------------------------
536
+ // Rule (g): No assistantId parameter in daemon store function signatures
537
+ //
538
+ // Store functions in the daemon layer must not accept assistantId as a
539
+ // parameter. The daemon is assistant-agnostic — all assistant scoping
540
+ // uses DAEMON_INTERNAL_ASSISTANT_ID internally.
541
+ // -------------------------------------------------------------------------
542
+
543
+ test("no assistantId parameter in daemon store function signatures", () => {
544
+ const repoRoot = getRepoRoot();
545
+
546
+ // Scan store files for exported function signatures that include
547
+ // assistantId as a parameter. This covers memory stores, contact stores,
548
+ // notification stores, credential/token stores, and call stores.
549
+ const storeGlobs = [
550
+ "assistant/src/memory/*.ts",
551
+ "assistant/src/contacts/*.ts",
552
+ "assistant/src/notifications/*.ts",
553
+ "assistant/src/runtime/auth/credential-service.ts",
554
+ "assistant/src/runtime/actor-token-store.ts",
555
+ "assistant/src/runtime/actor-refresh-token-store.ts",
556
+ "assistant/src/calls/call-store.ts",
557
+ ];
558
+
559
+ // Match exported function declarations/expressions with assistantId in
560
+ // their parameter lists. Patterns:
561
+ // export function foo(assistantId
562
+ // export function foo(bar, assistantId
563
+ // export async function foo(assistantId
564
+ // export const foo = (assistantId
565
+ // export const foo = async (assistantId
566
+ // We use a broad pattern that catches assistantId appearing after an
567
+ // opening paren in an export context.
568
+ const pattern =
569
+ "export\\s+(async\\s+)?function\\s+\\w+\\s*\\([^)]*assistantId|export\\s+const\\s+\\w+\\s*=\\s*(async\\s+)?\\([^)]*assistantId";
570
+
571
+ let grepOutput = "";
572
+ try {
573
+ grepOutput = execFileSync(
574
+ "git",
575
+ ["grep", "-nE", pattern, "--", ...storeGlobs],
576
+ { encoding: "utf-8", cwd: repoRoot },
577
+ ).trim();
578
+ } catch (err) {
579
+ // Exit code 1 means no matches — happy path
580
+ if ((err as { status?: number }).status === 1) {
581
+ return;
582
+ }
583
+ throw err;
584
+ }
585
+
586
+ const allLines = grepOutput.split("\n").filter((l) => l.length > 0);
587
+ const violations = allLines.filter((line) => {
588
+ const filePath = line.split(":")[0];
589
+ if (isTestFile(filePath)) return false;
590
+ if (isMigrationFile(filePath)) return false;
591
+
592
+ // Allow comments
593
+ const parts = line.split(":");
594
+ const content = parts.slice(2).join(":").trim();
595
+ if (
596
+ content.startsWith("//") ||
597
+ content.startsWith("*") ||
598
+ content.startsWith("/*")
599
+ ) {
600
+ return false;
601
+ }
602
+
603
+ return true;
604
+ });
605
+
606
+ if (violations.length > 0) {
607
+ const message = [
608
+ "Found daemon store functions with `assistantId` in their parameter signatures.",
609
+ "Store functions must not accept `assistantId` — the daemon is assistant-agnostic",
610
+ "and uses `DAEMON_INTERNAL_ASSISTANT_ID` implicitly.",
611
+ "",
612
+ "Violations:",
613
+ ...violations.map((v) => ` - ${v}`),
614
+ ].join("\n");
615
+
616
+ expect(violations, message).toEqual([]);
617
+ }
618
+ });
469
619
  });
@@ -27,7 +27,6 @@ mock.module("../util/platform.js", () => ({
27
27
  getDbPath: () => join(testDir, "test.db"),
28
28
  getLogPath: () => join(testDir, "test.log"),
29
29
  ensureDataDir: () => {},
30
- readHttpToken: () => null,
31
30
  }));
32
31
 
33
32
  mock.module("../util/logger.js", () => ({
@@ -27,7 +27,6 @@ mock.module("../util/platform.js", () => ({
27
27
  getDbPath: () => join(testDir, "test.db"),
28
28
  getLogPath: () => join(testDir, "test.log"),
29
29
  ensureDataDir: () => {},
30
- readHttpToken: () => null,
31
30
  }));
32
31
 
33
32
  mock.module("../util/logger.js", () => ({
@@ -15,7 +15,6 @@ function buildSignal(
15
15
  ): NotificationSignal {
16
16
  return {
17
17
  signalId: "test-signal-1",
18
- assistantId: "self",
19
18
  createdAt: Date.now(),
20
19
  sourceChannel: "voice",
21
20
  sourceSessionId: "test-session-1",