@vellumai/assistant 0.4.2 → 0.4.4
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/.env.example +3 -0
- package/ARCHITECTURE.md +124 -10
- package/README.md +43 -35
- package/docs/trusted-contact-access.md +20 -0
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +1 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
- package/src/__tests__/access-request-decision.test.ts +0 -1
- package/src/__tests__/actor-token-service.test.ts +1099 -0
- package/src/__tests__/agent-loop.test.ts +51 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
- package/src/__tests__/assistant-id-boundary-guard.test.ts +415 -0
- package/src/__tests__/call-controller.test.ts +49 -0
- package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
- package/src/__tests__/call-pointer-messages.test.ts +93 -3
- package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
- package/src/__tests__/call-routes-http.test.ts +0 -25
- package/src/__tests__/callback-handoff-copy.test.ts +186 -0
- package/src/__tests__/channel-approval-routes.test.ts +133 -12
- package/src/__tests__/channel-guardian.test.ts +0 -86
- package/src/__tests__/channel-readiness-service.test.ts +10 -16
- package/src/__tests__/checker.test.ts +33 -12
- package/src/__tests__/config-schema.test.ts +6 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
- package/src/__tests__/conversation-routes.test.ts +12 -3
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/daemon-server-session-init.test.ts +4 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +39 -13
- package/src/__tests__/guardian-dispatch.test.ts +8 -0
- package/src/__tests__/guardian-outbound-http.test.ts +4 -5
- package/src/__tests__/guardian-question-mode.test.ts +200 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
- package/src/__tests__/guardian-routing-state.test.ts +525 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
- package/src/__tests__/handlers-telegram-config.test.ts +0 -83
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
- package/src/__tests__/headless-browser-navigate.test.ts +2 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
- package/src/__tests__/ingress-routes-http.test.ts +55 -0
- package/src/__tests__/ipc-snapshot.test.ts +18 -51
- package/src/__tests__/non-member-access-request.test.ts +159 -9
- package/src/__tests__/notification-decision-fallback.test.ts +129 -4
- package/src/__tests__/notification-decision-strategy.test.ts +106 -2
- package/src/__tests__/notification-guardian-path.test.ts +3 -0
- package/src/__tests__/recording-intent-handler.test.ts +1 -0
- package/src/__tests__/relay-server.test.ts +1475 -33
- package/src/__tests__/send-endpoint-busy.test.ts +5 -0
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-confirmation-signals.test.ts +523 -0
- package/src/__tests__/session-init.benchmark.test.ts +0 -2
- package/src/__tests__/session-runtime-assembly.test.ts +4 -1
- package/src/__tests__/session-surfaces-task-progress.test.ts +44 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +21 -2
- package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +11 -1
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
- package/src/__tests__/trusted-contact-verification.test.ts +0 -1
- package/src/__tests__/twilio-config.test.ts +2 -13
- package/src/__tests__/twilio-routes.test.ts +4 -3
- package/src/__tests__/update-bulletin.test.ts +0 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-decision-primitive.ts +12 -3
- package/src/approvals/guardian-request-resolvers.ts +169 -11
- package/src/calls/call-constants.ts +29 -0
- package/src/calls/call-controller.ts +11 -3
- package/src/calls/call-domain.ts +33 -11
- package/src/calls/call-pointer-message-composer.ts +154 -0
- package/src/calls/call-pointer-messages.ts +106 -27
- package/src/calls/guardian-dispatch.ts +4 -2
- package/src/calls/relay-server.ts +921 -112
- package/src/calls/twilio-config.ts +4 -11
- package/src/calls/twilio-routes.ts +4 -6
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-session-bridge.ts +4 -3
- package/src/cli/core-commands.ts +7 -4
- package/src/cli.ts +5 -4
- package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +309 -10
- package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
- package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
- package/src/config/bundled-skills/messaging/SKILL.md +61 -12
- package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
- package/src/config/bundled-skills/twitter/SKILL.md +3 -3
- package/src/config/bundled-skills/vercel-token-setup/SKILL.md +215 -0
- package/src/config/calls-schema.ts +36 -0
- package/src/config/env.ts +22 -0
- package/src/config/feature-flag-registry.json +8 -8
- package/src/config/schema.ts +2 -2
- package/src/config/skills.ts +11 -0
- package/src/config/system-prompt.ts +11 -1
- package/src/config/templates/SOUL.md +2 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +8 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
- package/src/daemon/call-pointer-generators.ts +59 -0
- package/src/daemon/computer-use-session.ts +2 -5
- package/src/daemon/handlers/apps.ts +76 -20
- package/src/daemon/handlers/config-channels.ts +9 -61
- package/src/daemon/handlers/config-inbox.ts +11 -3
- package/src/daemon/handlers/config-ingress.ts +28 -3
- package/src/daemon/handlers/config-telegram.ts +12 -0
- package/src/daemon/handlers/config.ts +2 -6
- package/src/daemon/handlers/index.ts +2 -1
- package/src/daemon/handlers/pairing.ts +2 -0
- package/src/daemon/handlers/publish.ts +11 -46
- package/src/daemon/handlers/sessions.ts +59 -5
- package/src/daemon/handlers/shared.ts +17 -2
- package/src/daemon/ipc-contract/apps.ts +1 -0
- package/src/daemon/ipc-contract/inbox.ts +4 -0
- package/src/daemon/ipc-contract/integrations.ts +1 -97
- package/src/daemon/ipc-contract/messages.ts +47 -1
- package/src/daemon/ipc-contract/notifications.ts +11 -0
- package/src/daemon/ipc-contract-inventory.json +2 -4
- package/src/daemon/lifecycle.ts +17 -0
- package/src/daemon/server.ts +16 -2
- package/src/daemon/session-agent-loop-handlers.ts +20 -0
- package/src/daemon/session-agent-loop.ts +24 -12
- package/src/daemon/session-lifecycle.ts +1 -1
- package/src/daemon/session-process.ts +11 -1
- package/src/daemon/session-runtime-assembly.ts +6 -1
- package/src/daemon/session-surfaces.ts +32 -3
- package/src/daemon/session.ts +88 -1
- package/src/daemon/tool-side-effects.ts +22 -0
- package/src/home-base/prebuilt/brain-graph.html +1483 -0
- package/src/home-base/prebuilt/index.html +40 -0
- package/src/inbound/platform-callback-registration.ts +157 -0
- package/src/memory/canonical-guardian-store.ts +1 -1
- package/src/memory/conversation-crud.ts +2 -1
- package/src/memory/conversation-title-service.ts +16 -2
- package/src/memory/db-init.ts +8 -0
- package/src/memory/delivery-crud.ts +2 -1
- package/src/memory/guardian-action-store.ts +2 -1
- package/src/memory/guardian-approvals.ts +3 -2
- package/src/memory/ingress-invite-store.ts +12 -2
- package/src/memory/ingress-member-store.ts +4 -3
- package/src/memory/migrations/038-actor-token-records.ts +39 -0
- package/src/memory/migrations/124-voice-invite-display-metadata.ts +14 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema.ts +26 -5
- package/src/messaging/provider-types.ts +24 -0
- package/src/messaging/provider.ts +7 -0
- package/src/messaging/providers/gmail/adapter.ts +127 -0
- package/src/messaging/providers/sms/adapter.ts +40 -37
- package/src/notifications/adapters/macos.ts +45 -2
- package/src/notifications/broadcaster.ts +16 -0
- package/src/notifications/copy-composer.ts +50 -2
- package/src/notifications/decision-engine.ts +22 -9
- package/src/notifications/destination-resolver.ts +16 -2
- package/src/notifications/emit-signal.ts +18 -9
- package/src/notifications/guardian-question-mode.ts +419 -0
- package/src/notifications/signal.ts +14 -3
- package/src/permissions/checker.ts +13 -1
- package/src/permissions/prompter.ts +14 -0
- package/src/providers/anthropic/client.ts +20 -0
- package/src/providers/provider-send-message.ts +15 -3
- package/src/runtime/access-request-helper.ts +82 -4
- package/src/runtime/actor-token-service.ts +234 -0
- package/src/runtime/actor-token-store.ts +236 -0
- package/src/runtime/actor-trust-resolver.ts +2 -2
- package/src/runtime/assistant-scope.ts +10 -0
- package/src/runtime/channel-approvals.ts +5 -3
- package/src/runtime/channel-readiness-service.ts +23 -64
- package/src/runtime/channel-readiness-types.ts +3 -4
- package/src/runtime/channel-retry-sweep.ts +4 -1
- package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
- package/src/runtime/guardian-action-followup-executor.ts +1 -1
- package/src/runtime/guardian-context-resolver.ts +82 -0
- package/src/runtime/guardian-outbound-actions.ts +5 -7
- package/src/runtime/guardian-reply-router.ts +67 -30
- package/src/runtime/guardian-vellum-migration.ts +57 -0
- package/src/runtime/http-server.ts +75 -31
- package/src/runtime/http-types.ts +13 -0
- package/src/runtime/ingress-service.ts +14 -0
- package/src/runtime/invite-redemption-service.ts +10 -1
- package/src/runtime/local-actor-identity.ts +76 -0
- package/src/runtime/middleware/actor-token.ts +271 -0
- package/src/runtime/middleware/twilio-validation.ts +2 -4
- package/src/runtime/routes/approval-routes.ts +82 -7
- package/src/runtime/routes/brain-graph-routes.ts +222 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/channel-readiness-routes.ts +71 -0
- package/src/runtime/routes/channel-route-shared.ts +3 -3
- package/src/runtime/routes/conversation-attention-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +142 -53
- package/src/runtime/routes/events-routes.ts +22 -8
- package/src/runtime/routes/guardian-action-routes.ts +45 -3
- package/src/runtime/routes/guardian-approval-interception.ts +29 -0
- package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
- package/src/runtime/routes/inbound-conversation.ts +4 -3
- package/src/runtime/routes/inbound-message-handler.ts +147 -5
- package/src/runtime/routes/ingress-routes.ts +2 -0
- package/src/runtime/routes/integration-routes.ts +7 -15
- package/src/runtime/routes/pairing-routes.ts +163 -0
- package/src/runtime/routes/twilio-routes.ts +934 -0
- package/src/runtime/tool-grant-request-helper.ts +3 -1
- package/src/security/oauth2.ts +27 -2
- package/src/security/token-manager.ts +46 -10
- package/src/tools/browser/browser-execution.ts +4 -3
- package/src/tools/browser/browser-handoff.ts +10 -18
- package/src/tools/browser/browser-manager.ts +80 -25
- package/src/tools/browser/browser-screencast.ts +35 -119
- package/src/tools/calls/call-start.ts +2 -1
- package/src/tools/permission-checker.ts +15 -4
- package/src/tools/terminal/parser.ts +12 -0
- package/src/tools/tool-approval-handler.ts +244 -19
- package/src/workspace/git-service.ts +19 -0
- package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
- package/src/daemon/handlers/config-twilio.ts +0 -1082
|
@@ -62,7 +62,7 @@ function makeCtx(overrides: Partial<ToolSetupContext> = {}): ToolSetupContext {
|
|
|
62
62
|
sendToClient: mock(() => {}),
|
|
63
63
|
pendingSurfaceActions: new Map(),
|
|
64
64
|
lastSurfaceAction: new Map(),
|
|
65
|
-
surfaceState: new Map<string, { surfaceType: SurfaceType; data: SurfaceData }>(),
|
|
65
|
+
surfaceState: new Map<string, { surfaceType: SurfaceType; data: SurfaceData; title?: string }>(),
|
|
66
66
|
surfaceUndoStacks: new Map(),
|
|
67
67
|
currentTurnSurfaces: [],
|
|
68
68
|
isProcessing: () => false,
|
|
@@ -392,6 +392,85 @@ describe('session-tool-setup app refresh side effects', () => {
|
|
|
392
392
|
});
|
|
393
393
|
});
|
|
394
394
|
|
|
395
|
+
// ── app_create side effects ─────────────────────────────────────────
|
|
396
|
+
|
|
397
|
+
describe('app_create side effects', () => {
|
|
398
|
+
test('broadcasts app_files_changed after app_create', async () => {
|
|
399
|
+
const ctx = makeCtx();
|
|
400
|
+
const executor = makeFakeExecutor({
|
|
401
|
+
content: JSON.stringify({ id: 'new-app-1', name: 'My App' }),
|
|
402
|
+
isError: false,
|
|
403
|
+
});
|
|
404
|
+
const broadcastSpy = mock(() => {});
|
|
405
|
+
|
|
406
|
+
const toolFn = createToolExecutor(
|
|
407
|
+
executor as unknown as ToolExecutor, noopPrompter, noopSecretPrompter,
|
|
408
|
+
ctx, noopLifecycleHandler, broadcastSpy,
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
await toolFn('app_create', { name: 'My App', html: '<h1>hi</h1>' });
|
|
412
|
+
|
|
413
|
+
expect(broadcastSpy).toHaveBeenCalledTimes(1);
|
|
414
|
+
expect((broadcastSpy.mock.calls as unknown[][])[0][0]).toEqual({
|
|
415
|
+
type: 'app_files_changed',
|
|
416
|
+
appId: 'new-app-1',
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test('skips side effects when app_create result is an error', async () => {
|
|
421
|
+
const ctx = makeCtx();
|
|
422
|
+
const executor = makeFakeExecutor({ content: 'Error', isError: true });
|
|
423
|
+
const broadcastSpy = mock(() => {});
|
|
424
|
+
|
|
425
|
+
const toolFn = createToolExecutor(
|
|
426
|
+
executor as unknown as ToolExecutor, noopPrompter, noopSecretPrompter,
|
|
427
|
+
ctx, noopLifecycleHandler, broadcastSpy,
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
await toolFn('app_create', { name: 'Bad', html: '' });
|
|
431
|
+
|
|
432
|
+
expect(broadcastSpy).not.toHaveBeenCalled();
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// ── app_delete side effects ────────────────────────────────────────
|
|
437
|
+
|
|
438
|
+
describe('app_delete side effects', () => {
|
|
439
|
+
test('broadcasts app_files_changed after app_delete', async () => {
|
|
440
|
+
const ctx = makeCtx();
|
|
441
|
+
const executor = makeFakeExecutor({ content: '{}', isError: false });
|
|
442
|
+
const broadcastSpy = mock(() => {});
|
|
443
|
+
|
|
444
|
+
const toolFn = createToolExecutor(
|
|
445
|
+
executor as unknown as ToolExecutor, noopPrompter, noopSecretPrompter,
|
|
446
|
+
ctx, noopLifecycleHandler, broadcastSpy,
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
await toolFn('app_delete', { app_id: 'del-app-1' });
|
|
450
|
+
|
|
451
|
+
expect(broadcastSpy).toHaveBeenCalledTimes(1);
|
|
452
|
+
expect((broadcastSpy.mock.calls as unknown[][])[0][0]).toEqual({
|
|
453
|
+
type: 'app_files_changed',
|
|
454
|
+
appId: 'del-app-1',
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test('skips side effects when app_delete result is an error', async () => {
|
|
459
|
+
const ctx = makeCtx();
|
|
460
|
+
const executor = makeFakeExecutor({ content: 'Error', isError: true });
|
|
461
|
+
const broadcastSpy = mock(() => {});
|
|
462
|
+
|
|
463
|
+
const toolFn = createToolExecutor(
|
|
464
|
+
executor as unknown as ToolExecutor, noopPrompter, noopSecretPrompter,
|
|
465
|
+
ctx, noopLifecycleHandler, broadcastSpy,
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
await toolFn('app_delete', { app_id: 'del-err' });
|
|
469
|
+
|
|
470
|
+
expect(broadcastSpy).not.toHaveBeenCalled();
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
395
474
|
// ── Name-based hook targeting (skill-origin tools) ──────────────────
|
|
396
475
|
|
|
397
476
|
describe('name-based hooks fire for skill-origin tools', () => {
|
|
@@ -437,7 +516,7 @@ describe('session-tool-setup app refresh side effects', () => {
|
|
|
437
516
|
ctx, noopLifecycleHandler, broadcastSpy,
|
|
438
517
|
);
|
|
439
518
|
|
|
440
|
-
for (const toolName of ['read_file', 'write_file', 'shell', '
|
|
519
|
+
for (const toolName of ['read_file', 'write_file', 'shell', 'app_list']) {
|
|
441
520
|
refreshSpy.mockClear();
|
|
442
521
|
broadcastSpy.mockClear();
|
|
443
522
|
updatePublishedSpy.mockClear();
|
|
@@ -49,7 +49,7 @@ function makeCtx(overrides: Partial<ToolSetupContext> = {}): ToolSetupContext {
|
|
|
49
49
|
sendToClient: mock(() => {}),
|
|
50
50
|
pendingSurfaceActions: new Map(),
|
|
51
51
|
lastSurfaceAction: new Map(),
|
|
52
|
-
surfaceState: new Map<string, { surfaceType: SurfaceType; data: SurfaceData }>(),
|
|
52
|
+
surfaceState: new Map<string, { surfaceType: SurfaceType; data: SurfaceData; title?: string }>(),
|
|
53
53
|
surfaceUndoStacks: new Map(),
|
|
54
54
|
currentTurnSurfaces: [],
|
|
55
55
|
isProcessing: () => false,
|
|
@@ -49,7 +49,7 @@ function makeCtx(overrides: Partial<ToolSetupContext> = {}): ToolSetupContext {
|
|
|
49
49
|
sendToClient: mock(() => {}),
|
|
50
50
|
pendingSurfaceActions: new Map(),
|
|
51
51
|
lastSurfaceAction: new Map(),
|
|
52
|
-
surfaceState: new Map<string, { surfaceType: SurfaceType; data: SurfaceData }>(),
|
|
52
|
+
surfaceState: new Map<string, { surfaceType: SurfaceType; data: SurfaceData; title?: string }>(),
|
|
53
53
|
surfaceUndoStacks: new Map(),
|
|
54
54
|
currentTurnSurfaces: [],
|
|
55
55
|
isProcessing: () => false,
|
|
@@ -30,6 +30,9 @@ let checkResultOverride: { decision: string; reason: string } | undefined;
|
|
|
30
30
|
/** Function override for check() — when set, takes precedence over the static override. */
|
|
31
31
|
let checkFnOverride: ((toolName: string, input: Record<string, unknown>, workingDir: string, policyContext?: PolicyContext) => Promise<{ decision: string; reason: string }>) | undefined;
|
|
32
32
|
|
|
33
|
+
/** Override for generateScopeOptions — when set, returns this value instead of the default. */
|
|
34
|
+
let scopeOptionsOverride: ScopeOption[] | undefined;
|
|
35
|
+
|
|
33
36
|
/** Spy on addRule to capture calls without replacing the real implementation. */
|
|
34
37
|
let addRuleSpy: ReturnType<typeof spyOn> | undefined;
|
|
35
38
|
|
|
@@ -61,7 +64,7 @@ mock.module('../permissions/checker.js', () => ({
|
|
|
61
64
|
return { decision: 'allow', reason: 'allowed' };
|
|
62
65
|
},
|
|
63
66
|
generateAllowlistOptions: () => [{ label: 'exact', description: 'exact', pattern: 'exact' }],
|
|
64
|
-
generateScopeOptions: () => [{ label: '/tmp', scope: '/tmp' }],
|
|
67
|
+
generateScopeOptions: () => scopeOptionsOverride ?? [{ label: '/tmp', scope: '/tmp' }],
|
|
65
68
|
}));
|
|
66
69
|
|
|
67
70
|
mock.module('../memory/tool-usage-store.js', () => ({
|
|
@@ -317,6 +320,7 @@ describe('ToolExecutor contextual rule creation', () => {
|
|
|
317
320
|
getToolOverride = undefined;
|
|
318
321
|
checkResultOverride = undefined;
|
|
319
322
|
checkFnOverride = undefined;
|
|
323
|
+
scopeOptionsOverride = undefined;
|
|
320
324
|
if (addRuleSpy) { addRuleSpy.mockRestore(); addRuleSpy = undefined; }
|
|
321
325
|
});
|
|
322
326
|
|
|
@@ -434,7 +438,7 @@ describe('ToolExecutor contextual rule creation', () => {
|
|
|
434
438
|
expect(spy).not.toHaveBeenCalled();
|
|
435
439
|
});
|
|
436
440
|
|
|
437
|
-
test('always_allow without selectedScope does not create a rule', async () => {
|
|
441
|
+
test('always_allow without selectedScope for scoped tool does not create a rule', async () => {
|
|
438
442
|
checkResultOverride = { decision: 'prompt', reason: 'test prompt' };
|
|
439
443
|
const spy = setupAddRuleSpy();
|
|
440
444
|
|
|
@@ -446,6 +450,21 @@ describe('ToolExecutor contextual rule creation', () => {
|
|
|
446
450
|
expect(spy).not.toHaveBeenCalled();
|
|
447
451
|
});
|
|
448
452
|
|
|
453
|
+
test('always_allow without selectedScope for non-scoped tool creates rule with everywhere scope', async () => {
|
|
454
|
+
checkResultOverride = { decision: 'prompt', reason: 'test prompt' };
|
|
455
|
+
scopeOptionsOverride = [];
|
|
456
|
+
const spy = setupAddRuleSpy();
|
|
457
|
+
|
|
458
|
+
const prompter = makePrompterWithDecision('always_allow', 'some_tool:*', undefined);
|
|
459
|
+
const executor = new ToolExecutor(prompter);
|
|
460
|
+
const result = await executor.execute('some_tool', {}, makeContext());
|
|
461
|
+
|
|
462
|
+
expect(result.isError).toBe(false);
|
|
463
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
464
|
+
const [, , scope] = spy.mock.calls[0];
|
|
465
|
+
expect(scope).toBe('everywhere');
|
|
466
|
+
});
|
|
467
|
+
|
|
449
468
|
test('always_allow_high_risk for core tool sets allowHighRisk without execution target', async () => {
|
|
450
469
|
checkResultOverride = { decision: 'prompt', reason: 'test prompt' };
|
|
451
470
|
const spy = setupAddRuleSpy();
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* 2. tool_grant_request resolver registration and behavior
|
|
6
6
|
* 3. Canonical decision primitive grant minting for tool_grant_request kind
|
|
7
7
|
* 4. End-to-end: deny -> approve -> consume grant flow
|
|
8
|
+
* 5. Inline wait-and-resume for trusted-contact grant-gated tools
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
@@ -115,9 +116,12 @@ import {
|
|
|
115
116
|
import { getDb, initializeDb, resetDb } from '../memory/db.js';
|
|
116
117
|
import { scopedApprovalGrants } from '../memory/schema.js';
|
|
117
118
|
import { computeToolApprovalDigest } from '../security/tool-approval-digest.js';
|
|
118
|
-
import { ToolApprovalHandler } from '../tools/tool-approval-handler.js';
|
|
119
|
+
import { ToolApprovalHandler, waitForInlineGrant } from '../tools/tool-approval-handler.js';
|
|
119
120
|
import type { ToolContext, ToolLifecycleEvent } from '../tools/types.js';
|
|
120
121
|
|
|
122
|
+
/** Short wait config for tests — avoids blocking test suite on the 60s default. */
|
|
123
|
+
const TEST_INLINE_WAIT_CONFIG = { maxWaitMs: 100, intervalMs: 20 };
|
|
124
|
+
|
|
121
125
|
initializeDb();
|
|
122
126
|
|
|
123
127
|
function resetTables(): void {
|
|
@@ -189,7 +193,7 @@ describe('tool_grant_request resolver registration', () => {
|
|
|
189
193
|
// ---------------------------------------------------------------------------
|
|
190
194
|
|
|
191
195
|
describe('ToolApprovalHandler / grant-miss escalation', () => {
|
|
192
|
-
const handler = new ToolApprovalHandler();
|
|
196
|
+
const handler = new ToolApprovalHandler({ inlineGrantWait: TEST_INLINE_WAIT_CONFIG });
|
|
193
197
|
const events: ToolLifecycleEvent[] = [];
|
|
194
198
|
const emitLifecycleEvent = (event: ToolLifecycleEvent) => { events.push(event); };
|
|
195
199
|
|
|
@@ -225,9 +229,11 @@ describe('ToolApprovalHandler / grant-miss escalation', () => {
|
|
|
225
229
|
// Notification signal should have been emitted
|
|
226
230
|
expect(emittedSignals.length).toBe(1);
|
|
227
231
|
expect(emittedSignals[0].sourceEventName).toBe('guardian.question');
|
|
232
|
+
const payload = emittedSignals[0].contextPayload as Record<string, unknown>;
|
|
233
|
+
expect(payload.requestKind).toBe('tool_grant_request');
|
|
228
234
|
});
|
|
229
235
|
|
|
230
|
-
test('non-guardian grant-miss response includes request code', async () => {
|
|
236
|
+
test('non-guardian grant-miss response includes request code after timeout', async () => {
|
|
231
237
|
const toolName = 'bash';
|
|
232
238
|
const input = { command: 'deploy' };
|
|
233
239
|
|
|
@@ -238,9 +244,10 @@ describe('ToolApprovalHandler / grant-miss escalation', () => {
|
|
|
238
244
|
|
|
239
245
|
expect(result.allowed).toBe(false);
|
|
240
246
|
if (result.allowed) return;
|
|
241
|
-
|
|
247
|
+
// After inline wait times out, the message should include the request code
|
|
248
|
+
// and indicate the guardian did not approve in time.
|
|
249
|
+
expect(result.result.content).toContain('guardian approval was not received in time');
|
|
242
250
|
expect(result.result.content).toContain('request code:');
|
|
243
|
-
expect(result.result.content).toContain('Please retry after the guardian approves');
|
|
244
251
|
});
|
|
245
252
|
|
|
246
253
|
test('non-guardian duplicate grant-miss deduplicates the request', async () => {
|
|
@@ -249,7 +256,7 @@ describe('ToolApprovalHandler / grant-miss escalation', () => {
|
|
|
249
256
|
|
|
250
257
|
const context = makeContext({ guardianTrustClass: 'trusted_contact' });
|
|
251
258
|
|
|
252
|
-
// First invocation creates the request
|
|
259
|
+
// First invocation creates the request (and waits, then times out)
|
|
253
260
|
await handler.checkPreExecutionGates(
|
|
254
261
|
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
255
262
|
);
|
|
@@ -263,14 +270,15 @@ describe('ToolApprovalHandler / grant-miss escalation', () => {
|
|
|
263
270
|
// Reset notification tracking
|
|
264
271
|
emittedSignals.length = 0;
|
|
265
272
|
|
|
266
|
-
// Second invocation with same tool+input deduplicates
|
|
273
|
+
// Second invocation with same tool+input deduplicates (reuses the request, waits, times out)
|
|
267
274
|
const result = await handler.checkPreExecutionGates(
|
|
268
275
|
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
269
276
|
);
|
|
270
277
|
|
|
271
278
|
expect(result.allowed).toBe(false);
|
|
272
279
|
if (result.allowed) return;
|
|
273
|
-
|
|
280
|
+
// Both calls get the timeout message since inline wait now runs for deduped requests too
|
|
281
|
+
expect(result.result.content).toContain('guardian approval was not received in time');
|
|
274
282
|
|
|
275
283
|
// Still only one canonical request
|
|
276
284
|
const requests = listCanonicalGuardianRequests({
|
|
@@ -434,7 +442,8 @@ describe('applyCanonicalGuardianDecision / tool_grant_request', () => {
|
|
|
434
442
|
// ---------------------------------------------------------------------------
|
|
435
443
|
|
|
436
444
|
describe('end-to-end: tool grant escalation -> approval -> consume', () => {
|
|
437
|
-
|
|
445
|
+
// Use a wider wait window so the delayed guardian approval arrives in time
|
|
446
|
+
const handler = new ToolApprovalHandler({ inlineGrantWait: { maxWaitMs: 2_000, intervalMs: 20 } });
|
|
438
447
|
const events: ToolLifecycleEvent[] = [];
|
|
439
448
|
const emitLifecycleEvent = (event: ToolLifecycleEvent) => { events.push(event); };
|
|
440
449
|
|
|
@@ -444,54 +453,351 @@ describe('end-to-end: tool grant escalation -> approval -> consume', () => {
|
|
|
444
453
|
emittedSignals.length = 0;
|
|
445
454
|
});
|
|
446
455
|
|
|
447
|
-
test('
|
|
456
|
+
test('inline wait: guardian approves during wait -> tool proceeds inline', async () => {
|
|
457
|
+
const toolName = 'bash';
|
|
458
|
+
const input = { command: 'echo secret' };
|
|
459
|
+
const _inputDigest = computeToolApprovalDigest(toolName, input);
|
|
460
|
+
|
|
461
|
+
const context = makeContext({ guardianTrustClass: 'trusted_contact' });
|
|
462
|
+
|
|
463
|
+
// Schedule guardian approval after 100ms — within the 2s wait window.
|
|
464
|
+
// The approval happens asynchronously while checkPreExecutionGates is
|
|
465
|
+
// polling for the grant.
|
|
466
|
+
const approvalPromise = (async () => {
|
|
467
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
468
|
+
const pendingRequests = listCanonicalGuardianRequests({
|
|
469
|
+
kind: 'tool_grant_request',
|
|
470
|
+
status: 'pending',
|
|
471
|
+
toolName: 'bash',
|
|
472
|
+
});
|
|
473
|
+
if (pendingRequests.length === 0) return;
|
|
474
|
+
await applyCanonicalGuardianDecision({
|
|
475
|
+
requestId: pendingRequests[0].id,
|
|
476
|
+
action: 'approve_once',
|
|
477
|
+
actorContext: guardianActor(),
|
|
478
|
+
});
|
|
479
|
+
})();
|
|
480
|
+
|
|
481
|
+
const result = await handler.checkPreExecutionGates(
|
|
482
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
await approvalPromise;
|
|
486
|
+
|
|
487
|
+
// The tool invocation should have succeeded inline
|
|
488
|
+
expect(result.allowed).toBe(true);
|
|
489
|
+
if (!result.allowed) return;
|
|
490
|
+
expect(result.grantConsumed).toBe(true);
|
|
491
|
+
|
|
492
|
+
// Replay is denied (one-time grant semantics)
|
|
493
|
+
const replayResult = await handler.checkPreExecutionGates(
|
|
494
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
495
|
+
);
|
|
496
|
+
expect(replayResult.allowed).toBe(false);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test('pre-existing grant from prior approval is consumed immediately (no wait)', async () => {
|
|
448
500
|
const toolName = 'bash';
|
|
449
501
|
const input = { command: 'echo secret' };
|
|
450
502
|
const _inputDigest = computeToolApprovalDigest(toolName, input);
|
|
451
503
|
|
|
452
504
|
const context = makeContext({ guardianTrustClass: 'trusted_contact' });
|
|
453
505
|
|
|
454
|
-
// Step 1: First invocation
|
|
455
|
-
const
|
|
506
|
+
// Step 1: First invocation times out (short wait, no approval)
|
|
507
|
+
const shortHandler = new ToolApprovalHandler({ inlineGrantWait: TEST_INLINE_WAIT_CONFIG });
|
|
508
|
+
await shortHandler.checkPreExecutionGates(
|
|
456
509
|
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
457
510
|
);
|
|
458
|
-
expect(firstResult.allowed).toBe(false);
|
|
459
511
|
|
|
460
|
-
// Verify
|
|
512
|
+
// Verify request was created
|
|
461
513
|
const pendingRequests = listCanonicalGuardianRequests({
|
|
462
514
|
kind: 'tool_grant_request',
|
|
463
515
|
status: 'pending',
|
|
464
516
|
toolName: 'bash',
|
|
465
517
|
});
|
|
466
518
|
expect(pendingRequests.length).toBe(1);
|
|
467
|
-
const canonicalRequestId = pendingRequests[0].id;
|
|
468
519
|
|
|
469
|
-
// Step 2: Guardian approves the
|
|
520
|
+
// Step 2: Guardian approves the request (mints a grant)
|
|
470
521
|
const approvalResult = await applyCanonicalGuardianDecision({
|
|
471
|
-
requestId:
|
|
522
|
+
requestId: pendingRequests[0].id,
|
|
472
523
|
action: 'approve_once',
|
|
473
524
|
actorContext: guardianActor(),
|
|
474
525
|
});
|
|
475
526
|
expect(approvalResult.applied).toBe(true);
|
|
476
|
-
if (!approvalResult.applied) return;
|
|
477
|
-
expect(approvalResult.grantMinted).toBe(true);
|
|
478
527
|
|
|
479
|
-
//
|
|
480
|
-
const
|
|
481
|
-
expect(resolvedRequest!.status).toBe('approved');
|
|
482
|
-
|
|
483
|
-
// Step 3: Second identical invocation consumes the grant and succeeds
|
|
528
|
+
// Step 3: Second invocation finds the pre-existing grant immediately
|
|
529
|
+
const start = Date.now();
|
|
484
530
|
const secondResult = await handler.checkPreExecutionGates(
|
|
485
531
|
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
486
532
|
);
|
|
533
|
+
const elapsed = Date.now() - start;
|
|
534
|
+
|
|
487
535
|
expect(secondResult.allowed).toBe(true);
|
|
488
536
|
if (!secondResult.allowed) return;
|
|
489
537
|
expect(secondResult.grantConsumed).toBe(true);
|
|
538
|
+
// Should be nearly instant since the grant already exists
|
|
539
|
+
expect(elapsed).toBeLessThan(500);
|
|
540
|
+
});
|
|
541
|
+
});
|
|
490
542
|
|
|
491
|
-
|
|
492
|
-
|
|
543
|
+
// ---------------------------------------------------------------------------
|
|
544
|
+
// 5. Inline wait-and-resume for trusted-contact grant-gated tools
|
|
545
|
+
// ---------------------------------------------------------------------------
|
|
546
|
+
|
|
547
|
+
describe('inline wait-and-resume', () => {
|
|
548
|
+
const events: ToolLifecycleEvent[] = [];
|
|
549
|
+
const emitLifecycleEvent = (event: ToolLifecycleEvent) => { events.push(event); };
|
|
550
|
+
|
|
551
|
+
beforeEach(() => {
|
|
552
|
+
resetTables();
|
|
553
|
+
events.length = 0;
|
|
554
|
+
emittedSignals.length = 0;
|
|
555
|
+
deliveredReplies.length = 0;
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test('waitForInlineGrant returns granted when grant appears during wait', async () => {
|
|
559
|
+
// Create a canonical request manually
|
|
560
|
+
const req = createCanonicalGuardianRequest({
|
|
561
|
+
kind: 'tool_grant_request',
|
|
562
|
+
sourceType: 'channel',
|
|
563
|
+
sourceChannel: 'telegram',
|
|
564
|
+
conversationId: 'conv-1',
|
|
565
|
+
requesterExternalUserId: 'requester-1',
|
|
566
|
+
guardianExternalUserId: 'guardian-1',
|
|
567
|
+
toolName: 'bash',
|
|
568
|
+
inputDigest: 'sha256:waitgrant',
|
|
569
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// Schedule approval after 50ms
|
|
573
|
+
setTimeout(async () => {
|
|
574
|
+
await applyCanonicalGuardianDecision({
|
|
575
|
+
requestId: req.id,
|
|
576
|
+
action: 'approve_once',
|
|
577
|
+
actorContext: guardianActor(),
|
|
578
|
+
});
|
|
579
|
+
}, 50);
|
|
580
|
+
|
|
581
|
+
const result = await waitForInlineGrant(
|
|
582
|
+
req.id,
|
|
583
|
+
{
|
|
584
|
+
toolName: 'bash',
|
|
585
|
+
inputDigest: 'sha256:waitgrant',
|
|
586
|
+
consumingRequestId: 'consume-1',
|
|
587
|
+
assistantId: 'self',
|
|
588
|
+
conversationId: 'conv-1',
|
|
589
|
+
requesterExternalUserId: 'requester-1',
|
|
590
|
+
executionChannel: 'telegram',
|
|
591
|
+
},
|
|
592
|
+
{ maxWaitMs: 2_000, intervalMs: 20 },
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
expect(result.outcome).toBe('granted');
|
|
596
|
+
if (result.outcome === 'granted') {
|
|
597
|
+
expect(result.grant.id).toBeDefined();
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test('waitForInlineGrant returns denied when guardian rejects during wait', async () => {
|
|
602
|
+
const req = createCanonicalGuardianRequest({
|
|
603
|
+
kind: 'tool_grant_request',
|
|
604
|
+
sourceType: 'channel',
|
|
605
|
+
sourceChannel: 'telegram',
|
|
606
|
+
conversationId: 'conv-1',
|
|
607
|
+
requesterExternalUserId: 'requester-1',
|
|
608
|
+
guardianExternalUserId: 'guardian-1',
|
|
609
|
+
toolName: 'bash',
|
|
610
|
+
inputDigest: 'sha256:denywait',
|
|
611
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// Schedule rejection after 50ms
|
|
615
|
+
setTimeout(async () => {
|
|
616
|
+
await applyCanonicalGuardianDecision({
|
|
617
|
+
requestId: req.id,
|
|
618
|
+
action: 'reject',
|
|
619
|
+
actorContext: guardianActor(),
|
|
620
|
+
});
|
|
621
|
+
}, 50);
|
|
622
|
+
|
|
623
|
+
const result = await waitForInlineGrant(
|
|
624
|
+
req.id,
|
|
625
|
+
{
|
|
626
|
+
toolName: 'bash',
|
|
627
|
+
inputDigest: 'sha256:denywait',
|
|
628
|
+
consumingRequestId: 'consume-1',
|
|
629
|
+
assistantId: 'self',
|
|
630
|
+
},
|
|
631
|
+
{ maxWaitMs: 2_000, intervalMs: 20 },
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
expect(result.outcome).toBe('denied');
|
|
635
|
+
if (result.outcome === 'denied') {
|
|
636
|
+
expect(result.requestId).toBe(req.id);
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test('waitForInlineGrant returns timeout when no decision arrives', async () => {
|
|
641
|
+
const req = createCanonicalGuardianRequest({
|
|
642
|
+
kind: 'tool_grant_request',
|
|
643
|
+
sourceType: 'channel',
|
|
644
|
+
sourceChannel: 'telegram',
|
|
645
|
+
conversationId: 'conv-1',
|
|
646
|
+
requesterExternalUserId: 'requester-1',
|
|
647
|
+
guardianExternalUserId: 'guardian-1',
|
|
648
|
+
toolName: 'bash',
|
|
649
|
+
inputDigest: 'sha256:timeoutwait',
|
|
650
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
const start = Date.now();
|
|
654
|
+
const result = await waitForInlineGrant(
|
|
655
|
+
req.id,
|
|
656
|
+
{
|
|
657
|
+
toolName: 'bash',
|
|
658
|
+
inputDigest: 'sha256:timeoutwait',
|
|
659
|
+
consumingRequestId: 'consume-1',
|
|
660
|
+
assistantId: 'self',
|
|
661
|
+
},
|
|
662
|
+
{ maxWaitMs: 100, intervalMs: 20 },
|
|
663
|
+
);
|
|
664
|
+
const elapsed = Date.now() - start;
|
|
665
|
+
|
|
666
|
+
expect(result.outcome).toBe('timeout');
|
|
667
|
+
if (result.outcome === 'timeout') {
|
|
668
|
+
expect(result.requestId).toBe(req.id);
|
|
669
|
+
}
|
|
670
|
+
expect(elapsed).toBeGreaterThanOrEqual(80);
|
|
671
|
+
expect(elapsed).toBeLessThan(500);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
test('waitForInlineGrant returns aborted when signal fires during wait', async () => {
|
|
675
|
+
const req = createCanonicalGuardianRequest({
|
|
676
|
+
kind: 'tool_grant_request',
|
|
677
|
+
sourceType: 'channel',
|
|
678
|
+
sourceChannel: 'telegram',
|
|
679
|
+
conversationId: 'conv-1',
|
|
680
|
+
requesterExternalUserId: 'requester-1',
|
|
681
|
+
guardianExternalUserId: 'guardian-1',
|
|
682
|
+
toolName: 'bash',
|
|
683
|
+
inputDigest: 'sha256:abortwait',
|
|
684
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
const controller = new AbortController();
|
|
688
|
+
setTimeout(() => controller.abort(), 50);
|
|
689
|
+
|
|
690
|
+
const start = Date.now();
|
|
691
|
+
const result = await waitForInlineGrant(
|
|
692
|
+
req.id,
|
|
693
|
+
{
|
|
694
|
+
toolName: 'bash',
|
|
695
|
+
inputDigest: 'sha256:abortwait',
|
|
696
|
+
consumingRequestId: 'consume-1',
|
|
697
|
+
assistantId: 'self',
|
|
698
|
+
},
|
|
699
|
+
{ maxWaitMs: 5_000, intervalMs: 20, signal: controller.signal },
|
|
700
|
+
);
|
|
701
|
+
const elapsed = Date.now() - start;
|
|
702
|
+
|
|
703
|
+
expect(result.outcome).toBe('aborted');
|
|
704
|
+
expect(elapsed).toBeLessThan(500);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
test('inline wait: guardian rejects -> handler returns explicit denial', async () => {
|
|
708
|
+
const handler = new ToolApprovalHandler({ inlineGrantWait: { maxWaitMs: 2_000, intervalMs: 20 } });
|
|
709
|
+
|
|
710
|
+
const toolName = 'bash';
|
|
711
|
+
const input = { command: 'rm -rf /' };
|
|
712
|
+
const context = makeContext({ guardianTrustClass: 'trusted_contact' });
|
|
713
|
+
|
|
714
|
+
// Schedule rejection after 100ms
|
|
715
|
+
const rejectionPromise = (async () => {
|
|
716
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
717
|
+
const pending = listCanonicalGuardianRequests({
|
|
718
|
+
kind: 'tool_grant_request',
|
|
719
|
+
status: 'pending',
|
|
720
|
+
toolName: 'bash',
|
|
721
|
+
});
|
|
722
|
+
if (pending.length === 0) return;
|
|
723
|
+
await applyCanonicalGuardianDecision({
|
|
724
|
+
requestId: pending[0].id,
|
|
725
|
+
action: 'reject',
|
|
726
|
+
actorContext: guardianActor(),
|
|
727
|
+
});
|
|
728
|
+
})();
|
|
729
|
+
|
|
730
|
+
const start = Date.now();
|
|
731
|
+
const result = await handler.checkPreExecutionGates(
|
|
493
732
|
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
494
733
|
);
|
|
495
|
-
|
|
734
|
+
const elapsed = Date.now() - start;
|
|
735
|
+
|
|
736
|
+
await rejectionPromise;
|
|
737
|
+
|
|
738
|
+
expect(result.allowed).toBe(false);
|
|
739
|
+
if (result.allowed) return;
|
|
740
|
+
expect(result.result.content).toContain('guardian rejected the request');
|
|
741
|
+
// Should exit well before the 2s timeout
|
|
742
|
+
expect(elapsed).toBeLessThan(1_000);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
test('inline wait: abort signal cancels cleanly during wait', async () => {
|
|
746
|
+
const handler = new ToolApprovalHandler({ inlineGrantWait: { maxWaitMs: 5_000, intervalMs: 20 } });
|
|
747
|
+
|
|
748
|
+
const toolName = 'bash';
|
|
749
|
+
const input = { command: 'do something' };
|
|
750
|
+
const controller = new AbortController();
|
|
751
|
+
const context = makeContext({
|
|
752
|
+
guardianTrustClass: 'trusted_contact',
|
|
753
|
+
signal: controller.signal,
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
// Abort after 100ms
|
|
757
|
+
setTimeout(() => controller.abort(), 100);
|
|
758
|
+
|
|
759
|
+
const start = Date.now();
|
|
760
|
+
const result = await handler.checkPreExecutionGates(
|
|
761
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
762
|
+
);
|
|
763
|
+
const elapsed = Date.now() - start;
|
|
764
|
+
|
|
765
|
+
expect(result.allowed).toBe(false);
|
|
766
|
+
if (result.allowed) return;
|
|
767
|
+
expect(result.result.content).toBe('Cancelled');
|
|
768
|
+
expect(result.result.isError).toBe(true);
|
|
769
|
+
expect(elapsed).toBeLessThan(1_000);
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
test('unknown/unverified actors do NOT get inline wait behavior', async () => {
|
|
773
|
+
const handler = new ToolApprovalHandler({ inlineGrantWait: { maxWaitMs: 2_000, intervalMs: 20 } });
|
|
774
|
+
|
|
775
|
+
const toolName = 'bash';
|
|
776
|
+
const input = { command: 'ls' };
|
|
777
|
+
const context = makeContext({
|
|
778
|
+
guardianTrustClass: 'unknown',
|
|
779
|
+
executionChannel: 'telegram',
|
|
780
|
+
requesterExternalUserId: 'unknown-user',
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
const start = Date.now();
|
|
784
|
+
const result = await handler.checkPreExecutionGates(
|
|
785
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
786
|
+
);
|
|
787
|
+
const elapsed = Date.now() - start;
|
|
788
|
+
|
|
789
|
+
expect(result.allowed).toBe(false);
|
|
790
|
+
if (result.allowed) return;
|
|
791
|
+
// Unknown actors get the generic fail-closed message, not the wait behavior
|
|
792
|
+
expect(result.result.content).toContain('verified channel identity');
|
|
793
|
+
// Should be near-instant, no waiting
|
|
794
|
+
expect(elapsed).toBeLessThan(200);
|
|
795
|
+
|
|
796
|
+
// No canonical request created
|
|
797
|
+
const requests = listCanonicalGuardianRequests({
|
|
798
|
+
kind: 'tool_grant_request',
|
|
799
|
+
status: 'pending',
|
|
800
|
+
});
|
|
801
|
+
expect(requests.length).toBe(0);
|
|
496
802
|
});
|
|
497
803
|
});
|