@vellumai/assistant 0.3.18 → 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.
- package/ARCHITECTURE.md +4 -0
- package/docs/architecture/security.md +80 -0
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -0
- package/src/__tests__/call-controller.test.ts +170 -0
- package/src/__tests__/checker.test.ts +60 -0
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +511 -0
- package/src/__tests__/guardian-dispatch.test.ts +61 -1
- package/src/__tests__/guardian-grant-minting.test.ts +543 -0
- package/src/__tests__/ipc-snapshot.test.ts +1 -0
- package/src/__tests__/remote-skill-policy.test.ts +215 -0
- package/src/__tests__/scoped-approval-grants.test.ts +521 -0
- package/src/__tests__/scoped-grant-security-matrix.test.ts +443 -0
- package/src/__tests__/trust-store.test.ts +2 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +571 -0
- package/src/calls/call-controller.ts +27 -6
- package/src/calls/call-domain.ts +12 -0
- package/src/calls/guardian-dispatch.ts +8 -0
- package/src/calls/relay-server.ts +13 -0
- package/src/calls/voice-session-bridge.ts +42 -3
- package/src/config/bundled-skills/notifications/SKILL.md +18 -0
- package/src/config/schema.ts +6 -0
- package/src/config/skills-schema.ts +27 -0
- package/src/daemon/handlers/config-channels.ts +18 -0
- package/src/daemon/handlers/skills.ts +45 -2
- package/src/daemon/ipc-contract/skills.ts +1 -0
- package/src/daemon/session-process.ts +12 -0
- package/src/memory/db-init.ts +9 -1
- package/src/memory/embedding-local.ts +16 -7
- package/src/memory/guardian-action-store.ts +8 -0
- package/src/memory/guardian-verification.ts +1 -1
- package/src/memory/migrations/033-scoped-approval-grants.ts +51 -0
- package/src/memory/migrations/034-guardian-action-tool-metadata.ts +12 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema.ts +30 -0
- package/src/memory/scoped-approval-grants.ts +509 -0
- package/src/permissions/checker.ts +27 -0
- package/src/runtime/guardian-action-grant-minter.ts +97 -0
- package/src/runtime/routes/guardian-approval-interception.ts +116 -0
- package/src/runtime/routes/inbound-message-handler.ts +94 -27
- package/src/security/tool-approval-digest.ts +67 -0
- package/src/skills/remote-skill-policy.ts +131 -0
|
@@ -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
|
+
});
|
|
@@ -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();
|