@vellumai/assistant 0.3.16 → 0.3.19

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 (114) hide show
  1. package/ARCHITECTURE.md +74 -13
  2. package/README.md +6 -0
  3. package/docs/architecture/http-token-refresh.md +23 -1
  4. package/docs/architecture/security.md +80 -0
  5. package/package.json +1 -1
  6. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -0
  7. package/src/__tests__/access-request-decision.test.ts +4 -7
  8. package/src/__tests__/call-controller.test.ts +170 -0
  9. package/src/__tests__/channel-guardian.test.ts +3 -1
  10. package/src/__tests__/checker.test.ts +139 -48
  11. package/src/__tests__/config-watcher.test.ts +11 -13
  12. package/src/__tests__/conversation-pairing.test.ts +103 -3
  13. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -1
  14. package/src/__tests__/guardian-action-followup-executor.test.ts +1 -1
  15. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +511 -0
  16. package/src/__tests__/guardian-action-late-reply.test.ts +131 -0
  17. package/src/__tests__/guardian-action-store.test.ts +182 -0
  18. package/src/__tests__/guardian-dispatch.test.ts +180 -0
  19. package/src/__tests__/guardian-grant-minting.test.ts +543 -0
  20. package/src/__tests__/ipc-snapshot.test.ts +22 -0
  21. package/src/__tests__/non-member-access-request.test.ts +1 -2
  22. package/src/__tests__/notification-broadcaster.test.ts +115 -4
  23. package/src/__tests__/notification-decision-strategy.test.ts +2 -1
  24. package/src/__tests__/notification-deep-link.test.ts +44 -1
  25. package/src/__tests__/notification-guardian-path.test.ts +157 -0
  26. package/src/__tests__/notification-thread-candidate-validation.test.ts +215 -0
  27. package/src/__tests__/remote-skill-policy.test.ts +215 -0
  28. package/src/__tests__/scoped-approval-grants.test.ts +521 -0
  29. package/src/__tests__/scoped-grant-security-matrix.test.ts +443 -0
  30. package/src/__tests__/slack-channel-config.test.ts +3 -3
  31. package/src/__tests__/trust-store.test.ts +23 -21
  32. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +5 -7
  33. package/src/__tests__/trusted-contact-multichannel.test.ts +2 -6
  34. package/src/__tests__/trusted-contact-verification.test.ts +9 -9
  35. package/src/__tests__/update-bulletin-state.test.ts +1 -1
  36. package/src/__tests__/update-bulletin.test.ts +66 -3
  37. package/src/__tests__/update-template-contract.test.ts +6 -11
  38. package/src/__tests__/voice-scoped-grant-consumer.test.ts +571 -0
  39. package/src/__tests__/voice-session-bridge.test.ts +109 -9
  40. package/src/calls/call-controller.ts +150 -8
  41. package/src/calls/call-domain.ts +12 -0
  42. package/src/calls/guardian-action-sweep.ts +1 -1
  43. package/src/calls/guardian-dispatch.ts +16 -0
  44. package/src/calls/relay-server.ts +13 -0
  45. package/src/calls/voice-session-bridge.ts +46 -5
  46. package/src/cli/core-commands.ts +41 -1
  47. package/src/config/bundled-skills/notifications/SKILL.md +18 -0
  48. package/src/config/schema.ts +6 -0
  49. package/src/config/skills-schema.ts +27 -0
  50. package/src/config/templates/UPDATES.md +5 -6
  51. package/src/config/update-bulletin-format.ts +2 -0
  52. package/src/config/update-bulletin-state.ts +1 -1
  53. package/src/config/update-bulletin-template-path.ts +6 -0
  54. package/src/config/update-bulletin.ts +21 -6
  55. package/src/daemon/config-watcher.ts +3 -2
  56. package/src/daemon/daemon-control.ts +64 -10
  57. package/src/daemon/handlers/config-channels.ts +18 -0
  58. package/src/daemon/handlers/config-slack-channel.ts +1 -1
  59. package/src/daemon/handlers/identity.ts +45 -25
  60. package/src/daemon/handlers/sessions.ts +1 -1
  61. package/src/daemon/handlers/skills.ts +45 -2
  62. package/src/daemon/ipc-contract/sessions.ts +1 -1
  63. package/src/daemon/ipc-contract/skills.ts +1 -0
  64. package/src/daemon/ipc-contract/workspace.ts +12 -1
  65. package/src/daemon/ipc-contract-inventory.json +1 -0
  66. package/src/daemon/lifecycle.ts +8 -0
  67. package/src/daemon/server.ts +25 -3
  68. package/src/daemon/session-process.ts +450 -184
  69. package/src/daemon/tls-certs.ts +17 -12
  70. package/src/daemon/tool-side-effects.ts +1 -1
  71. package/src/memory/channel-delivery-store.ts +18 -20
  72. package/src/memory/channel-guardian-store.ts +39 -42
  73. package/src/memory/conversation-crud.ts +2 -2
  74. package/src/memory/conversation-queries.ts +2 -2
  75. package/src/memory/conversation-store.ts +24 -25
  76. package/src/memory/db-init.ts +17 -1
  77. package/src/memory/embedding-local.ts +16 -7
  78. package/src/memory/fts-reconciler.ts +41 -26
  79. package/src/memory/guardian-action-store.ts +65 -7
  80. package/src/memory/guardian-verification.ts +1 -0
  81. package/src/memory/jobs-worker.ts +2 -2
  82. package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +15 -0
  83. package/src/memory/migrations/032-notification-delivery-thread-decision.ts +20 -0
  84. package/src/memory/migrations/033-scoped-approval-grants.ts +51 -0
  85. package/src/memory/migrations/034-guardian-action-tool-metadata.ts +12 -0
  86. package/src/memory/migrations/index.ts +6 -2
  87. package/src/memory/schema-migration.ts +1 -0
  88. package/src/memory/schema.ts +36 -1
  89. package/src/memory/scoped-approval-grants.ts +509 -0
  90. package/src/memory/search/semantic.ts +3 -3
  91. package/src/notifications/README.md +158 -17
  92. package/src/notifications/broadcaster.ts +68 -50
  93. package/src/notifications/conversation-pairing.ts +96 -18
  94. package/src/notifications/decision-engine.ts +6 -3
  95. package/src/notifications/deliveries-store.ts +12 -0
  96. package/src/notifications/emit-signal.ts +1 -0
  97. package/src/notifications/thread-candidates.ts +60 -25
  98. package/src/notifications/types.ts +2 -1
  99. package/src/permissions/checker.ts +28 -16
  100. package/src/permissions/defaults.ts +14 -4
  101. package/src/runtime/guardian-action-followup-executor.ts +1 -1
  102. package/src/runtime/guardian-action-grant-minter.ts +97 -0
  103. package/src/runtime/http-server.ts +11 -11
  104. package/src/runtime/routes/access-request-decision.ts +1 -1
  105. package/src/runtime/routes/debug-routes.ts +4 -4
  106. package/src/runtime/routes/guardian-approval-interception.ts +120 -4
  107. package/src/runtime/routes/inbound-message-handler.ts +100 -33
  108. package/src/runtime/routes/integration-routes.ts +2 -2
  109. package/src/security/tool-approval-digest.ts +67 -0
  110. package/src/skills/remote-skill-policy.ts +131 -0
  111. package/src/tools/permission-checker.ts +1 -2
  112. package/src/tools/secret-detection-handler.ts +1 -1
  113. package/src/tools/system/voice-config.ts +1 -1
  114. package/src/version.ts +29 -2
@@ -0,0 +1,443 @@
1
+ /**
2
+ * Security test matrix for channel-agnostic scoped approval grants.
3
+ *
4
+ * This file covers scenarios NOT already tested in:
5
+ * - scoped-approval-grants.test.ts (CRUD, digest, basic consume semantics)
6
+ * - voice-scoped-grant-consumer.test.ts (voice bridge integration)
7
+ * - guardian-grant-minting.test.ts (grant minting on approval decisions)
8
+ *
9
+ * Additional scenarios tested here:
10
+ * 6. Requester identity mismatch denied
11
+ * 8. Concurrent consume attempts: only one succeeds
12
+ * 12. Restart behavior remains fail-closed — grants stored in persistent DB
13
+ *
14
+ * Cross-reference:
15
+ * 1. Voice happy path — voice-scoped-grant-consumer.test.ts
16
+ * 2. Replay denied — scoped-approval-grants.test.ts + voice-scoped-grant-consumer.test.ts
17
+ * 3. Tool mismatch denied — scoped-approval-grants.test.ts + voice-scoped-grant-consumer.test.ts
18
+ * 4. Input mismatch denied — scoped-approval-grants.test.ts
19
+ * 5. Execution-channel mismatch denied — scoped-approval-grants.test.ts
20
+ * 7. Expired grant denied — scoped-approval-grants.test.ts
21
+ * 9. Stale decision cannot mint extra grant — guardian-grant-minting.test.ts
22
+ * 10. Informational ASK_GUARDIAN cannot mint grant — guardian-grant-minting.test.ts
23
+ * 11. Guardian identity mismatch cannot mint grant — guardian-grant-minting.test.ts
24
+ */
25
+
26
+ import { mkdtempSync, rmSync } from 'node:fs';
27
+ import { tmpdir } from 'node:os';
28
+ import { join } from 'node:path';
29
+
30
+ import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
31
+
32
+ const testDir = mkdtempSync(join(tmpdir(), 'scoped-grant-security-matrix-'));
33
+
34
+ mock.module('../util/platform.js', () => ({
35
+ getDataDir: () => testDir,
36
+ isMacOS: () => process.platform === 'darwin',
37
+ isLinux: () => process.platform === 'linux',
38
+ isWindows: () => process.platform === 'win32',
39
+ getSocketPath: () => join(testDir, 'test.sock'),
40
+ getPidPath: () => join(testDir, 'test.pid'),
41
+ getDbPath: () => join(testDir, 'test.db'),
42
+ getLogPath: () => join(testDir, 'test.log'),
43
+ ensureDataDir: () => {},
44
+ migrateToDataLayout: () => {},
45
+ migrateToWorkspaceLayout: () => {},
46
+ }));
47
+
48
+ mock.module('../util/logger.js', () => ({
49
+ getLogger: () =>
50
+ new Proxy({} as Record<string, unknown>, {
51
+ get: () => () => {},
52
+ }),
53
+ isDebug: () => false,
54
+ truncateForLog: (value: string) => value,
55
+ }));
56
+
57
+ import {
58
+ type CreateScopedApprovalGrantParams,
59
+ consumeScopedApprovalGrantByToolSignature,
60
+ createScopedApprovalGrant,
61
+ } from '../memory/scoped-approval-grants.js';
62
+ import { getDb, initializeDb, resetDb } from '../memory/db.js';
63
+ import { scopedApprovalGrants } from '../memory/schema.js';
64
+ import { computeToolApprovalDigest } from '../security/tool-approval-digest.js';
65
+
66
+ initializeDb();
67
+
68
+ function clearTables(): void {
69
+ const db = getDb();
70
+ db.delete(scopedApprovalGrants).run();
71
+ }
72
+
73
+ afterAll(() => {
74
+ resetDb();
75
+ try {
76
+ rmSync(testDir, { recursive: true });
77
+ } catch {
78
+ /* best effort */
79
+ }
80
+ });
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Helper to build grant params with sensible defaults
84
+ // ---------------------------------------------------------------------------
85
+
86
+ function grantParams(overrides: Partial<CreateScopedApprovalGrantParams> = {}): CreateScopedApprovalGrantParams {
87
+ const futureExpiry = new Date(Date.now() + 60_000).toISOString();
88
+ return {
89
+ assistantId: 'self',
90
+ scopeMode: 'tool_signature',
91
+ toolName: 'bash',
92
+ inputDigest: computeToolApprovalDigest('bash', { cmd: 'ls' }),
93
+ requestChannel: 'telegram',
94
+ decisionChannel: 'telegram',
95
+ expiresAt: futureExpiry,
96
+ ...overrides,
97
+ };
98
+ }
99
+
100
+ // ===========================================================================
101
+ // 6. Requester identity mismatch denied
102
+ // ===========================================================================
103
+
104
+ describe('security matrix: requester identity mismatch', () => {
105
+ beforeEach(() => clearTables());
106
+
107
+ test('grant scoped to a specific requester cannot be consumed by a different requester', () => {
108
+ const digest = computeToolApprovalDigest('bash', { cmd: 'ls' });
109
+ createScopedApprovalGrant(
110
+ grantParams({
111
+ toolName: 'bash',
112
+ inputDigest: digest,
113
+ requesterExternalUserId: 'user-alice',
114
+ }),
115
+ );
116
+
117
+ // Attempt to consume as a different user
118
+ const wrongUser = consumeScopedApprovalGrantByToolSignature({
119
+ toolName: 'bash',
120
+ inputDigest: digest,
121
+ consumingRequestId: 'c1',
122
+ requesterExternalUserId: 'user-bob',
123
+ });
124
+ expect(wrongUser.ok).toBe(false);
125
+
126
+ // Correct user succeeds
127
+ const correctUser = consumeScopedApprovalGrantByToolSignature({
128
+ toolName: 'bash',
129
+ inputDigest: digest,
130
+ consumingRequestId: 'c2',
131
+ requesterExternalUserId: 'user-alice',
132
+ });
133
+ expect(correctUser.ok).toBe(true);
134
+ });
135
+
136
+ test('grant with null requesterExternalUserId allows any requester (wildcard)', () => {
137
+ const digest = computeToolApprovalDigest('bash', { cmd: 'ls' });
138
+ createScopedApprovalGrant(
139
+ grantParams({
140
+ toolName: 'bash',
141
+ inputDigest: digest,
142
+ requesterExternalUserId: null,
143
+ }),
144
+ );
145
+
146
+ // Any user can consume when requester is null (wildcard)
147
+ const result = consumeScopedApprovalGrantByToolSignature({
148
+ toolName: 'bash',
149
+ inputDigest: digest,
150
+ consumingRequestId: 'c1',
151
+ requesterExternalUserId: 'user-anyone',
152
+ });
153
+ expect(result.ok).toBe(true);
154
+ });
155
+
156
+ test('consume without providing requester only matches grants with null requester', () => {
157
+ const digest = computeToolApprovalDigest('bash', { cmd: 'ls' });
158
+
159
+ // Grant scoped to a specific requester
160
+ createScopedApprovalGrant(
161
+ grantParams({
162
+ toolName: 'bash',
163
+ inputDigest: digest,
164
+ requesterExternalUserId: 'user-alice',
165
+ }),
166
+ );
167
+
168
+ // Consume without specifying requester — should NOT match a requester-scoped grant
169
+ const result = consumeScopedApprovalGrantByToolSignature({
170
+ toolName: 'bash',
171
+ inputDigest: digest,
172
+ consumingRequestId: 'c1',
173
+ // No requesterExternalUserId provided
174
+ });
175
+ expect(result.ok).toBe(false);
176
+ });
177
+ });
178
+
179
+ // ===========================================================================
180
+ // 8. Concurrent consume attempts: only one succeeds
181
+ // ===========================================================================
182
+
183
+ describe('security matrix: concurrent consume (CAS)', () => {
184
+ beforeEach(() => clearTables());
185
+
186
+ test('only one of multiple concurrent consumers succeeds for the same grant', () => {
187
+ const digest = computeToolApprovalDigest('bash', { cmd: 'rm -rf /' });
188
+ createScopedApprovalGrant(
189
+ grantParams({
190
+ toolName: 'bash',
191
+ inputDigest: digest,
192
+ }),
193
+ );
194
+
195
+ // Simulate concurrent consumers racing to consume the same grant.
196
+ // Since SQLite is synchronous in Bun, we simulate by issuing
197
+ // back-to-back consume calls — the CAS mechanism ensures only the
198
+ // first succeeds.
199
+ const results: boolean[] = [];
200
+ for (let i = 0; i < 5; i++) {
201
+ const result = consumeScopedApprovalGrantByToolSignature({
202
+ toolName: 'bash',
203
+ inputDigest: digest,
204
+ consumingRequestId: `concurrent-consumer-${i}`,
205
+ });
206
+ results.push(result.ok);
207
+ }
208
+
209
+ // Exactly one should succeed
210
+ const successes = results.filter(Boolean);
211
+ expect(successes.length).toBe(1);
212
+
213
+ // The first consumer should win
214
+ expect(results[0]).toBe(true);
215
+ });
216
+
217
+ test('with multiple matching grants, each consumer gets at most one grant', () => {
218
+ const digest = computeToolApprovalDigest('bash', { cmd: 'ls' });
219
+
220
+ // Create 3 grants for the same tool signature
221
+ for (let i = 0; i < 3; i++) {
222
+ createScopedApprovalGrant(
223
+ grantParams({
224
+ toolName: 'bash',
225
+ inputDigest: digest,
226
+ }),
227
+ );
228
+ }
229
+
230
+ // 5 consumers compete for 3 grants
231
+ const results: boolean[] = [];
232
+ for (let i = 0; i < 5; i++) {
233
+ const result = consumeScopedApprovalGrantByToolSignature({
234
+ toolName: 'bash',
235
+ inputDigest: digest,
236
+ consumingRequestId: `consumer-${i}`,
237
+ });
238
+ results.push(result.ok);
239
+ }
240
+
241
+ // Exactly 3 should succeed (one per grant)
242
+ const successes = results.filter(Boolean);
243
+ expect(successes.length).toBe(3);
244
+
245
+ // The last 2 should fail
246
+ expect(results[3]).toBe(false);
247
+ expect(results[4]).toBe(false);
248
+ });
249
+ });
250
+
251
+ // ===========================================================================
252
+ // 12. Restart behavior remains fail-closed — grants stored in persistent DB
253
+ // ===========================================================================
254
+
255
+ describe('security matrix: persistence and fail-closed behavior', () => {
256
+ beforeEach(() => clearTables());
257
+
258
+ test('grants survive DB re-initialization (simulating daemon restart)', () => {
259
+ const digest = computeToolApprovalDigest('bash', { cmd: 'ls' });
260
+
261
+ // Create a grant
262
+ const grant = createScopedApprovalGrant(
263
+ grantParams({
264
+ toolName: 'bash',
265
+ inputDigest: digest,
266
+ }),
267
+ );
268
+ expect(grant.status).toBe('active');
269
+
270
+ // Re-initialize the DB (simulates daemon restart — the SQLite file persists)
271
+ initializeDb();
272
+
273
+ // The grant should still be consumable after restart
274
+ const result = consumeScopedApprovalGrantByToolSignature({
275
+ toolName: 'bash',
276
+ inputDigest: digest,
277
+ consumingRequestId: 'post-restart-consumer',
278
+ });
279
+ expect(result.ok).toBe(true);
280
+ expect(result.grant!.id).toBe(grant.id);
281
+ });
282
+
283
+ test('consumed grants remain consumed after DB re-initialization', () => {
284
+ const digest = computeToolApprovalDigest('bash', { cmd: 'ls' });
285
+
286
+ createScopedApprovalGrant(
287
+ grantParams({
288
+ toolName: 'bash',
289
+ inputDigest: digest,
290
+ }),
291
+ );
292
+
293
+ // Consume the grant
294
+ const first = consumeScopedApprovalGrantByToolSignature({
295
+ toolName: 'bash',
296
+ inputDigest: digest,
297
+ consumingRequestId: 'pre-restart-consumer',
298
+ });
299
+ expect(first.ok).toBe(true);
300
+
301
+ // Re-initialize the DB (simulates daemon restart)
302
+ initializeDb();
303
+
304
+ // The consumed grant must NOT be consumable again after restart
305
+ const second = consumeScopedApprovalGrantByToolSignature({
306
+ toolName: 'bash',
307
+ inputDigest: digest,
308
+ consumingRequestId: 'post-restart-consumer',
309
+ });
310
+ expect(second.ok).toBe(false);
311
+ });
312
+
313
+ test('no grants means fail-closed (deny by default)', () => {
314
+ // Empty grant table — no grants at all
315
+ const digest = computeToolApprovalDigest('bash', { cmd: 'dangerous-command' });
316
+
317
+ const result = consumeScopedApprovalGrantByToolSignature({
318
+ toolName: 'bash',
319
+ inputDigest: digest,
320
+ consumingRequestId: 'consumer-1',
321
+ });
322
+
323
+ // Must fail closed — no grant = no permission
324
+ expect(result.ok).toBe(false);
325
+ expect(result.grant).toBeNull();
326
+ });
327
+ });
328
+
329
+ // ===========================================================================
330
+ // Combined cross-scope invariants
331
+ // ===========================================================================
332
+
333
+ describe('security matrix: cross-scope invariants', () => {
334
+ beforeEach(() => clearTables());
335
+
336
+ test('grant for one assistant cannot be consumed by another assistant', () => {
337
+ const digest = computeToolApprovalDigest('bash', { cmd: 'ls' });
338
+ createScopedApprovalGrant(
339
+ grantParams({
340
+ toolName: 'bash',
341
+ inputDigest: digest,
342
+ assistantId: 'assistant-alpha',
343
+ }),
344
+ );
345
+
346
+ // Attempt consumption from a different assistant
347
+ const wrongAssistant = consumeScopedApprovalGrantByToolSignature({
348
+ toolName: 'bash',
349
+ inputDigest: digest,
350
+ consumingRequestId: 'c1',
351
+ assistantId: 'assistant-beta',
352
+ });
353
+ expect(wrongAssistant.ok).toBe(false);
354
+
355
+ // Correct assistant succeeds
356
+ const correctAssistant = consumeScopedApprovalGrantByToolSignature({
357
+ toolName: 'bash',
358
+ inputDigest: digest,
359
+ consumingRequestId: 'c2',
360
+ assistantId: 'assistant-alpha',
361
+ });
362
+ expect(correctAssistant.ok).toBe(true);
363
+ });
364
+
365
+ test('all scope fields must match simultaneously for consumption', () => {
366
+ const digest = computeToolApprovalDigest('bash', { cmd: 'ls' });
367
+
368
+ // Create a maximally-scoped grant
369
+ createScopedApprovalGrant(
370
+ grantParams({
371
+ toolName: 'bash',
372
+ inputDigest: digest,
373
+ assistantId: 'self',
374
+ executionChannel: 'voice',
375
+ conversationId: 'conv-123',
376
+ callSessionId: 'call-456',
377
+ requesterExternalUserId: 'user-alice',
378
+ }),
379
+ );
380
+
381
+ // Each field mismatch should independently cause failure:
382
+
383
+ // Wrong execution channel
384
+ expect(consumeScopedApprovalGrantByToolSignature({
385
+ toolName: 'bash',
386
+ inputDigest: digest,
387
+ consumingRequestId: 'c-chan',
388
+ assistantId: 'self',
389
+ executionChannel: 'sms',
390
+ conversationId: 'conv-123',
391
+ callSessionId: 'call-456',
392
+ requesterExternalUserId: 'user-alice',
393
+ }).ok).toBe(false);
394
+
395
+ // Wrong conversation
396
+ expect(consumeScopedApprovalGrantByToolSignature({
397
+ toolName: 'bash',
398
+ inputDigest: digest,
399
+ consumingRequestId: 'c-conv',
400
+ assistantId: 'self',
401
+ executionChannel: 'voice',
402
+ conversationId: 'conv-999',
403
+ callSessionId: 'call-456',
404
+ requesterExternalUserId: 'user-alice',
405
+ }).ok).toBe(false);
406
+
407
+ // Wrong call session
408
+ expect(consumeScopedApprovalGrantByToolSignature({
409
+ toolName: 'bash',
410
+ inputDigest: digest,
411
+ consumingRequestId: 'c-call',
412
+ assistantId: 'self',
413
+ executionChannel: 'voice',
414
+ conversationId: 'conv-123',
415
+ callSessionId: 'call-999',
416
+ requesterExternalUserId: 'user-alice',
417
+ }).ok).toBe(false);
418
+
419
+ // Wrong requester
420
+ expect(consumeScopedApprovalGrantByToolSignature({
421
+ toolName: 'bash',
422
+ inputDigest: digest,
423
+ consumingRequestId: 'c-user',
424
+ assistantId: 'self',
425
+ executionChannel: 'voice',
426
+ conversationId: 'conv-123',
427
+ callSessionId: 'call-456',
428
+ requesterExternalUserId: 'user-bob',
429
+ }).ok).toBe(false);
430
+
431
+ // All fields match — succeeds
432
+ expect(consumeScopedApprovalGrantByToolSignature({
433
+ toolName: 'bash',
434
+ inputDigest: digest,
435
+ consumingRequestId: 'c-all',
436
+ assistantId: 'self',
437
+ executionChannel: 'voice',
438
+ conversationId: 'conv-123',
439
+ callSessionId: 'call-456',
440
+ requesterExternalUserId: 'user-alice',
441
+ }).ok).toBe(true);
442
+ });
443
+ });
@@ -105,9 +105,9 @@ mock.module('../tools/credentials/metadata-store.js', () => ({
105
105
  const originalFetch = globalThis.fetch;
106
106
 
107
107
  import {
108
+ clearSlackChannelConfig,
108
109
  getSlackChannelConfig,
109
110
  setSlackChannelConfig,
110
- clearSlackChannelConfig,
111
111
  } from '../daemon/handlers/config-slack-channel.js';
112
112
 
113
113
  afterAll(() => {
@@ -186,7 +186,7 @@ describe('Slack channel config handler', () => {
186
186
  status: 200,
187
187
  headers: { 'content-type': 'application/json' },
188
188
  });
189
- }) as typeof globalThis.fetch;
189
+ }) as unknown as typeof globalThis.fetch;
190
190
 
191
191
  const result = await setSlackChannelConfig('xoxb-valid-bot-token');
192
192
  expect(result.success).toBe(true);
@@ -204,7 +204,7 @@ describe('Slack channel config handler', () => {
204
204
  status: 200,
205
205
  headers: { 'content-type': 'application/json' },
206
206
  });
207
- }) as typeof globalThis.fetch;
207
+ }) as unknown as typeof globalThis.fetch;
208
208
 
209
209
  const result = await setSlackChannelConfig('xoxb-bad-token');
210
210
  expect(result.success).toBe(false);
@@ -297,15 +297,15 @@ describe('Trust Store', () => {
297
297
  });
298
298
 
299
299
  test('returns null when tool does not match', () => {
300
- addRule('file_write', 'git *', '/tmp');
301
- // host_bash default is 'ask' so findMatchingRule (allow-only) won't find it
302
- const match = findMatchingRule('host_bash', 'git push', '/tmp');
300
+ addRule('file_write', 'file_write:/tmp/*', '/tmp');
301
+ // host_file_read default is 'ask' so findMatchingRule (allow-only) won't find it
302
+ const match = findMatchingRule('host_file_read', 'host_file_read:/etc/hosts', '/tmp');
303
303
  expect(match).toBeNull();
304
304
  });
305
305
 
306
306
  test('returns null when pattern does not match', () => {
307
- addRule('host_bash', 'git *', '/tmp');
308
- const match = findMatchingRule('host_bash', 'npm install', '/tmp');
307
+ addRule('host_file_read', 'host_file_read:/etc/hosts', '/tmp');
308
+ const match = findMatchingRule('host_file_read', 'host_file_read:/var/log/syslog', '/tmp');
309
309
  expect(match).toBeNull();
310
310
  });
311
311
 
@@ -324,8 +324,8 @@ describe('Trust Store', () => {
324
324
  });
325
325
 
326
326
  test('does not match when scope is outside rule scope', () => {
327
- addRule('host_bash', 'npm *', '/home/user/project');
328
- const match = findMatchingRule('host_bash', 'npm install', '/home/other');
327
+ addRule('host_file_read', 'host_file_read:/home/user/project/*', '/home/user/project');
328
+ const match = findMatchingRule('host_file_read', 'host_file_read:/home/user/project/file.txt', '/home/other');
329
329
  expect(match).toBeNull();
330
330
  });
331
331
 
@@ -342,8 +342,8 @@ describe('Trust Store', () => {
342
342
  });
343
343
 
344
344
  test('does not match sibling path with shared prefix', () => {
345
- addRule('host_bash', 'npm *', '/home/user/project');
346
- const match = findMatchingRule('host_bash', 'npm install', '/home/user/project-evil');
345
+ addRule('host_file_read', 'host_file_read:/home/user/project/*', '/home/user/project');
346
+ const match = findMatchingRule('host_file_read', 'host_file_read:/home/user/project/file.txt', '/home/user/project-evil');
347
347
  expect(match).toBeNull();
348
348
  });
349
349
 
@@ -360,8 +360,8 @@ describe('Trust Store', () => {
360
360
  });
361
361
 
362
362
  test('does not match sibling with glob-suffixed scope', () => {
363
- addRule('host_bash', 'npm *', '/home/user/project*');
364
- const match = findMatchingRule('host_bash', 'npm install', '/home/user/project-evil');
363
+ addRule('host_file_read', 'host_file_read:/home/user/project/*', '/home/user/project*');
364
+ const match = findMatchingRule('host_file_read', 'host_file_read:/home/user/project/file.txt', '/home/user/project-evil');
365
365
  expect(match).toBeNull();
366
366
  });
367
367
  });
@@ -375,9 +375,9 @@ describe('Trust Store', () => {
375
375
  });
376
376
 
377
377
  test('matches exact string', () => {
378
- addRule('host_bash', 'git status', '/tmp');
379
- expect(findMatchingRule('host_bash', 'git status', '/tmp')).not.toBeNull();
380
- expect(findMatchingRule('host_bash', 'git push', '/tmp')).toBeNull();
378
+ addRule('host_file_read', 'host_file_read:/etc/hosts', '/tmp');
379
+ expect(findMatchingRule('host_file_read', 'host_file_read:/etc/hosts', '/tmp')).not.toBeNull();
380
+ expect(findMatchingRule('host_file_read', 'host_file_read:/etc/passwd', '/tmp')).toBeNull();
381
381
  });
382
382
 
383
383
  test('matches file path pattern', () => {
@@ -545,9 +545,9 @@ describe('Trust Store', () => {
545
545
  });
546
546
 
547
547
  test('findMatchingRule ignores deny rules', () => {
548
- // Use host_bashbash has a default allow rule that would match.
549
- addRule('host_bash', 'rm *', '/tmp', 'deny');
550
- const match = findMatchingRule('host_bash', 'rm file.txt', '/tmp');
548
+ // Use host_file_readit has an 'ask' default so findMatchingRule (allow-only) won't find it.
549
+ addRule('host_file_read', 'host_file_read:/etc/*', '/tmp', 'deny');
550
+ const match = findMatchingRule('host_file_read', 'host_file_read:/etc/hosts', '/tmp');
551
551
  expect(match).toBeNull();
552
552
  });
553
553
 
@@ -806,12 +806,12 @@ describe('Trust Store', () => {
806
806
  expect(match!.priority).toBe(DEFAULT_PRIORITY_BY_ID.get('default:ask-host_file_edit-global')!);
807
807
  });
808
808
 
809
- test('findHighestPriorityRule matches default ask for host_bash', () => {
809
+ test('findHighestPriorityRule matches default allow for host_bash', () => {
810
810
  const match = findHighestPriorityRule('host_bash', ['ls'], '/tmp');
811
811
  expect(match).not.toBeNull();
812
- expect(match!.id).toBe('default:ask-host_bash-global');
813
- expect(match!.decision).toBe('ask');
814
- expect(match!.priority).toBe(DEFAULT_PRIORITY_BY_ID.get('default:ask-host_bash-global')!);
812
+ expect(match!.id).toBe('default:allow-host_bash-global');
813
+ expect(match!.decision).toBe('allow');
814
+ expect(match!.priority).toBe(DEFAULT_PRIORITY_BY_ID.get('default:allow-host_bash-global')!);
815
815
  });
816
816
 
817
817
  test('findHighestPriorityRule matches default ask for computer_use_click', () => {
@@ -838,6 +838,7 @@ describe('Trust Store', () => {
838
838
  expect(match).not.toBeNull();
839
839
  expect(match!.id).toBe('default:allow-bash-rm-bootstrap');
840
840
  expect(match!.decision).toBe('allow');
841
+ expect(match!.allowHighRisk).toBe(true);
841
842
  // Outside workspace, the bootstrap rule doesn't match — the global
842
843
  // default:allow-bash-global rule matches instead (not the bootstrap rule).
843
844
  const other = findHighestPriorityRule('bash', ['rm BOOTSTRAP.md'], '/tmp/other-project');
@@ -852,6 +853,7 @@ describe('Trust Store', () => {
852
853
  expect(match).not.toBeNull();
853
854
  expect(match!.id).toBe('default:allow-bash-rm-updates');
854
855
  expect(match!.decision).toBe('allow');
856
+ expect(match!.allowHighRisk).toBe(true);
855
857
  // Outside workspace, should NOT match the updates rule
856
858
  const other = findHighestPriorityRule('bash', ['rm UPDATES.md'], '/tmp/other-project');
857
859
  expect(other).not.toBeNull();
@@ -85,14 +85,12 @@ mock.module('../runtime/approval-message-composer.js', () => ({
85
85
  import {
86
86
  createApprovalRequest,
87
87
  createBinding,
88
- findPendingAccessRequestForRequester,
89
- getAllPendingApprovalsByGuardianChat,
90
88
  } from '../memory/channel-guardian-store.js';
89
+ import { getDb, initializeDb, resetDb } from '../memory/db.js';
90
+ import { findMember, upsertMember } from '../memory/ingress-member-store.js';
91
91
  import {
92
92
  createOutboundSession,
93
93
  } from '../runtime/channel-guardian-service.js';
94
- import { findMember, upsertMember } from '../memory/ingress-member-store.js';
95
- import { initializeDb, resetDb } from '../memory/db.js';
96
94
  import { handleChannelInbound } from '../runtime/routes/channel-routes.js';
97
95
 
98
96
  initializeDb();
@@ -110,7 +108,6 @@ const TEST_BEARER_TOKEN = 'test-token';
110
108
  const GUARDIAN_APPROVAL_TTL_MS = 5 * 60 * 1000;
111
109
 
112
110
  function resetState(): void {
113
- const { getDb } = require('../memory/db.js');
114
111
  const db = getDb();
115
112
  db.run('DELETE FROM channel_guardian_approval_requests');
116
113
  db.run('DELETE FROM channel_guardian_bindings');
@@ -177,7 +174,7 @@ describe('trusted contact lifecycle notification signals', () => {
177
174
  const testRequestId = `req-deny-${Date.now()}`;
178
175
 
179
176
  // Create a pending access request approval
180
- const approval = createApprovalRequest({
177
+ const _approval = createApprovalRequest({
181
178
  runId: `ingress-access-request-${Date.now()}`,
182
179
  requestId: testRequestId,
183
180
  conversationId: 'access-req-telegram-requester-user-456',
@@ -252,7 +249,7 @@ describe('trusted contact lifecycle notification signals', () => {
252
249
  const testRequestId = `req-approve-${Date.now()}`;
253
250
 
254
251
  // Create a pending access request approval
255
- const approval = createApprovalRequest({
252
+ const _approval = createApprovalRequest({
256
253
  runId: `ingress-access-request-${Date.now()}`,
257
254
  requestId: testRequestId,
258
255
  conversationId: 'access-req-telegram-requester-user-456',
@@ -426,6 +423,7 @@ describe('trusted contact activated notification signal', () => {
426
423
 
427
424
  test('guardian verification does NOT emit activated signal', async () => {
428
425
  // Create an inbound challenge (guardian flow, not trusted contact)
426
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
429
427
  const { createVerificationChallenge } = require('../runtime/channel-guardian-service.js');
430
428
  const { secret } = createVerificationChallenge('self', 'telegram');
431
429
 
@@ -77,16 +77,13 @@ import {
77
77
  createBinding,
78
78
  findPendingAccessRequestForRequester,
79
79
  } from '../memory/channel-guardian-store.js';
80
+ import { getDb, initializeDb, resetDb } from '../memory/db.js';
81
+ import { findMember, upsertMember } from '../memory/ingress-member-store.js';
80
82
  import {
81
83
  createOutboundSession,
82
84
  validateAndConsumeChallenge,
83
85
  } from '../runtime/channel-guardian-service.js';
84
- import { findMember, upsertMember } from '../memory/ingress-member-store.js';
85
- import { initializeDb, resetDb } from '../memory/db.js';
86
86
  import { handleChannelInbound } from '../runtime/routes/channel-routes.js';
87
- import {
88
- handleAccessRequestDecision,
89
- } from '../runtime/routes/access-request-decision.js';
90
87
 
91
88
  initializeDb();
92
89
 
@@ -102,7 +99,6 @@ afterAll(() => {
102
99
  const TEST_BEARER_TOKEN = 'test-token';
103
100
 
104
101
  function resetState(): void {
105
- const { getDb } = require('../memory/db.js');
106
102
  const db = getDb();
107
103
  db.run('DELETE FROM channel_guardian_approval_requests');
108
104
  db.run('DELETE FROM channel_guardian_bindings');