@vellumai/assistant 0.3.26 → 0.3.28
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 +48 -1
- package/Dockerfile +2 -2
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +6 -2
- package/src/__tests__/agent-loop.test.ts +119 -0
- package/src/__tests__/bundled-asset.test.ts +107 -0
- package/src/__tests__/canonical-guardian-store.test.ts +636 -0
- package/src/__tests__/channel-approval-routes.test.ts +174 -1
- package/src/__tests__/emit-signal-routing-intent.test.ts +43 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +205 -345
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +599 -0
- package/src/__tests__/guardian-dispatch.test.ts +19 -19
- package/src/__tests__/guardian-routing-invariants.test.ts +954 -0
- package/src/__tests__/mcp-cli.test.ts +77 -0
- package/src/__tests__/non-member-access-request.test.ts +31 -29
- package/src/__tests__/notification-decision-fallback.test.ts +61 -3
- package/src/__tests__/notification-decision-strategy.test.ts +17 -0
- package/src/__tests__/notification-guardian-path.test.ts +13 -15
- package/src/__tests__/onboarding-template-contract.test.ts +116 -21
- package/src/__tests__/secret-scanner-executor.test.ts +59 -0
- package/src/__tests__/secret-scanner.test.ts +8 -0
- package/src/__tests__/sensitive-output-placeholders.test.ts +208 -0
- package/src/__tests__/session-runtime-assembly.test.ts +76 -47
- package/src/__tests__/tool-grant-request-escalation.test.ts +497 -0
- package/src/agent/loop.ts +46 -3
- package/src/approvals/guardian-decision-primitive.ts +285 -0
- package/src/approvals/guardian-request-resolvers.ts +539 -0
- package/src/calls/guardian-dispatch.ts +46 -40
- package/src/calls/relay-server.ts +147 -2
- package/src/calls/types.ts +1 -1
- package/src/config/system-prompt.ts +2 -1
- package/src/config/templates/BOOTSTRAP.md +47 -31
- package/src/config/templates/USER.md +5 -0
- package/src/config/update-bulletin-template-path.ts +4 -1
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +22 -17
- package/src/daemon/handlers/guardian-actions.ts +45 -66
- package/src/daemon/ipc-contract/guardian-actions.ts +7 -0
- package/src/daemon/lifecycle.ts +3 -16
- package/src/daemon/server.ts +18 -0
- package/src/daemon/session-agent-loop-handlers.ts +5 -4
- package/src/daemon/session-agent-loop.ts +32 -5
- package/src/daemon/session-process.ts +68 -307
- package/src/daemon/session-runtime-assembly.ts +112 -24
- package/src/daemon/session-tool-setup.ts +1 -0
- package/src/daemon/session.ts +1 -0
- package/src/home-base/prebuilt/seed.ts +2 -1
- package/src/hooks/templates.ts +2 -1
- package/src/memory/canonical-guardian-store.ts +524 -0
- package/src/memory/channel-guardian-store.ts +1 -0
- package/src/memory/db-init.ts +16 -0
- package/src/memory/guardian-action-store.ts +7 -60
- package/src/memory/guardian-approvals.ts +9 -4
- package/src/memory/migrations/036-normalize-phone-identities.ts +289 -0
- package/src/memory/migrations/118-reminder-routing-intent.ts +3 -3
- package/src/memory/migrations/121-canonical-guardian-requests.ts +59 -0
- package/src/memory/migrations/122-canonical-guardian-requester-chat-id.ts +15 -0
- package/src/memory/migrations/123-canonical-guardian-deliveries-destination-index.ts +15 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/migrations/registry.ts +5 -0
- package/src/memory/schema-migration.ts +1 -0
- package/src/memory/schema.ts +52 -0
- package/src/notifications/copy-composer.ts +16 -4
- package/src/notifications/decision-engine.ts +57 -0
- package/src/permissions/defaults.ts +2 -0
- package/src/runtime/access-request-helper.ts +137 -0
- package/src/runtime/actor-trust-resolver.ts +225 -0
- package/src/runtime/channel-guardian-service.ts +12 -4
- package/src/runtime/guardian-context-resolver.ts +32 -7
- package/src/runtime/guardian-decision-types.ts +6 -0
- package/src/runtime/guardian-reply-router.ts +687 -0
- package/src/runtime/http-server.ts +8 -0
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +116 -0
- package/src/runtime/routes/conversation-routes.ts +18 -0
- package/src/runtime/routes/guardian-action-routes.ts +100 -109
- package/src/runtime/routes/inbound-message-handler.ts +170 -525
- package/src/runtime/tool-grant-request-helper.ts +195 -0
- package/src/tools/executor.ts +13 -1
- package/src/tools/sensitive-output-placeholders.ts +203 -0
- package/src/tools/tool-approval-handler.ts +44 -1
- package/src/tools/types.ts +11 -0
- package/src/util/bundled-asset.ts +31 -0
- package/src/util/canonicalize-identity.ts +52 -0
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
* - IPC handlers (guardian-actions.ts)
|
|
5
5
|
*
|
|
6
6
|
* Covers: conversationId scoping, stale handling, access-request routing,
|
|
7
|
-
* invalid action rejection,
|
|
7
|
+
* invalid action rejection, and not-found paths.
|
|
8
|
+
*
|
|
9
|
+
* All decisions now go through the canonical guardian decision primitive
|
|
10
|
+
* (`applyCanonicalGuardianDecision`), so tests create canonical requests
|
|
11
|
+
* and mock that function.
|
|
8
12
|
*/
|
|
9
13
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
10
14
|
import { tmpdir } from 'node:os';
|
|
@@ -33,49 +37,27 @@ mock.module('../util/logger.js', () => ({
|
|
|
33
37
|
}),
|
|
34
38
|
}));
|
|
35
39
|
|
|
36
|
-
// Mock
|
|
37
|
-
const
|
|
38
|
-
(..._args: any[]): { applied: boolean; requestId?: string; reason?: string;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
// Mock applyCanonicalGuardianDecision — the single decision write path.
|
|
41
|
+
const mockApplyCanonicalGuardianDecision = mock(
|
|
42
|
+
(..._args: any[]): Promise<{ applied: boolean; requestId?: string; reason?: string; grantMinted?: boolean }> =>
|
|
43
|
+
Promise.resolve({
|
|
44
|
+
applied: true,
|
|
45
|
+
requestId: 'req-123',
|
|
46
|
+
grantMinted: false,
|
|
47
|
+
}),
|
|
42
48
|
);
|
|
43
49
|
mock.module('../approvals/guardian-decision-primitive.js', () => ({
|
|
44
|
-
|
|
45
|
-
}));
|
|
46
|
-
|
|
47
|
-
// Mock handleChannelDecision for the pending-interactions fallback path
|
|
48
|
-
const mockHandleChannelDecision = mock(
|
|
49
|
-
(..._args: any[]): { applied: boolean; requestId?: string } => ({
|
|
50
|
-
applied: true,
|
|
51
|
-
requestId: 'req-456',
|
|
52
|
-
}),
|
|
53
|
-
);
|
|
54
|
-
mock.module('../runtime/channel-approvals.js', () => ({
|
|
55
|
-
handleChannelDecision: mockHandleChannelDecision,
|
|
56
|
-
}));
|
|
57
|
-
|
|
58
|
-
// Mock handleAccessRequestDecision for ingress_access_request routing
|
|
59
|
-
const mockHandleAccessRequestDecision = mock(
|
|
60
|
-
(..._args: any[]): { handled: boolean; type: string; verificationSessionId?: string; verificationCode?: string } => ({
|
|
61
|
-
handled: true,
|
|
62
|
-
type: 'approved',
|
|
63
|
-
verificationSessionId: 'vs-1',
|
|
64
|
-
verificationCode: '123456',
|
|
65
|
-
}),
|
|
66
|
-
);
|
|
67
|
-
mock.module('../runtime/routes/access-request-decision.js', () => ({
|
|
68
|
-
handleAccessRequestDecision: mockHandleAccessRequestDecision,
|
|
50
|
+
applyCanonicalGuardianDecision: mockApplyCanonicalGuardianDecision,
|
|
69
51
|
}));
|
|
70
52
|
|
|
71
53
|
import { guardianActionsHandlers } from '../daemon/handlers/guardian-actions.js';
|
|
72
54
|
import {
|
|
73
|
-
|
|
74
|
-
|
|
55
|
+
createCanonicalGuardianRequest,
|
|
56
|
+
generateCanonicalRequestCode,
|
|
57
|
+
} from '../memory/canonical-guardian-store.js';
|
|
75
58
|
import { initializeDb, resetDb } from '../memory/db.js';
|
|
76
59
|
import { getDb } from '../memory/db.js';
|
|
77
60
|
import { conversations } from '../memory/schema.js';
|
|
78
|
-
import * as pendingInteractions from '../runtime/pending-interactions.js';
|
|
79
61
|
import {
|
|
80
62
|
handleGuardianActionDecision,
|
|
81
63
|
handleGuardianActionsPending,
|
|
@@ -94,44 +76,44 @@ function ensureConversation(id: string): void {
|
|
|
94
76
|
|
|
95
77
|
function resetTables(): void {
|
|
96
78
|
const db = getDb();
|
|
79
|
+
db.run('DELETE FROM canonical_guardian_requests');
|
|
97
80
|
db.run('DELETE FROM channel_guardian_approval_requests');
|
|
98
81
|
db.run('DELETE FROM conversations');
|
|
99
|
-
|
|
100
|
-
mockApplyGuardianDecision.mockClear();
|
|
101
|
-
mockHandleChannelDecision.mockClear();
|
|
102
|
-
mockHandleAccessRequestDecision.mockClear();
|
|
82
|
+
mockApplyCanonicalGuardianDecision.mockClear();
|
|
103
83
|
}
|
|
104
84
|
|
|
105
|
-
/** Create a
|
|
106
|
-
function
|
|
85
|
+
/** Create a canonical guardian request for testing. */
|
|
86
|
+
function createTestCanonicalRequest(overrides: {
|
|
107
87
|
conversationId: string;
|
|
108
88
|
requestId: string;
|
|
89
|
+
kind?: string;
|
|
109
90
|
toolName?: string;
|
|
110
91
|
guardianExternalUserId?: string;
|
|
111
|
-
|
|
92
|
+
questionText?: string;
|
|
93
|
+
expiresAt?: string;
|
|
112
94
|
}) {
|
|
113
95
|
ensureConversation(overrides.conversationId);
|
|
114
|
-
return
|
|
115
|
-
|
|
116
|
-
|
|
96
|
+
return createCanonicalGuardianRequest({
|
|
97
|
+
id: overrides.requestId,
|
|
98
|
+
kind: overrides.kind ?? 'tool_approval',
|
|
99
|
+
sourceType: 'desktop',
|
|
100
|
+
sourceChannel: 'vellum',
|
|
117
101
|
conversationId: overrides.conversationId,
|
|
118
|
-
|
|
119
|
-
requesterExternalUserId: 'user-1',
|
|
120
|
-
requesterChatId: 'chat-1',
|
|
121
|
-
guardianExternalUserId: overrides.guardianExternalUserId ?? 'guardian-1',
|
|
122
|
-
guardianChatId: 'gchat-1',
|
|
102
|
+
guardianExternalUserId: overrides.guardianExternalUserId,
|
|
123
103
|
toolName: overrides.toolName ?? 'bash',
|
|
124
|
-
|
|
125
|
-
|
|
104
|
+
questionText: overrides.questionText,
|
|
105
|
+
requestCode: generateCanonicalRequestCode(),
|
|
106
|
+
status: 'pending',
|
|
107
|
+
expiresAt: overrides.expiresAt ?? new Date(Date.now() + 60_000).toISOString(),
|
|
126
108
|
});
|
|
127
109
|
}
|
|
128
110
|
|
|
129
|
-
//
|
|
111
|
+
// -- IPC helper ---------------------------------------------------------------
|
|
130
112
|
|
|
131
113
|
/** Minimal stub for IPC socket and context to capture sent messages. */
|
|
132
114
|
function createIpcStub() {
|
|
133
115
|
const sent: Array<Record<string, unknown>> = [];
|
|
134
|
-
const socket = {} as unknown; // opaque
|
|
116
|
+
const socket = {} as unknown; // opaque -- the handler just passes it through
|
|
135
117
|
const ctx = {
|
|
136
118
|
send: (_socket: unknown, msg: Record<string, unknown>) => {
|
|
137
119
|
sent.push(msg);
|
|
@@ -140,7 +122,7 @@ function createIpcStub() {
|
|
|
140
122
|
return { socket, ctx, sent };
|
|
141
123
|
}
|
|
142
124
|
|
|
143
|
-
//
|
|
125
|
+
// -- Cleanup ------------------------------------------------------------------
|
|
144
126
|
|
|
145
127
|
afterAll(() => {
|
|
146
128
|
resetDb();
|
|
@@ -191,7 +173,9 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
191
173
|
expect(body.error.message).toContain('Invalid action');
|
|
192
174
|
});
|
|
193
175
|
|
|
194
|
-
test('returns 404 when no
|
|
176
|
+
test('returns 404 when no canonical request exists (not_found from canonical primitive)', async () => {
|
|
177
|
+
mockApplyCanonicalGuardianDecision.mockResolvedValueOnce({ applied: false, reason: 'not_found' });
|
|
178
|
+
|
|
195
179
|
const req = new Request('http://localhost/v1/guardian-actions/decision', {
|
|
196
180
|
method: 'POST',
|
|
197
181
|
body: JSON.stringify({ requestId: 'nonexistent', action: 'approve_once' }),
|
|
@@ -200,9 +184,9 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
200
184
|
expect(res.status).toBe(404);
|
|
201
185
|
});
|
|
202
186
|
|
|
203
|
-
test('applies decision via
|
|
204
|
-
|
|
205
|
-
|
|
187
|
+
test('applies decision via applyCanonicalGuardianDecision for tool approval', async () => {
|
|
188
|
+
createTestCanonicalRequest({ conversationId: 'conv-1', requestId: 'req-gd-1' });
|
|
189
|
+
mockApplyCanonicalGuardianDecision.mockResolvedValueOnce({ applied: true, requestId: 'req-gd-1', grantMinted: false });
|
|
206
190
|
|
|
207
191
|
const req = new Request('http://localhost/v1/guardian-actions/decision', {
|
|
208
192
|
method: 'POST',
|
|
@@ -213,26 +197,26 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
213
197
|
const body = await res.json();
|
|
214
198
|
expect(body.applied).toBe(true);
|
|
215
199
|
expect(body.requestId).toBe('req-gd-1');
|
|
216
|
-
expect(
|
|
200
|
+
expect(mockApplyCanonicalGuardianDecision).toHaveBeenCalledTimes(1);
|
|
217
201
|
});
|
|
218
202
|
|
|
219
|
-
test('rejects decision when conversationId does not match
|
|
220
|
-
|
|
203
|
+
test('rejects decision when conversationId does not match canonical request', async () => {
|
|
204
|
+
createTestCanonicalRequest({ conversationId: 'conv-1', requestId: 'req-scope-1' });
|
|
221
205
|
|
|
222
206
|
const req = new Request('http://localhost/v1/guardian-actions/decision', {
|
|
223
207
|
method: 'POST',
|
|
224
208
|
body: JSON.stringify({ requestId: 'req-scope-1', action: 'approve_once', conversationId: 'conv-wrong' }),
|
|
225
209
|
});
|
|
226
210
|
const res = await handleGuardianActionDecision(req);
|
|
227
|
-
expect(res.status).toBe(
|
|
211
|
+
expect(res.status).toBe(404);
|
|
228
212
|
const body = await res.json();
|
|
229
|
-
expect(body.error.message).toContain('
|
|
230
|
-
expect(
|
|
213
|
+
expect(body.error.message).toContain('No pending guardian action');
|
|
214
|
+
expect(mockApplyCanonicalGuardianDecision).not.toHaveBeenCalled();
|
|
231
215
|
});
|
|
232
216
|
|
|
233
|
-
test('allows decision when conversationId matches
|
|
234
|
-
|
|
235
|
-
|
|
217
|
+
test('allows decision when conversationId matches canonical request', async () => {
|
|
218
|
+
createTestCanonicalRequest({ conversationId: 'conv-match', requestId: 'req-scope-2' });
|
|
219
|
+
mockApplyCanonicalGuardianDecision.mockResolvedValueOnce({ applied: true, requestId: 'req-scope-2', grantMinted: false });
|
|
236
220
|
|
|
237
221
|
const req = new Request('http://localhost/v1/guardian-actions/decision', {
|
|
238
222
|
method: 'POST',
|
|
@@ -245,8 +229,8 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
245
229
|
});
|
|
246
230
|
|
|
247
231
|
test('allows decision when no conversationId is provided (backward compat)', async () => {
|
|
248
|
-
|
|
249
|
-
|
|
232
|
+
createTestCanonicalRequest({ conversationId: 'conv-any', requestId: 'req-scope-3' });
|
|
233
|
+
mockApplyCanonicalGuardianDecision.mockResolvedValueOnce({ applied: true, requestId: 'req-scope-3', grantMinted: false });
|
|
250
234
|
|
|
251
235
|
const req = new Request('http://localhost/v1/guardian-actions/decision', {
|
|
252
236
|
method: 'POST',
|
|
@@ -256,13 +240,15 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
256
240
|
expect(res.status).toBe(200);
|
|
257
241
|
});
|
|
258
242
|
|
|
259
|
-
test('
|
|
260
|
-
|
|
243
|
+
test('applies decision for access_request kind through canonical primitive', async () => {
|
|
244
|
+
createTestCanonicalRequest({
|
|
261
245
|
conversationId: 'conv-access',
|
|
262
246
|
requestId: 'req-access-1',
|
|
247
|
+
kind: 'access_request',
|
|
263
248
|
toolName: 'ingress_access_request',
|
|
264
249
|
guardianExternalUserId: 'guardian-42',
|
|
265
250
|
});
|
|
251
|
+
mockApplyCanonicalGuardianDecision.mockResolvedValueOnce({ applied: true, requestId: 'req-access-1', grantMinted: false });
|
|
266
252
|
|
|
267
253
|
const req = new Request('http://localhost/v1/guardian-actions/decision', {
|
|
268
254
|
method: 'POST',
|
|
@@ -272,53 +258,13 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
272
258
|
expect(res.status).toBe(200);
|
|
273
259
|
const body = await res.json();
|
|
274
260
|
expect(body.applied).toBe(true);
|
|
275
|
-
|
|
276
|
-
expect(
|
|
277
|
-
// Should NOT call applyGuardianDecision for access requests
|
|
278
|
-
expect(mockApplyGuardianDecision).not.toHaveBeenCalled();
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
test('maps reject to deny for access request decisions', async () => {
|
|
282
|
-
createTestApproval({
|
|
283
|
-
conversationId: 'conv-access-deny',
|
|
284
|
-
requestId: 'req-access-deny',
|
|
285
|
-
toolName: 'ingress_access_request',
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
const req = new Request('http://localhost/v1/guardian-actions/decision', {
|
|
289
|
-
method: 'POST',
|
|
290
|
-
body: JSON.stringify({ requestId: 'req-access-deny', action: 'reject' }),
|
|
291
|
-
});
|
|
292
|
-
await handleGuardianActionDecision(req);
|
|
293
|
-
const call = mockHandleAccessRequestDecision.mock.calls[0]!;
|
|
294
|
-
expect(call[1]).toBe('deny');
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
test('returns stale when access request decision is stale', async () => {
|
|
298
|
-
createTestApproval({
|
|
299
|
-
conversationId: 'conv-access-stale',
|
|
300
|
-
requestId: 'req-access-stale',
|
|
301
|
-
toolName: 'ingress_access_request',
|
|
302
|
-
});
|
|
303
|
-
mockHandleAccessRequestDecision.mockReturnValueOnce({
|
|
304
|
-
handled: false,
|
|
305
|
-
type: 'stale' as const,
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
const req = new Request('http://localhost/v1/guardian-actions/decision', {
|
|
309
|
-
method: 'POST',
|
|
310
|
-
body: JSON.stringify({ requestId: 'req-access-stale', action: 'approve_once' }),
|
|
311
|
-
});
|
|
312
|
-
const res = await handleGuardianActionDecision(req);
|
|
313
|
-
const body = await res.json();
|
|
314
|
-
expect(body.applied).toBe(false);
|
|
315
|
-
expect(body.reason).toBe('stale');
|
|
316
|
-
expect(body.requestId).toBe('req-access-stale');
|
|
261
|
+
// All decisions go through the canonical primitive
|
|
262
|
+
expect(mockApplyCanonicalGuardianDecision).toHaveBeenCalledTimes(1);
|
|
317
263
|
});
|
|
318
264
|
|
|
319
|
-
test('
|
|
320
|
-
|
|
321
|
-
|
|
265
|
+
test('returns stale reason from canonical decision primitive', async () => {
|
|
266
|
+
createTestCanonicalRequest({ conversationId: 'conv-stale', requestId: 'req-stale-1' });
|
|
267
|
+
mockApplyCanonicalGuardianDecision.mockResolvedValueOnce({ applied: false, reason: 'already_resolved' });
|
|
322
268
|
|
|
323
269
|
const req = new Request('http://localhost/v1/guardian-actions/decision', {
|
|
324
270
|
method: 'POST',
|
|
@@ -327,60 +273,25 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
327
273
|
const res = await handleGuardianActionDecision(req);
|
|
328
274
|
const body = await res.json();
|
|
329
275
|
expect(body.applied).toBe(false);
|
|
330
|
-
expect(body.reason).toBe('
|
|
331
|
-
// requestId should fall back to the original
|
|
276
|
+
expect(body.reason).toBe('already_resolved');
|
|
277
|
+
// requestId should fall back to the original request ID
|
|
332
278
|
expect(body.requestId).toBe('req-stale-1');
|
|
333
279
|
});
|
|
334
280
|
|
|
335
|
-
test('
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
session: fakeSession,
|
|
339
|
-
conversationId: 'conv-pi',
|
|
340
|
-
kind: 'confirmation',
|
|
341
|
-
});
|
|
342
|
-
mockHandleChannelDecision.mockReturnValueOnce({ applied: true, requestId: 'req-pi-1' });
|
|
343
|
-
|
|
344
|
-
const req = new Request('http://localhost/v1/guardian-actions/decision', {
|
|
345
|
-
method: 'POST',
|
|
346
|
-
body: JSON.stringify({ requestId: 'req-pi-1', action: 'approve_always' }),
|
|
347
|
-
});
|
|
348
|
-
const res = await handleGuardianActionDecision(req);
|
|
349
|
-
const body = await res.json();
|
|
350
|
-
expect(body.applied).toBe(true);
|
|
351
|
-
expect(mockHandleChannelDecision).toHaveBeenCalledTimes(1);
|
|
352
|
-
expect(mockApplyGuardianDecision).not.toHaveBeenCalled();
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
test('rejects interaction decision when conversationId mismatches', async () => {
|
|
356
|
-
const fakeSession = {} as any;
|
|
357
|
-
pendingInteractions.register('req-pi-scope', {
|
|
358
|
-
session: fakeSession,
|
|
359
|
-
conversationId: 'conv-pi-correct',
|
|
360
|
-
kind: 'confirmation',
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
const req = new Request('http://localhost/v1/guardian-actions/decision', {
|
|
364
|
-
method: 'POST',
|
|
365
|
-
body: JSON.stringify({ requestId: 'req-pi-scope', action: 'approve_once', conversationId: 'conv-pi-wrong' }),
|
|
366
|
-
});
|
|
367
|
-
const res = await handleGuardianActionDecision(req);
|
|
368
|
-
expect(res.status).toBe(400);
|
|
369
|
-
expect(mockHandleChannelDecision).not.toHaveBeenCalled();
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
test('passes actorExternalUserId as undefined (unauthenticated endpoint)', async () => {
|
|
373
|
-
createTestApproval({ conversationId: 'conv-actor', requestId: 'req-actor-1' });
|
|
374
|
-
mockApplyGuardianDecision.mockReturnValueOnce({ applied: true, requestId: 'req-actor-1' });
|
|
281
|
+
test('passes actorContext with undefined externalUserId (unauthenticated endpoint)', async () => {
|
|
282
|
+
createTestCanonicalRequest({ conversationId: 'conv-actor', requestId: 'req-actor-1' });
|
|
283
|
+
mockApplyCanonicalGuardianDecision.mockResolvedValueOnce({ applied: true, requestId: 'req-actor-1', grantMinted: false });
|
|
375
284
|
|
|
376
285
|
const req = new Request('http://localhost/v1/guardian-actions/decision', {
|
|
377
286
|
method: 'POST',
|
|
378
287
|
body: JSON.stringify({ requestId: 'req-actor-1', action: 'approve_once' }),
|
|
379
288
|
});
|
|
380
289
|
await handleGuardianActionDecision(req);
|
|
381
|
-
const call =
|
|
382
|
-
|
|
383
|
-
expect(
|
|
290
|
+
const call = mockApplyCanonicalGuardianDecision.mock.calls[0]![0] as Record<string, unknown>;
|
|
291
|
+
const actorContext = call.actorContext as Record<string, unknown>;
|
|
292
|
+
expect(actorContext.externalUserId).toBeUndefined();
|
|
293
|
+
expect(actorContext.channel).toBe('vellum');
|
|
294
|
+
expect(actorContext.isTrusted).toBe(true);
|
|
384
295
|
});
|
|
385
296
|
});
|
|
386
297
|
|
|
@@ -397,8 +308,12 @@ describe('HTTP handleGuardianActionsPending', () => {
|
|
|
397
308
|
expect(res.status).toBe(400);
|
|
398
309
|
});
|
|
399
310
|
|
|
400
|
-
test('returns prompts for a conversation with pending
|
|
401
|
-
|
|
311
|
+
test('returns prompts for a conversation with pending canonical requests', () => {
|
|
312
|
+
createTestCanonicalRequest({
|
|
313
|
+
conversationId: 'conv-list',
|
|
314
|
+
requestId: 'req-list-1',
|
|
315
|
+
questionText: 'Run bash: ls',
|
|
316
|
+
});
|
|
402
317
|
|
|
403
318
|
const req = new Request('http://localhost/v1/guardian-actions/pending?conversationId=conv-list');
|
|
404
319
|
const res = handleGuardianActionsPending(req);
|
|
@@ -411,7 +326,7 @@ describe('HTTP handleGuardianActionsPending', () => {
|
|
|
411
326
|
expect(prompts[0].questionText).toBe('Run bash: ls');
|
|
412
327
|
});
|
|
413
328
|
|
|
414
|
-
test('returns empty prompts for a conversation with no pending
|
|
329
|
+
test('returns empty prompts for a conversation with no pending requests', () => {
|
|
415
330
|
const prompts = listGuardianDecisionPrompts({ conversationId: 'conv-empty' });
|
|
416
331
|
expect(prompts).toHaveLength(0);
|
|
417
332
|
});
|
|
@@ -424,100 +339,108 @@ describe('HTTP handleGuardianActionsPending', () => {
|
|
|
424
339
|
describe('listGuardianDecisionPrompts', () => {
|
|
425
340
|
beforeEach(resetTables);
|
|
426
341
|
|
|
427
|
-
test('excludes expired
|
|
342
|
+
test('excludes expired canonical requests', () => {
|
|
428
343
|
ensureConversation('conv-expired');
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
344
|
+
createCanonicalGuardianRequest({
|
|
345
|
+
id: 'req-expired',
|
|
346
|
+
kind: 'tool_approval',
|
|
347
|
+
sourceType: 'desktop',
|
|
348
|
+
sourceChannel: 'vellum',
|
|
433
349
|
conversationId: 'conv-expired',
|
|
434
|
-
channel: 'vellum',
|
|
435
|
-
requesterExternalUserId: 'user-1',
|
|
436
|
-
requesterChatId: 'chat-1',
|
|
437
|
-
guardianExternalUserId: 'guardian-1',
|
|
438
|
-
guardianChatId: 'gchat-1',
|
|
439
350
|
toolName: 'bash',
|
|
440
|
-
|
|
351
|
+
requestCode: generateCanonicalRequestCode(),
|
|
352
|
+
status: 'pending',
|
|
353
|
+
expiresAt: new Date(Date.now() - 1000).toISOString(), // already expired
|
|
441
354
|
});
|
|
442
355
|
|
|
443
356
|
const prompts = listGuardianDecisionPrompts({ conversationId: 'conv-expired' });
|
|
444
357
|
expect(prompts).toHaveLength(0);
|
|
445
358
|
});
|
|
446
359
|
|
|
447
|
-
test('
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
conversationId: 'conv-no-reqid',
|
|
453
|
-
channel: 'vellum',
|
|
454
|
-
requesterExternalUserId: 'user-1',
|
|
455
|
-
requesterChatId: 'chat-1',
|
|
456
|
-
guardianExternalUserId: 'guardian-1',
|
|
457
|
-
guardianChatId: 'gchat-1',
|
|
458
|
-
toolName: 'bash',
|
|
459
|
-
expiresAt: Date.now() + 60_000,
|
|
360
|
+
test('includes pending canonical requests with toolName', () => {
|
|
361
|
+
createTestCanonicalRequest({
|
|
362
|
+
conversationId: 'conv-tool',
|
|
363
|
+
requestId: 'req-tool-prompt',
|
|
364
|
+
toolName: 'read_file',
|
|
460
365
|
});
|
|
461
366
|
|
|
462
|
-
const prompts = listGuardianDecisionPrompts({ conversationId: 'conv-
|
|
463
|
-
expect(prompts).toHaveLength(
|
|
367
|
+
const prompts = listGuardianDecisionPrompts({ conversationId: 'conv-tool' });
|
|
368
|
+
expect(prompts).toHaveLength(1);
|
|
369
|
+
expect(prompts[0].toolName).toBe('read_file');
|
|
370
|
+
expect(prompts[0].requestId).toBe('req-tool-prompt');
|
|
464
371
|
});
|
|
465
372
|
|
|
466
|
-
test('
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
kind: 'confirmation',
|
|
472
|
-
confirmationDetails: {
|
|
473
|
-
toolName: 'read_file',
|
|
474
|
-
input: { path: '/etc/passwd' },
|
|
475
|
-
riskLevel: 'high',
|
|
476
|
-
allowlistOptions: [],
|
|
477
|
-
scopeOptions: [],
|
|
478
|
-
persistentDecisionsAllowed: true,
|
|
479
|
-
},
|
|
373
|
+
test('generates questionText from toolName when questionText is not set', () => {
|
|
374
|
+
createTestCanonicalRequest({
|
|
375
|
+
conversationId: 'conv-gen-qt',
|
|
376
|
+
requestId: 'req-gen-qt',
|
|
377
|
+
toolName: 'bash',
|
|
480
378
|
});
|
|
481
379
|
|
|
482
|
-
const prompts = listGuardianDecisionPrompts({ conversationId: 'conv-
|
|
380
|
+
const prompts = listGuardianDecisionPrompts({ conversationId: 'conv-gen-qt' });
|
|
483
381
|
expect(prompts).toHaveLength(1);
|
|
484
|
-
expect(prompts[0].
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
session: fakeSession,
|
|
494
|
-
conversationId: 'conv-dedup',
|
|
495
|
-
kind: 'confirmation',
|
|
496
|
-
confirmationDetails: {
|
|
497
|
-
toolName: 'bash',
|
|
498
|
-
input: {},
|
|
499
|
-
riskLevel: 'medium',
|
|
500
|
-
allowlistOptions: [],
|
|
501
|
-
scopeOptions: [],
|
|
502
|
-
},
|
|
382
|
+
expect(prompts[0].questionText).toBe('Approve tool: bash');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('uses questionText when it is set', () => {
|
|
386
|
+
createTestCanonicalRequest({
|
|
387
|
+
conversationId: 'conv-qt',
|
|
388
|
+
requestId: 'req-qt',
|
|
389
|
+
toolName: 'bash',
|
|
390
|
+
questionText: 'Run bash: ls -la',
|
|
503
391
|
});
|
|
504
392
|
|
|
505
|
-
const prompts = listGuardianDecisionPrompts({ conversationId: 'conv-
|
|
506
|
-
// Should only appear once (from the channel approval)
|
|
393
|
+
const prompts = listGuardianDecisionPrompts({ conversationId: 'conv-qt' });
|
|
507
394
|
expect(prompts).toHaveLength(1);
|
|
508
|
-
expect(prompts[0].
|
|
395
|
+
expect(prompts[0].questionText).toBe('Run bash: ls -la');
|
|
509
396
|
});
|
|
510
397
|
|
|
511
|
-
test('
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
398
|
+
test('returns prompt with correct shape fields', () => {
|
|
399
|
+
createTestCanonicalRequest({
|
|
400
|
+
conversationId: 'conv-shape',
|
|
401
|
+
requestId: 'req-shape',
|
|
402
|
+
toolName: 'bash',
|
|
403
|
+
questionText: 'Test prompt',
|
|
517
404
|
});
|
|
518
405
|
|
|
519
|
-
const prompts = listGuardianDecisionPrompts({ conversationId: 'conv-
|
|
520
|
-
expect(prompts).toHaveLength(
|
|
406
|
+
const prompts = listGuardianDecisionPrompts({ conversationId: 'conv-shape' });
|
|
407
|
+
expect(prompts).toHaveLength(1);
|
|
408
|
+
const prompt = prompts[0];
|
|
409
|
+
expect(prompt.requestId).toBe('req-shape');
|
|
410
|
+
expect(prompt.state).toBe('pending');
|
|
411
|
+
expect(prompt.conversationId).toBe('conv-shape');
|
|
412
|
+
expect(prompt.toolName).toBe('bash');
|
|
413
|
+
expect(prompt.actions).toBeDefined();
|
|
414
|
+
expect(prompt.expiresAt).toBeGreaterThan(Date.now() - 5000);
|
|
415
|
+
expect(prompt.kind).toBe('tool_approval');
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test('includes access_request kind canonical requests', () => {
|
|
419
|
+
createTestCanonicalRequest({
|
|
420
|
+
conversationId: 'conv-ar-prompt',
|
|
421
|
+
requestId: 'req-ar-prompt',
|
|
422
|
+
kind: 'access_request',
|
|
423
|
+
toolName: 'ingress_access_request',
|
|
424
|
+
questionText: 'User wants access',
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const prompts = listGuardianDecisionPrompts({ conversationId: 'conv-ar-prompt' });
|
|
428
|
+
expect(prompts).toHaveLength(1);
|
|
429
|
+
expect(prompts[0].kind).toBe('access_request');
|
|
430
|
+
expect(prompts[0].questionText).toBe('User wants access');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test('only returns requests for the given conversationId', () => {
|
|
434
|
+
createTestCanonicalRequest({ conversationId: 'conv-a', requestId: 'req-a' });
|
|
435
|
+
createTestCanonicalRequest({ conversationId: 'conv-b', requestId: 'req-b' });
|
|
436
|
+
|
|
437
|
+
const promptsA = listGuardianDecisionPrompts({ conversationId: 'conv-a' });
|
|
438
|
+
expect(promptsA).toHaveLength(1);
|
|
439
|
+
expect(promptsA[0].requestId).toBe('req-a');
|
|
440
|
+
|
|
441
|
+
const promptsB = listGuardianDecisionPrompts({ conversationId: 'conv-b' });
|
|
442
|
+
expect(promptsB).toHaveLength(1);
|
|
443
|
+
expect(promptsB[0].requestId).toBe('req-b');
|
|
521
444
|
});
|
|
522
445
|
});
|
|
523
446
|
|
|
@@ -530,9 +453,9 @@ describe('IPC guardian_action_decision', () => {
|
|
|
530
453
|
|
|
531
454
|
const handler = guardianActionsHandlers.guardian_action_decision;
|
|
532
455
|
|
|
533
|
-
test('rejects invalid action', () => {
|
|
456
|
+
test('rejects invalid action', async () => {
|
|
534
457
|
const { socket, ctx, sent } = createIpcStub();
|
|
535
|
-
handler(
|
|
458
|
+
await handler(
|
|
536
459
|
{ type: 'guardian_action_decision', requestId: 'req-ipc-1', action: 'self_destruct' } as any,
|
|
537
460
|
socket as any,
|
|
538
461
|
ctx as any,
|
|
@@ -543,9 +466,11 @@ describe('IPC guardian_action_decision', () => {
|
|
|
543
466
|
expect(sent[0].requestId).toBe('req-ipc-1');
|
|
544
467
|
});
|
|
545
468
|
|
|
546
|
-
test('returns not_found when no
|
|
469
|
+
test('returns not_found when no canonical request exists', async () => {
|
|
470
|
+
mockApplyCanonicalGuardianDecision.mockResolvedValueOnce({ applied: false, reason: 'not_found' });
|
|
471
|
+
|
|
547
472
|
const { socket, ctx, sent } = createIpcStub();
|
|
548
|
-
handler(
|
|
473
|
+
await handler(
|
|
549
474
|
{ type: 'guardian_action_decision', requestId: 'req-ghost', action: 'approve_once' } as any,
|
|
550
475
|
socket as any,
|
|
551
476
|
ctx as any,
|
|
@@ -555,12 +480,12 @@ describe('IPC guardian_action_decision', () => {
|
|
|
555
480
|
expect(sent[0].reason).toBe('not_found');
|
|
556
481
|
});
|
|
557
482
|
|
|
558
|
-
test('applies decision via
|
|
559
|
-
|
|
560
|
-
|
|
483
|
+
test('applies decision via applyCanonicalGuardianDecision for tool approval', async () => {
|
|
484
|
+
createTestCanonicalRequest({ conversationId: 'conv-ipc-1', requestId: 'req-ipc-gd' });
|
|
485
|
+
mockApplyCanonicalGuardianDecision.mockResolvedValueOnce({ applied: true, requestId: 'req-ipc-gd', grantMinted: false });
|
|
561
486
|
|
|
562
487
|
const { socket, ctx, sent } = createIpcStub();
|
|
563
|
-
handler(
|
|
488
|
+
await handler(
|
|
564
489
|
{ type: 'guardian_action_decision', requestId: 'req-ipc-gd', action: 'approve_once' } as any,
|
|
565
490
|
socket as any,
|
|
566
491
|
ctx as any,
|
|
@@ -568,14 +493,14 @@ describe('IPC guardian_action_decision', () => {
|
|
|
568
493
|
expect(sent).toHaveLength(1);
|
|
569
494
|
expect(sent[0].applied).toBe(true);
|
|
570
495
|
expect(sent[0].requestId).toBe('req-ipc-gd');
|
|
571
|
-
expect(
|
|
496
|
+
expect(mockApplyCanonicalGuardianDecision).toHaveBeenCalledTimes(1);
|
|
572
497
|
});
|
|
573
498
|
|
|
574
|
-
test('rejects decision when conversationId does not match
|
|
575
|
-
|
|
499
|
+
test('rejects decision when conversationId does not match canonical request', async () => {
|
|
500
|
+
createTestCanonicalRequest({ conversationId: 'conv-ipc-correct', requestId: 'req-ipc-scope' });
|
|
576
501
|
|
|
577
502
|
const { socket, ctx, sent } = createIpcStub();
|
|
578
|
-
handler(
|
|
503
|
+
await handler(
|
|
579
504
|
{
|
|
580
505
|
type: 'guardian_action_decision',
|
|
581
506
|
requestId: 'req-ipc-scope',
|
|
@@ -587,17 +512,16 @@ describe('IPC guardian_action_decision', () => {
|
|
|
587
512
|
);
|
|
588
513
|
expect(sent).toHaveLength(1);
|
|
589
514
|
expect(sent[0].applied).toBe(false);
|
|
590
|
-
expect(sent[0].reason).toBe('
|
|
591
|
-
expect(
|
|
592
|
-
expect(mockApplyGuardianDecision).not.toHaveBeenCalled();
|
|
515
|
+
expect(sent[0].reason).toBe('not_found');
|
|
516
|
+
expect(mockApplyCanonicalGuardianDecision).not.toHaveBeenCalled();
|
|
593
517
|
});
|
|
594
518
|
|
|
595
|
-
test('allows decision when conversationId matches', () => {
|
|
596
|
-
|
|
597
|
-
|
|
519
|
+
test('allows decision when conversationId matches', async () => {
|
|
520
|
+
createTestCanonicalRequest({ conversationId: 'conv-ipc-match', requestId: 'req-ipc-match' });
|
|
521
|
+
mockApplyCanonicalGuardianDecision.mockResolvedValueOnce({ applied: true, requestId: 'req-ipc-match', grantMinted: false });
|
|
598
522
|
|
|
599
523
|
const { socket, ctx, sent } = createIpcStub();
|
|
600
|
-
handler(
|
|
524
|
+
await handler(
|
|
601
525
|
{
|
|
602
526
|
type: 'guardian_action_decision',
|
|
603
527
|
requestId: 'req-ipc-match',
|
|
@@ -611,125 +535,57 @@ describe('IPC guardian_action_decision', () => {
|
|
|
611
535
|
expect(sent[0].applied).toBe(true);
|
|
612
536
|
});
|
|
613
537
|
|
|
614
|
-
test('
|
|
615
|
-
|
|
538
|
+
test('applies decision for access_request kind through canonical primitive', async () => {
|
|
539
|
+
createTestCanonicalRequest({
|
|
616
540
|
conversationId: 'conv-ipc-access',
|
|
617
541
|
requestId: 'req-ipc-access',
|
|
542
|
+
kind: 'access_request',
|
|
618
543
|
toolName: 'ingress_access_request',
|
|
619
544
|
guardianExternalUserId: 'guardian-99',
|
|
620
545
|
});
|
|
546
|
+
mockApplyCanonicalGuardianDecision.mockResolvedValueOnce({ applied: true, requestId: 'req-ipc-access', grantMinted: false });
|
|
621
547
|
|
|
622
548
|
const { socket, ctx, sent } = createIpcStub();
|
|
623
|
-
handler(
|
|
549
|
+
await handler(
|
|
624
550
|
{ type: 'guardian_action_decision', requestId: 'req-ipc-access', action: 'approve_once' } as any,
|
|
625
551
|
socket as any,
|
|
626
552
|
ctx as any,
|
|
627
553
|
);
|
|
628
554
|
expect(sent).toHaveLength(1);
|
|
629
555
|
expect(sent[0].applied).toBe(true);
|
|
630
|
-
expect(
|
|
631
|
-
// Actor is 'desktop' because this endpoint is unauthenticated —
|
|
632
|
-
// we cannot verify the caller is the assigned guardian.
|
|
633
|
-
const call = mockHandleAccessRequestDecision.mock.calls[0]!;
|
|
634
|
-
expect(call[2]).toBe('desktop');
|
|
556
|
+
expect(mockApplyCanonicalGuardianDecision).toHaveBeenCalledTimes(1);
|
|
635
557
|
});
|
|
636
558
|
|
|
637
|
-
test('returns
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
requestId: 'req-ipc-stale-ar',
|
|
641
|
-
toolName: 'ingress_access_request',
|
|
642
|
-
});
|
|
643
|
-
mockHandleAccessRequestDecision.mockReturnValueOnce({
|
|
644
|
-
handled: false,
|
|
645
|
-
type: 'stale' as const,
|
|
646
|
-
});
|
|
559
|
+
test('returns already_resolved for stale canonical request', async () => {
|
|
560
|
+
createTestCanonicalRequest({ conversationId: 'conv-ipc-stale', requestId: 'req-ipc-stale' });
|
|
561
|
+
mockApplyCanonicalGuardianDecision.mockResolvedValueOnce({ applied: false, reason: 'already_resolved' });
|
|
647
562
|
|
|
648
563
|
const { socket, ctx, sent } = createIpcStub();
|
|
649
|
-
handler(
|
|
650
|
-
{ type: 'guardian_action_decision', requestId: 'req-ipc-stale-ar', action: 'approve_once' } as any,
|
|
651
|
-
socket as any,
|
|
652
|
-
ctx as any,
|
|
653
|
-
);
|
|
654
|
-
expect(sent).toHaveLength(1);
|
|
655
|
-
expect(sent[0].applied).toBe(false);
|
|
656
|
-
expect(sent[0].reason).toBe('stale');
|
|
657
|
-
expect(sent[0].requestId).toBe('req-ipc-stale-ar');
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
test('preserves requestId when applyGuardianDecision returns without one', () => {
|
|
661
|
-
createTestApproval({ conversationId: 'conv-ipc-stale', requestId: 'req-ipc-stale' });
|
|
662
|
-
mockApplyGuardianDecision.mockReturnValueOnce({ applied: false, reason: 'stale' });
|
|
663
|
-
|
|
664
|
-
const { socket, ctx, sent } = createIpcStub();
|
|
665
|
-
handler(
|
|
564
|
+
await handler(
|
|
666
565
|
{ type: 'guardian_action_decision', requestId: 'req-ipc-stale', action: 'approve_once' } as any,
|
|
667
566
|
socket as any,
|
|
668
567
|
ctx as any,
|
|
669
568
|
);
|
|
670
569
|
expect(sent).toHaveLength(1);
|
|
671
570
|
expect(sent[0].requestId).toBe('req-ipc-stale');
|
|
672
|
-
expect(sent[0].reason).toBe('
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
test('falls back to pending interactions', () => {
|
|
676
|
-
const fakeSession = {} as any;
|
|
677
|
-
pendingInteractions.register('req-ipc-pi', {
|
|
678
|
-
session: fakeSession,
|
|
679
|
-
conversationId: 'conv-ipc-pi',
|
|
680
|
-
kind: 'confirmation',
|
|
681
|
-
});
|
|
682
|
-
mockHandleChannelDecision.mockReturnValueOnce({ applied: true, requestId: 'req-ipc-pi' });
|
|
683
|
-
|
|
684
|
-
const { socket, ctx, sent } = createIpcStub();
|
|
685
|
-
handler(
|
|
686
|
-
{ type: 'guardian_action_decision', requestId: 'req-ipc-pi', action: 'approve_always' } as any,
|
|
687
|
-
socket as any,
|
|
688
|
-
ctx as any,
|
|
689
|
-
);
|
|
690
|
-
expect(sent).toHaveLength(1);
|
|
691
|
-
expect(sent[0].applied).toBe(true);
|
|
692
|
-
expect(mockHandleChannelDecision).toHaveBeenCalledTimes(1);
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
test('rejects interaction fallback when conversationId mismatches', () => {
|
|
696
|
-
const fakeSession = {} as any;
|
|
697
|
-
pendingInteractions.register('req-ipc-pi-scope', {
|
|
698
|
-
session: fakeSession,
|
|
699
|
-
conversationId: 'conv-ipc-pi-right',
|
|
700
|
-
kind: 'confirmation',
|
|
701
|
-
});
|
|
702
|
-
|
|
703
|
-
const { socket, ctx, sent } = createIpcStub();
|
|
704
|
-
handler(
|
|
705
|
-
{
|
|
706
|
-
type: 'guardian_action_decision',
|
|
707
|
-
requestId: 'req-ipc-pi-scope',
|
|
708
|
-
action: 'approve_once',
|
|
709
|
-
conversationId: 'conv-ipc-pi-wrong',
|
|
710
|
-
} as any,
|
|
711
|
-
socket as any,
|
|
712
|
-
ctx as any,
|
|
713
|
-
);
|
|
714
|
-
expect(sent).toHaveLength(1);
|
|
715
|
-
expect(sent[0].applied).toBe(false);
|
|
716
|
-
expect(sent[0].reason).toBe('conversation_mismatch');
|
|
717
|
-
expect(mockHandleChannelDecision).not.toHaveBeenCalled();
|
|
571
|
+
expect(sent[0].reason).toBe('already_resolved');
|
|
718
572
|
});
|
|
719
573
|
|
|
720
|
-
test('passes
|
|
721
|
-
|
|
722
|
-
|
|
574
|
+
test('passes actorContext with undefined externalUserId (unauthenticated endpoint)', async () => {
|
|
575
|
+
createTestCanonicalRequest({ conversationId: 'conv-ipc-actor', requestId: 'req-ipc-actor' });
|
|
576
|
+
mockApplyCanonicalGuardianDecision.mockResolvedValueOnce({ applied: true, requestId: 'req-ipc-actor', grantMinted: false });
|
|
723
577
|
|
|
724
578
|
const { socket, ctx } = createIpcStub();
|
|
725
|
-
handler(
|
|
579
|
+
await handler(
|
|
726
580
|
{ type: 'guardian_action_decision', requestId: 'req-ipc-actor', action: 'approve_once' } as any,
|
|
727
581
|
socket as any,
|
|
728
582
|
ctx as any,
|
|
729
583
|
);
|
|
730
|
-
const call =
|
|
731
|
-
|
|
732
|
-
expect(
|
|
584
|
+
const call = mockApplyCanonicalGuardianDecision.mock.calls[0]![0] as Record<string, unknown>;
|
|
585
|
+
const actorContext = call.actorContext as Record<string, unknown>;
|
|
586
|
+
expect(actorContext.externalUserId).toBeUndefined();
|
|
587
|
+
expect(actorContext.channel).toBe('vellum');
|
|
588
|
+
expect(actorContext.isTrusted).toBe(true);
|
|
733
589
|
});
|
|
734
590
|
});
|
|
735
591
|
|
|
@@ -743,7 +599,11 @@ describe('IPC guardian_actions_pending_request', () => {
|
|
|
743
599
|
const handler = guardianActionsHandlers.guardian_actions_pending_request;
|
|
744
600
|
|
|
745
601
|
test('returns prompts for a conversation', () => {
|
|
746
|
-
|
|
602
|
+
createTestCanonicalRequest({
|
|
603
|
+
conversationId: 'conv-ipc-list',
|
|
604
|
+
requestId: 'req-ipc-list',
|
|
605
|
+
questionText: 'Run bash: pwd',
|
|
606
|
+
});
|
|
747
607
|
|
|
748
608
|
const { socket, ctx, sent } = createIpcStub();
|
|
749
609
|
handler(
|
|
@@ -760,7 +620,7 @@ describe('IPC guardian_actions_pending_request', () => {
|
|
|
760
620
|
expect(prompts[0].questionText).toBe('Run bash: pwd');
|
|
761
621
|
});
|
|
762
622
|
|
|
763
|
-
test('returns empty prompts for conversation with no pending
|
|
623
|
+
test('returns empty prompts for conversation with no pending requests', () => {
|
|
764
624
|
const { socket, ctx, sent } = createIpcStub();
|
|
765
625
|
handler(
|
|
766
626
|
{ type: 'guardian_actions_pending_request', conversationId: 'conv-empty-ipc' } as any,
|