@vellumai/assistant 0.3.18 → 0.3.20

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 (202) hide show
  1. package/ARCHITECTURE.md +155 -15
  2. package/Dockerfile +1 -0
  3. package/README.md +40 -4
  4. package/docs/architecture/integrations.md +7 -11
  5. package/docs/architecture/security.md +80 -0
  6. package/package.json +1 -1
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -0
  8. package/src/__tests__/approval-primitive.test.ts +540 -0
  9. package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
  10. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
  11. package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
  12. package/src/__tests__/call-controller.test.ts +605 -104
  13. package/src/__tests__/channel-invite-transport.test.ts +264 -0
  14. package/src/__tests__/checker.test.ts +60 -0
  15. package/src/__tests__/cli.test.ts +42 -1
  16. package/src/__tests__/config-schema.test.ts +11 -127
  17. package/src/__tests__/config-watcher.test.ts +0 -8
  18. package/src/__tests__/daemon-lifecycle.test.ts +1 -0
  19. package/src/__tests__/daemon-server-session-init.test.ts +8 -2
  20. package/src/__tests__/diff.test.ts +22 -0
  21. package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
  22. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +779 -0
  23. package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
  24. package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
  25. package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
  26. package/src/__tests__/guardian-dispatch.test.ts +185 -1
  27. package/src/__tests__/guardian-grant-minting.test.ts +532 -0
  28. package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
  29. package/src/__tests__/invite-redemption-service.test.ts +306 -0
  30. package/src/__tests__/ipc-snapshot.test.ts +58 -0
  31. package/src/__tests__/notification-decision-fallback.test.ts +88 -0
  32. package/src/__tests__/remote-skill-policy.test.ts +215 -0
  33. package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
  34. package/src/__tests__/sandbox-host-parity.test.ts +6 -13
  35. package/src/__tests__/scoped-approval-grants.test.ts +521 -0
  36. package/src/__tests__/scoped-grant-security-matrix.test.ts +444 -0
  37. package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
  38. package/src/__tests__/session-load-history-repair.test.ts +169 -2
  39. package/src/__tests__/session-runtime-assembly.test.ts +33 -5
  40. package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
  41. package/src/__tests__/skill-feature-flags.test.ts +188 -0
  42. package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
  43. package/src/__tests__/skill-mirror-parity.test.ts +1 -0
  44. package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
  45. package/src/__tests__/system-prompt.test.ts +1 -1
  46. package/src/__tests__/terminal-sandbox.test.ts +142 -9
  47. package/src/__tests__/terminal-tools.test.ts +2 -93
  48. package/src/__tests__/thread-seed-composer.test.ts +18 -0
  49. package/src/__tests__/tool-approval-handler.test.ts +350 -0
  50. package/src/__tests__/trust-store.test.ts +2 -0
  51. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
  52. package/src/__tests__/voice-scoped-grant-consumer.test.ts +533 -0
  53. package/src/agent/loop.ts +36 -1
  54. package/src/approvals/approval-primitive.ts +381 -0
  55. package/src/approvals/guardian-decision-primitive.ts +191 -0
  56. package/src/calls/call-controller.ts +276 -212
  57. package/src/calls/call-domain.ts +56 -6
  58. package/src/calls/guardian-dispatch.ts +56 -0
  59. package/src/calls/relay-server.ts +13 -0
  60. package/src/calls/types.ts +1 -1
  61. package/src/calls/voice-session-bridge.ts +59 -4
  62. package/src/cli/core-commands.ts +0 -4
  63. package/src/cli.ts +76 -34
  64. package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
  65. package/src/config/assistant-feature-flags.ts +162 -0
  66. package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
  67. package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
  68. package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
  69. package/src/config/bundled-skills/notifications/SKILL.md +18 -0
  70. package/src/config/bundled-skills/reminder/SKILL.md +49 -2
  71. package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
  72. package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
  73. package/src/config/core-schema.ts +1 -1
  74. package/src/config/env-registry.ts +10 -0
  75. package/src/config/feature-flag-registry.json +61 -0
  76. package/src/config/loader.ts +22 -1
  77. package/src/config/sandbox-schema.ts +0 -39
  78. package/src/config/schema.ts +12 -2
  79. package/src/config/skill-state.ts +34 -0
  80. package/src/config/skills-schema.ts +26 -0
  81. package/src/config/skills.ts +9 -0
  82. package/src/config/system-prompt.ts +110 -46
  83. package/src/config/templates/SOUL.md +1 -1
  84. package/src/config/types.ts +19 -1
  85. package/src/config/vellum-skills/catalog.json +1 -1
  86. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
  87. package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
  88. package/src/config/vellum-skills/telegram-setup/SKILL.md +1 -1
  89. package/src/config/vellum-skills/trusted-contacts/SKILL.md +104 -3
  90. package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
  91. package/src/daemon/config-watcher.ts +0 -1
  92. package/src/daemon/daemon-control.ts +1 -1
  93. package/src/daemon/guardian-invite-intent.ts +124 -0
  94. package/src/daemon/handlers/avatar.ts +68 -0
  95. package/src/daemon/handlers/browser.ts +2 -2
  96. package/src/daemon/handlers/config-channels.ts +18 -0
  97. package/src/daemon/handlers/guardian-actions.ts +120 -0
  98. package/src/daemon/handlers/index.ts +4 -0
  99. package/src/daemon/handlers/sessions.ts +19 -0
  100. package/src/daemon/handlers/shared.ts +3 -1
  101. package/src/daemon/handlers/skills.ts +45 -2
  102. package/src/daemon/install-cli-launchers.ts +58 -13
  103. package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
  104. package/src/daemon/ipc-contract/sessions.ts +8 -2
  105. package/src/daemon/ipc-contract/settings.ts +25 -2
  106. package/src/daemon/ipc-contract/skills.ts +1 -0
  107. package/src/daemon/ipc-contract-inventory.json +10 -0
  108. package/src/daemon/ipc-contract.ts +4 -0
  109. package/src/daemon/lifecycle.ts +6 -2
  110. package/src/daemon/main.ts +1 -0
  111. package/src/daemon/server.ts +1 -0
  112. package/src/daemon/session-lifecycle.ts +52 -7
  113. package/src/daemon/session-memory.ts +45 -0
  114. package/src/daemon/session-process.ts +260 -422
  115. package/src/daemon/session-runtime-assembly.ts +12 -0
  116. package/src/daemon/session-skill-tools.ts +14 -1
  117. package/src/daemon/session-tool-setup.ts +5 -0
  118. package/src/daemon/session.ts +11 -0
  119. package/src/daemon/tool-side-effects.ts +35 -9
  120. package/src/index.ts +0 -2
  121. package/src/memory/conversation-display-order-migration.ts +44 -0
  122. package/src/memory/conversation-queries.ts +2 -0
  123. package/src/memory/conversation-store.ts +91 -0
  124. package/src/memory/db-init.ts +13 -1
  125. package/src/memory/embedding-local.ts +22 -8
  126. package/src/memory/guardian-action-store.ts +133 -2
  127. package/src/memory/guardian-verification.ts +1 -1
  128. package/src/memory/ingress-invite-store.ts +95 -1
  129. package/src/memory/migrations/033-scoped-approval-grants.ts +51 -0
  130. package/src/memory/migrations/034-guardian-action-tool-metadata.ts +12 -0
  131. package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
  132. package/src/memory/migrations/index.ts +3 -0
  133. package/src/memory/schema.ts +35 -1
  134. package/src/memory/scoped-approval-grants.ts +518 -0
  135. package/src/messaging/providers/slack/client.ts +12 -0
  136. package/src/messaging/providers/slack/types.ts +5 -0
  137. package/src/notifications/decision-engine.ts +49 -12
  138. package/src/notifications/emit-signal.ts +7 -0
  139. package/src/notifications/signal.ts +7 -0
  140. package/src/notifications/thread-seed-composer.ts +2 -1
  141. package/src/permissions/checker.ts +27 -0
  142. package/src/runtime/channel-approval-types.ts +16 -6
  143. package/src/runtime/channel-approvals.ts +19 -15
  144. package/src/runtime/channel-invite-transport.ts +85 -0
  145. package/src/runtime/channel-invite-transports/telegram.ts +105 -0
  146. package/src/runtime/guardian-action-grant-minter.ts +154 -0
  147. package/src/runtime/guardian-action-message-composer.ts +30 -0
  148. package/src/runtime/guardian-decision-types.ts +91 -0
  149. package/src/runtime/http-server.ts +23 -1
  150. package/src/runtime/ingress-service.ts +22 -0
  151. package/src/runtime/invite-redemption-service.ts +181 -0
  152. package/src/runtime/invite-redemption-templates.ts +39 -0
  153. package/src/runtime/routes/call-routes.ts +2 -1
  154. package/src/runtime/routes/guardian-action-routes.ts +206 -0
  155. package/src/runtime/routes/guardian-approval-interception.ts +66 -74
  156. package/src/runtime/routes/inbound-message-handler.ts +568 -409
  157. package/src/runtime/routes/pairing-routes.ts +4 -0
  158. package/src/security/encrypted-store.ts +31 -17
  159. package/src/security/keychain.ts +176 -2
  160. package/src/security/secure-keys.ts +97 -0
  161. package/src/security/tool-approval-digest.ts +67 -0
  162. package/src/skills/remote-skill-policy.ts +131 -0
  163. package/src/tools/browser/browser-execution.ts +2 -2
  164. package/src/tools/browser/browser-manager.ts +46 -32
  165. package/src/tools/browser/browser-screencast.ts +2 -2
  166. package/src/tools/calls/call-start.ts +1 -1
  167. package/src/tools/executor.ts +22 -17
  168. package/src/tools/network/script-proxy/session-manager.ts +1 -5
  169. package/src/tools/skills/load.ts +22 -8
  170. package/src/tools/system/avatar-generator.ts +119 -0
  171. package/src/tools/system/navigate-settings.ts +65 -0
  172. package/src/tools/system/open-system-settings.ts +75 -0
  173. package/src/tools/system/voice-config.ts +121 -32
  174. package/src/tools/terminal/backends/native.ts +40 -19
  175. package/src/tools/terminal/backends/types.ts +3 -3
  176. package/src/tools/terminal/parser.ts +1 -1
  177. package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
  178. package/src/tools/terminal/sandbox.ts +1 -12
  179. package/src/tools/terminal/shell.ts +3 -31
  180. package/src/tools/tool-approval-handler.ts +141 -3
  181. package/src/tools/tool-manifest.ts +6 -0
  182. package/src/tools/types.ts +6 -0
  183. package/src/util/diff.ts +36 -13
  184. package/Dockerfile.sandbox +0 -5
  185. package/src/__tests__/doordash-client.test.ts +0 -187
  186. package/src/__tests__/doordash-session.test.ts +0 -154
  187. package/src/__tests__/signup-e2e.test.ts +0 -354
  188. package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
  189. package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
  190. package/src/cli/doordash.ts +0 -1057
  191. package/src/config/bundled-skills/doordash/SKILL.md +0 -163
  192. package/src/config/templates/LOOKS.md +0 -25
  193. package/src/doordash/cart-queries.ts +0 -787
  194. package/src/doordash/client.ts +0 -1016
  195. package/src/doordash/order-queries.ts +0 -85
  196. package/src/doordash/queries.ts +0 -13
  197. package/src/doordash/query-extractor.ts +0 -94
  198. package/src/doordash/search-queries.ts +0 -203
  199. package/src/doordash/session.ts +0 -84
  200. package/src/doordash/store-queries.ts +0 -246
  201. package/src/doordash/types.ts +0 -367
  202. package/src/tools/terminal/backends/docker.ts +0 -379
@@ -66,7 +66,7 @@ const DEFAULT_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
66
66
  // Helpers
67
67
  // ---------------------------------------------------------------------------
68
68
 
69
- function hashToken(rawToken: string): string {
69
+ export function hashToken(rawToken: string): string {
70
70
  return createHash('sha256').update(rawToken).digest('hex');
71
71
  }
72
72
 
@@ -268,6 +268,12 @@ export function redeemInvite(params: {
268
268
  return { error: 'invite_max_uses_reached' };
269
269
  }
270
270
 
271
+ // Enforce channel-scoped redemption: when the caller specifies a channel, it
272
+ // must match the channel the invite was created for.
273
+ if (params.sourceChannel && params.sourceChannel !== invite.sourceChannel) {
274
+ return { error: 'invite_channel_mismatch' };
275
+ }
276
+
271
277
  const newUseCount = invite.useCount + 1;
272
278
  const newStatus = newUseCount >= invite.maxUses ? 'redeemed' : 'active';
273
279
 
@@ -323,6 +329,94 @@ export function redeemInvite(params: {
323
329
  return { invite: updatedInvite, member: rowToMember(memberRow) };
324
330
  }
325
331
 
332
+ // ---------------------------------------------------------------------------
333
+ // recordInviteUse — consume one use without creating a member row
334
+ // ---------------------------------------------------------------------------
335
+
336
+ /**
337
+ * Increment an invite's use count and record redemption metadata without
338
+ * inserting a new member row. Used when reactivating an existing inactive
339
+ * member via invite — the member row already exists and just needs an
340
+ * update, so the transactional INSERT in `redeemInvite` would hit a
341
+ * unique-key constraint.
342
+ *
343
+ * Returns `true` if the use was recorded, or `false` if the invite was
344
+ * concurrently revoked/expired (the WHERE clause constrains to
345
+ * `status = 'active'` so a stale write is impossible).
346
+ */
347
+ export function recordInviteUse(params: {
348
+ inviteId: string;
349
+ externalUserId?: string;
350
+ externalChatId?: string;
351
+ }): boolean {
352
+ const db = getDb();
353
+ const now = Date.now();
354
+
355
+ const invite = db
356
+ .select()
357
+ .from(assistantIngressInvites)
358
+ .where(eq(assistantIngressInvites.id, params.inviteId))
359
+ .get();
360
+
361
+ if (!invite) return false;
362
+
363
+ const newUseCount = invite.useCount + 1;
364
+ const newStatus = newUseCount >= invite.maxUses ? 'redeemed' : 'active';
365
+
366
+ // Constrain the update to active invites so a concurrent revoke/expire
367
+ // prevents this write rather than silently overwriting the new status.
368
+ db.update(assistantIngressInvites)
369
+ .set({
370
+ useCount: newUseCount,
371
+ status: newStatus,
372
+ redeemedByExternalUserId: params.externalUserId ?? null,
373
+ redeemedByExternalChatId: params.externalChatId ?? null,
374
+ redeemedAt: now,
375
+ updatedAt: now,
376
+ })
377
+ .where(
378
+ and(
379
+ eq(assistantIngressInvites.id, invite.id),
380
+ eq(assistantIngressInvites.status, 'active'),
381
+ ),
382
+ )
383
+ .run();
384
+
385
+ // Re-read to confirm the update took effect (the WHERE clause constrains
386
+ // to status = 'active', so a concurrent revoke/expire would prevent it).
387
+ const updated = db
388
+ .select({ useCount: assistantIngressInvites.useCount })
389
+ .from(assistantIngressInvites)
390
+ .where(eq(assistantIngressInvites.id, invite.id))
391
+ .get();
392
+
393
+ return !!updated && updated.useCount === newUseCount;
394
+ }
395
+
396
+ // ---------------------------------------------------------------------------
397
+ // markInviteExpired
398
+ // ---------------------------------------------------------------------------
399
+
400
+ /**
401
+ * Transition an invite's status to 'expired' in storage. This is safe to call
402
+ * even if the invite is already expired — the WHERE clause scopes the update
403
+ * to 'active' rows so it becomes a no-op in that case.
404
+ */
405
+ export function markInviteExpired(inviteId: string): void {
406
+ const db = getDb();
407
+ const now = Date.now();
408
+
409
+ db.update(assistantIngressInvites)
410
+ .set({ status: 'expired', updatedAt: now })
411
+ .where(
412
+ and(
413
+ eq(assistantIngressInvites.id, inviteId),
414
+ eq(assistantIngressInvites.status, 'active'),
415
+ ),
416
+ )
417
+ .run();
418
+ }
419
+
326
420
  // ---------------------------------------------------------------------------
327
421
  // findByTokenHash
328
422
  // ---------------------------------------------------------------------------
@@ -0,0 +1,51 @@
1
+ import type { DrizzleDb } from '../db-connection.js';
2
+
3
+ /**
4
+ * Create the scoped_approval_grants table for channel-agnostic scoped
5
+ * approval grants. Supports two scope modes:
6
+ * - request_id: grant is scoped to a specific request
7
+ * - tool_signature: grant is scoped to a tool name + input digest
8
+ *
9
+ * Grants are one-time-use (active -> consumed via CAS) and carry a
10
+ * mandatory TTL (expires_at).
11
+ */
12
+ export function createScopedApprovalGrantsTable(database: DrizzleDb): void {
13
+ database.run(/*sql*/ `
14
+ CREATE TABLE IF NOT EXISTS scoped_approval_grants (
15
+ id TEXT PRIMARY KEY,
16
+ assistant_id TEXT NOT NULL,
17
+ scope_mode TEXT NOT NULL,
18
+ request_id TEXT,
19
+ tool_name TEXT,
20
+ input_digest TEXT,
21
+ request_channel TEXT NOT NULL,
22
+ decision_channel TEXT NOT NULL,
23
+ execution_channel TEXT,
24
+ conversation_id TEXT,
25
+ call_session_id TEXT,
26
+ requester_external_user_id TEXT,
27
+ guardian_external_user_id TEXT,
28
+ status TEXT NOT NULL,
29
+ expires_at TEXT NOT NULL,
30
+ consumed_at TEXT,
31
+ consumed_by_request_id TEXT,
32
+ created_at TEXT NOT NULL,
33
+ updated_at TEXT NOT NULL
34
+ )
35
+ `);
36
+
37
+ // Index for request_id-based lookups (scope_mode = 'request_id')
38
+ database.run(
39
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_scoped_grants_request_id ON scoped_approval_grants(request_id) WHERE request_id IS NOT NULL`,
40
+ );
41
+
42
+ // Index for tool_signature-based lookups (scope_mode = 'tool_signature')
43
+ database.run(
44
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_scoped_grants_tool_sig ON scoped_approval_grants(tool_name, input_digest) WHERE tool_name IS NOT NULL`,
45
+ );
46
+
47
+ // Index for expiry sweeps
48
+ database.run(
49
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_scoped_grants_status_expires ON scoped_approval_grants(status, expires_at)`,
50
+ );
51
+ }
@@ -0,0 +1,12 @@
1
+ import type { DrizzleDb } from '../db-connection.js';
2
+
3
+ /**
4
+ * Add tool_name and input_digest columns to guardian_action_requests for
5
+ * structured tool-approval tracking. These are nullable — informational
6
+ * ASK_GUARDIAN requests leave them NULL while tool-approval requests
7
+ * carry the tool identity and canonical input digest.
8
+ */
9
+ export function migrateGuardianActionToolMetadata(database: DrizzleDb): void {
10
+ try { database.run(/*sql*/ `ALTER TABLE guardian_action_requests ADD COLUMN tool_name TEXT`); } catch { /* already exists */ }
11
+ try { database.run(/*sql*/ `ALTER TABLE guardian_action_requests ADD COLUMN input_digest TEXT`); } catch { /* already exists */ }
12
+ }
@@ -0,0 +1,23 @@
1
+ import type { DrizzleDb } from '../db-connection.js';
2
+
3
+ /**
4
+ * Add supersession metadata columns to guardian_action_requests and
5
+ * create an index for efficient pending-request lookups by call session.
6
+ *
7
+ * - superseded_by_request_id: links to the request that replaced this one
8
+ * - superseded_at: timestamp when supersession occurred
9
+ * - Index (call_session_id, status, created_at DESC) for fast lookups of
10
+ * the most recent pending request per call session
11
+ *
12
+ * The existing expired_reason column already supports 'superseded' as a
13
+ * value — this migration adds the structural metadata to track the
14
+ * supersession chain.
15
+ */
16
+ export function migrateGuardianActionSupersession(database: DrizzleDb): void {
17
+ try { database.run(/*sql*/ `ALTER TABLE guardian_action_requests ADD COLUMN superseded_by_request_id TEXT`); } catch { /* already exists */ }
18
+ try { database.run(/*sql*/ `ALTER TABLE guardian_action_requests ADD COLUMN superseded_at INTEGER`); } catch { /* already exists */ }
19
+
20
+ database.run(
21
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_guardian_action_requests_session_status_created ON guardian_action_requests(call_session_id, status, created_at DESC)`,
22
+ );
23
+ }
@@ -34,6 +34,9 @@ export { migrateGuardianVerificationPurpose } from './030-guardian-verification-
34
34
  export { migrateConversationsThreadTypeIndex } from './031-conversations-thread-type-index.js';
35
35
  export { migrateGuardianDeliveryConversationIndex } from './032-guardian-delivery-conversation-index.js';
36
36
  export { migrateNotificationDeliveryThreadDecision } from './032-notification-delivery-thread-decision.js';
37
+ export { createScopedApprovalGrantsTable } from './033-scoped-approval-grants.js';
38
+ export { migrateGuardianActionToolMetadata } from './034-guardian-action-tool-metadata.js';
39
+ export { migrateGuardianActionSupersession } from './035-guardian-action-supersession.js';
37
40
  export { createCoreTables } from './100-core-tables.js';
38
41
  export { createWatchersAndLogsTables } from './101-watchers-and-logs.js';
39
42
  export { addCoreColumns } from './102-alter-table-columns.js';
@@ -843,9 +843,15 @@ export const guardianActionRequests = sqliteTable('guardian_action_requests', {
843
843
  lateAnsweredAt: integer('late_answered_at'),
844
844
  followupAction: text('followup_action'), // call_back | message_back | decline
845
845
  followupCompletedAt: integer('followup_completed_at'),
846
+ toolName: text('tool_name'), // tool identity for tool-approval requests
847
+ inputDigest: text('input_digest'), // canonical SHA-256 digest of tool input
848
+ supersededByRequestId: text('superseded_by_request_id'), // links to the request that replaced this one
849
+ supersededAt: integer('superseded_at'), // epoch ms when supersession occurred
846
850
  createdAt: integer('created_at').notNull(),
847
851
  updatedAt: integer('updated_at').notNull(),
848
- });
852
+ }, (table) => [
853
+ index('idx_guardian_action_requests_session_status_created').on(table.callSessionId, table.status, table.createdAt),
854
+ ]);
849
855
 
850
856
  // ── Guardian Action Deliveries (per-channel delivery tracking) ───────
851
857
 
@@ -1075,3 +1081,31 @@ export const conversationAssistantAttentionState = sqliteTable('conversation_ass
1075
1081
  index('idx_conv_attn_state_assistant_latest_msg').on(table.assistantId, table.latestAssistantMessageAt),
1076
1082
  index('idx_conv_attn_state_assistant_last_seen').on(table.assistantId, table.lastSeenAssistantMessageAt),
1077
1083
  ]);
1084
+
1085
+ // ── Scoped Approval Grants ──────────────────────────────────────────
1086
+
1087
+ export const scopedApprovalGrants = sqliteTable('scoped_approval_grants', {
1088
+ id: text('id').primaryKey(),
1089
+ assistantId: text('assistant_id').notNull(),
1090
+ scopeMode: text('scope_mode').notNull(), // 'request_id' | 'tool_signature'
1091
+ requestId: text('request_id'),
1092
+ toolName: text('tool_name'),
1093
+ inputDigest: text('input_digest'),
1094
+ requestChannel: text('request_channel').notNull(),
1095
+ decisionChannel: text('decision_channel').notNull(),
1096
+ executionChannel: text('execution_channel'), // null = any channel
1097
+ conversationId: text('conversation_id'),
1098
+ callSessionId: text('call_session_id'),
1099
+ requesterExternalUserId: text('requester_external_user_id'),
1100
+ guardianExternalUserId: text('guardian_external_user_id'),
1101
+ status: text('status').notNull(), // 'active' | 'consumed' | 'expired' | 'revoked'
1102
+ expiresAt: text('expires_at').notNull(),
1103
+ consumedAt: text('consumed_at'),
1104
+ consumedByRequestId: text('consumed_by_request_id'),
1105
+ createdAt: text('created_at').notNull(),
1106
+ updatedAt: text('updated_at').notNull(),
1107
+ }, (table) => [
1108
+ index('idx_scoped_grants_request_id').on(table.requestId),
1109
+ index('idx_scoped_grants_tool_sig').on(table.toolName, table.inputDigest),
1110
+ index('idx_scoped_grants_status_expires').on(table.status, table.expiresAt),
1111
+ ]);