@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
|
@@ -302,24 +302,18 @@ describe('ChannelReadinessService', () => {
|
|
|
302
302
|
expect(snapshot.reasons).toEqual([]);
|
|
303
303
|
});
|
|
304
304
|
|
|
305
|
-
test('remote cache
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const key = context?.assistantId ?? '__default__';
|
|
312
|
-
remoteCalls[key] = (remoteCalls[key] ?? 0) + 1;
|
|
313
|
-
return [{ name: 'remote', passed: true, message: `ok-${key}` }];
|
|
314
|
-
},
|
|
315
|
-
};
|
|
305
|
+
test('remote cache uses fixed internal scope (no per-assistantId scoping)', async () => {
|
|
306
|
+
const probe = makeProbe(
|
|
307
|
+
'sms',
|
|
308
|
+
[{ name: 'local', passed: true, message: 'ok' }],
|
|
309
|
+
[{ name: 'remote', passed: true, message: 'ok' }],
|
|
310
|
+
);
|
|
316
311
|
service.registerProbe(probe);
|
|
317
312
|
|
|
318
|
-
|
|
319
|
-
await service.getReadiness('sms', true
|
|
320
|
-
await service.getReadiness('sms', true
|
|
313
|
+
// All calls share the same cache key since there is no assistantId dimension
|
|
314
|
+
await service.getReadiness('sms', true);
|
|
315
|
+
await service.getReadiness('sms', true);
|
|
321
316
|
|
|
322
|
-
expect(
|
|
323
|
-
expect(remoteCalls['ast-beta']).toBe(1);
|
|
317
|
+
expect(probe.remoteCallCount).toBe(1);
|
|
324
318
|
});
|
|
325
319
|
});
|
|
@@ -63,7 +63,7 @@ mock.module('../config/loader.js', () => ({
|
|
|
63
63
|
setNestedValue: () => {},
|
|
64
64
|
}));
|
|
65
65
|
|
|
66
|
-
import { _resetLegacyDeprecationWarning,check, classifyRisk, generateAllowlistOptions, generateScopeOptions } from '../permissions/checker.js';
|
|
66
|
+
import { _resetLegacyDeprecationWarning,check, classifyRisk, generateAllowlistOptions, generateScopeOptions, SCOPE_AWARE_TOOLS } from '../permissions/checker.js';
|
|
67
67
|
import { getDefaultRuleTemplates } from '../permissions/defaults.js';
|
|
68
68
|
import { addRule, clearCache, findHighestPriorityRule } from '../permissions/trust-store.js';
|
|
69
69
|
import type { TrustRule } from '../permissions/types.js';
|
|
@@ -1341,21 +1341,42 @@ describe('Permission Checker', () => {
|
|
|
1341
1341
|
expect(options[2]).toEqual({ label: 'everywhere', scope: 'everywhere' });
|
|
1342
1342
|
});
|
|
1343
1343
|
|
|
1344
|
-
test('scope
|
|
1344
|
+
test('scope-aware tools all produce the same directory-based ordering', () => {
|
|
1345
1345
|
const workingDir = join(homedir(), 'projects', 'myapp');
|
|
1346
1346
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
expect(
|
|
1350
|
-
expect(nonHostOpts[nonHostOpts.length - 1].scope).toBe('everywhere');
|
|
1347
|
+
const bashOpts = generateScopeOptions(workingDir, 'bash');
|
|
1348
|
+
expect(bashOpts[0].scope).toBe(workingDir);
|
|
1349
|
+
expect(bashOpts[bashOpts.length - 1].scope).toBe('everywhere');
|
|
1351
1350
|
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
expect(hostOpts[0].scope).toBe(workingDir);
|
|
1355
|
-
expect(hostOpts[hostOpts.length - 1].scope).toBe('everywhere');
|
|
1351
|
+
const hostBashOpts = generateScopeOptions(workingDir, 'host_bash');
|
|
1352
|
+
expect(bashOpts.map(o => o.scope)).toEqual(hostBashOpts.map(o => o.scope));
|
|
1356
1353
|
|
|
1357
|
-
|
|
1358
|
-
expect(
|
|
1354
|
+
const fileOpts = generateScopeOptions(workingDir, 'file_write');
|
|
1355
|
+
expect(bashOpts.map(o => o.scope)).toEqual(fileOpts.map(o => o.scope));
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
test('returns empty for non-scoped tools', () => {
|
|
1359
|
+
const workingDir = join(homedir(), 'projects', 'myapp');
|
|
1360
|
+
expect(generateScopeOptions(workingDir, 'web_fetch')).toHaveLength(0);
|
|
1361
|
+
expect(generateScopeOptions(workingDir, 'browser_navigate')).toHaveLength(0);
|
|
1362
|
+
expect(generateScopeOptions(workingDir, 'skill_load')).toHaveLength(0);
|
|
1363
|
+
expect(generateScopeOptions(workingDir, 'credential_store')).toHaveLength(0);
|
|
1364
|
+
expect(generateScopeOptions(workingDir, 'computer_use_click')).toHaveLength(0);
|
|
1365
|
+
expect(generateScopeOptions(workingDir, 'my_custom_mcp_tool')).toHaveLength(0);
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
test('returns directory options when toolName is omitted (backward compat)', () => {
|
|
1369
|
+
const options = generateScopeOptions('/home/user/project');
|
|
1370
|
+
expect(options).toHaveLength(3);
|
|
1371
|
+
expect(options[0].scope).toBe('/home/user/project');
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
test('SCOPE_AWARE_TOOLS contains only filesystem and shell tools', () => {
|
|
1375
|
+
expect(SCOPE_AWARE_TOOLS).toEqual(new Set([
|
|
1376
|
+
'bash', 'host_bash',
|
|
1377
|
+
'file_read', 'file_write', 'file_edit',
|
|
1378
|
+
'host_file_read', 'host_file_write', 'host_file_edit',
|
|
1379
|
+
]));
|
|
1359
1380
|
});
|
|
1360
1381
|
});
|
|
1361
1382
|
|
|
@@ -581,6 +581,12 @@ describe('AssistantConfigSchema', () => {
|
|
|
581
581
|
provider: 'twilio',
|
|
582
582
|
maxDurationSeconds: 3600,
|
|
583
583
|
userConsultTimeoutSeconds: 120,
|
|
584
|
+
ttsPlaybackDelayMs: 3000,
|
|
585
|
+
accessRequestPollIntervalMs: 500,
|
|
586
|
+
guardianWaitUpdateInitialIntervalMs: 5000,
|
|
587
|
+
guardianWaitUpdateInitialWindowMs: 30000,
|
|
588
|
+
guardianWaitUpdateSteadyMinIntervalMs: 7000,
|
|
589
|
+
guardianWaitUpdateSteadyMaxIntervalMs: 10000,
|
|
584
590
|
disclosure: {
|
|
585
591
|
enabled: true,
|
|
586
592
|
text: 'At the very beginning of the call, introduce yourself as an assistant calling on behalf of the person you represent. Do not say "AI assistant".',
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the confirmation-request -> guardian.question notification bridge.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that:
|
|
5
|
+
* 1. Trusted-contact confirmation_requests emit guardian.question notifications
|
|
6
|
+
* 2. Canonical delivery rows are persisted for guardian destinations
|
|
7
|
+
* 3. Guardian and unknown actor sessions are correctly skipped
|
|
8
|
+
* 4. Missing guardian binding causes a skip
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
16
|
+
|
|
17
|
+
const testDir = mkdtempSync(join(tmpdir(), 'confirmation-bridge-test-'));
|
|
18
|
+
|
|
19
|
+
mock.module('../util/platform.js', () => ({
|
|
20
|
+
getDataDir: () => testDir,
|
|
21
|
+
isMacOS: () => process.platform === 'darwin',
|
|
22
|
+
isLinux: () => process.platform === 'linux',
|
|
23
|
+
isWindows: () => process.platform === 'win32',
|
|
24
|
+
getSocketPath: () => join(testDir, 'test.sock'),
|
|
25
|
+
getPidPath: () => join(testDir, 'test.pid'),
|
|
26
|
+
getDbPath: () => join(testDir, 'test.db'),
|
|
27
|
+
getLogPath: () => join(testDir, 'test.log'),
|
|
28
|
+
ensureDataDir: () => {},
|
|
29
|
+
migrateToDataLayout: () => {},
|
|
30
|
+
migrateToWorkspaceLayout: () => {},
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
mock.module('../util/logger.js', () => ({
|
|
34
|
+
getLogger: () =>
|
|
35
|
+
new Proxy({} as Record<string, unknown>, {
|
|
36
|
+
get: () => () => {},
|
|
37
|
+
}),
|
|
38
|
+
isDebug: () => false,
|
|
39
|
+
truncateForLog: (value: string) => value,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
// Mock notification emission — capture calls without running the full pipeline
|
|
43
|
+
const emittedSignals: Array<Record<string, unknown>> = [];
|
|
44
|
+
const mockOnThreadCreatedCallbacks: Array<(info: { conversationId: string; title: string; sourceEventName: string }) => void> = [];
|
|
45
|
+
mock.module('../notifications/emit-signal.js', () => ({
|
|
46
|
+
emitNotificationSignal: async (params: Record<string, unknown>) => {
|
|
47
|
+
emittedSignals.push(params);
|
|
48
|
+
// Capture onThreadCreated callback so tests can invoke it
|
|
49
|
+
if (typeof params.onThreadCreated === 'function') {
|
|
50
|
+
mockOnThreadCreatedCallbacks.push(params.onThreadCreated as (info: { conversationId: string; title: string; sourceEventName: string }) => void);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
signalId: 'test-signal',
|
|
54
|
+
deduplicated: false,
|
|
55
|
+
dispatched: true,
|
|
56
|
+
reason: 'ok',
|
|
57
|
+
deliveryResults: [
|
|
58
|
+
{ channel: 'telegram', destination: 'guardian-chat-1', success: true },
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
registerBroadcastFn: () => {},
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
// Mock channel guardian service — provide a guardian binding for 'self' + 'telegram'
|
|
66
|
+
mock.module('../runtime/channel-guardian-service.js', () => ({
|
|
67
|
+
getGuardianBinding: (assistantId: string, channel: string) => {
|
|
68
|
+
if (assistantId === 'self' && channel === 'telegram') {
|
|
69
|
+
return {
|
|
70
|
+
id: 'binding-1',
|
|
71
|
+
assistantId: 'self',
|
|
72
|
+
channel: 'telegram',
|
|
73
|
+
guardianExternalUserId: 'guardian-1',
|
|
74
|
+
guardianDeliveryChatId: 'guardian-chat-1',
|
|
75
|
+
status: 'active',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
},
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.js';
|
|
83
|
+
import {
|
|
84
|
+
createCanonicalGuardianRequest,
|
|
85
|
+
generateCanonicalRequestCode,
|
|
86
|
+
listCanonicalGuardianDeliveries,
|
|
87
|
+
} from '../memory/canonical-guardian-store.js';
|
|
88
|
+
import { getDb, initializeDb, resetDb } from '../memory/db.js';
|
|
89
|
+
import { bridgeConfirmationRequestToGuardian } from '../runtime/confirmation-request-guardian-bridge.js';
|
|
90
|
+
|
|
91
|
+
initializeDb();
|
|
92
|
+
|
|
93
|
+
function resetTables(): void {
|
|
94
|
+
const db = getDb();
|
|
95
|
+
db.run('DELETE FROM canonical_guardian_deliveries');
|
|
96
|
+
db.run('DELETE FROM canonical_guardian_requests');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
afterAll(() => {
|
|
100
|
+
resetDb();
|
|
101
|
+
try {
|
|
102
|
+
rmSync(testDir, { recursive: true });
|
|
103
|
+
} catch {
|
|
104
|
+
/* best effort */
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Helpers
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
function makeCanonicalRequest(overrides: Record<string, unknown> = {}) {
|
|
113
|
+
return createCanonicalGuardianRequest({
|
|
114
|
+
id: `req-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
115
|
+
kind: 'tool_approval',
|
|
116
|
+
sourceType: 'channel',
|
|
117
|
+
sourceChannel: 'telegram',
|
|
118
|
+
conversationId: 'conv-1',
|
|
119
|
+
requesterExternalUserId: 'requester-1',
|
|
120
|
+
guardianExternalUserId: 'guardian-1',
|
|
121
|
+
toolName: 'bash',
|
|
122
|
+
status: 'pending',
|
|
123
|
+
requestCode: generateCanonicalRequestCode(),
|
|
124
|
+
expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
|
|
125
|
+
...overrides,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function makeTrustedContactContext(overrides: Partial<GuardianRuntimeContext> = {}): GuardianRuntimeContext {
|
|
130
|
+
return {
|
|
131
|
+
sourceChannel: 'telegram',
|
|
132
|
+
trustClass: 'trusted_contact',
|
|
133
|
+
guardianExternalUserId: 'guardian-1',
|
|
134
|
+
guardianChatId: 'guardian-chat-1',
|
|
135
|
+
requesterExternalUserId: 'requester-1',
|
|
136
|
+
requesterChatId: 'requester-chat-1',
|
|
137
|
+
requesterIdentifier: '@requester',
|
|
138
|
+
...overrides,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ===========================================================================
|
|
143
|
+
// TESTS
|
|
144
|
+
// ===========================================================================
|
|
145
|
+
|
|
146
|
+
describe('bridgeConfirmationRequestToGuardian', () => {
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
resetTables();
|
|
149
|
+
emittedSignals.length = 0;
|
|
150
|
+
mockOnThreadCreatedCallbacks.length = 0;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('emits guardian.question for trusted-contact sessions', () => {
|
|
154
|
+
const canonicalRequest = makeCanonicalRequest();
|
|
155
|
+
const guardianContext = makeTrustedContactContext();
|
|
156
|
+
|
|
157
|
+
const result = bridgeConfirmationRequestToGuardian({
|
|
158
|
+
canonicalRequest,
|
|
159
|
+
guardianContext,
|
|
160
|
+
conversationId: 'conv-1',
|
|
161
|
+
toolName: 'bash',
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect('bridged' in result && result.bridged).toBe(true);
|
|
165
|
+
expect(emittedSignals).toHaveLength(1);
|
|
166
|
+
expect(emittedSignals[0].sourceEventName).toBe('guardian.question');
|
|
167
|
+
expect(emittedSignals[0].sourceChannel).toBe('telegram');
|
|
168
|
+
expect(emittedSignals[0].sourceSessionId).toBe('conv-1');
|
|
169
|
+
|
|
170
|
+
const payload = emittedSignals[0].contextPayload as Record<string, unknown>;
|
|
171
|
+
expect(payload.requestId).toBe(canonicalRequest.id);
|
|
172
|
+
expect(payload.requestCode).toBe(canonicalRequest.requestCode);
|
|
173
|
+
expect(payload.toolName).toBe('bash');
|
|
174
|
+
expect(payload.requesterExternalUserId).toBe('requester-1');
|
|
175
|
+
expect(payload.requesterIdentifier).toBe('@requester');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('skips guardian actor sessions (self-approve)', () => {
|
|
179
|
+
const canonicalRequest = makeCanonicalRequest();
|
|
180
|
+
const guardianContext: GuardianRuntimeContext = {
|
|
181
|
+
sourceChannel: 'telegram',
|
|
182
|
+
trustClass: 'guardian',
|
|
183
|
+
guardianExternalUserId: 'guardian-1',
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const result = bridgeConfirmationRequestToGuardian({
|
|
187
|
+
canonicalRequest,
|
|
188
|
+
guardianContext,
|
|
189
|
+
conversationId: 'conv-1',
|
|
190
|
+
toolName: 'bash',
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect('skipped' in result && result.skipped).toBe(true);
|
|
194
|
+
if ('skipped' in result) {
|
|
195
|
+
expect(result.reason).toBe('not_trusted_contact');
|
|
196
|
+
}
|
|
197
|
+
expect(emittedSignals).toHaveLength(0);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('skips unknown actor sessions', () => {
|
|
201
|
+
const canonicalRequest = makeCanonicalRequest();
|
|
202
|
+
const guardianContext: GuardianRuntimeContext = {
|
|
203
|
+
sourceChannel: 'telegram',
|
|
204
|
+
trustClass: 'unknown',
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const result = bridgeConfirmationRequestToGuardian({
|
|
208
|
+
canonicalRequest,
|
|
209
|
+
guardianContext,
|
|
210
|
+
conversationId: 'conv-1',
|
|
211
|
+
toolName: 'bash',
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
expect('skipped' in result && result.skipped).toBe(true);
|
|
215
|
+
if ('skipped' in result) {
|
|
216
|
+
expect(result.reason).toBe('not_trusted_contact');
|
|
217
|
+
}
|
|
218
|
+
expect(emittedSignals).toHaveLength(0);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('skips when guardian identity is missing', () => {
|
|
222
|
+
const canonicalRequest = makeCanonicalRequest();
|
|
223
|
+
const guardianContext = makeTrustedContactContext({
|
|
224
|
+
guardianExternalUserId: undefined,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const result = bridgeConfirmationRequestToGuardian({
|
|
228
|
+
canonicalRequest,
|
|
229
|
+
guardianContext,
|
|
230
|
+
conversationId: 'conv-1',
|
|
231
|
+
toolName: 'bash',
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect('skipped' in result && result.skipped).toBe(true);
|
|
235
|
+
if ('skipped' in result) {
|
|
236
|
+
expect(result.reason).toBe('missing_guardian_identity');
|
|
237
|
+
}
|
|
238
|
+
expect(emittedSignals).toHaveLength(0);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('skips when no guardian binding exists for channel', () => {
|
|
242
|
+
const canonicalRequest = makeCanonicalRequest({ sourceChannel: 'sms' });
|
|
243
|
+
const guardianContext = makeTrustedContactContext({
|
|
244
|
+
sourceChannel: 'sms',
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const result = bridgeConfirmationRequestToGuardian({
|
|
248
|
+
canonicalRequest,
|
|
249
|
+
guardianContext,
|
|
250
|
+
conversationId: 'conv-1',
|
|
251
|
+
toolName: 'bash',
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
expect('skipped' in result && result.skipped).toBe(true);
|
|
255
|
+
if ('skipped' in result) {
|
|
256
|
+
expect(result.reason).toBe('no_guardian_binding');
|
|
257
|
+
}
|
|
258
|
+
expect(emittedSignals).toHaveLength(0);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('sets correct attention hints for urgency', () => {
|
|
262
|
+
const canonicalRequest = makeCanonicalRequest();
|
|
263
|
+
const guardianContext = makeTrustedContactContext();
|
|
264
|
+
|
|
265
|
+
bridgeConfirmationRequestToGuardian({
|
|
266
|
+
canonicalRequest,
|
|
267
|
+
guardianContext,
|
|
268
|
+
conversationId: 'conv-1',
|
|
269
|
+
toolName: 'bash',
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const hints = emittedSignals[0].attentionHints as Record<string, unknown>;
|
|
273
|
+
expect(hints.requiresAction).toBe(true);
|
|
274
|
+
expect(hints.urgency).toBe('high');
|
|
275
|
+
expect(hints.isAsyncBackground).toBe(false);
|
|
276
|
+
expect(hints.visibleInSourceNow).toBe(false);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('uses dedupe key scoped to canonical request ID', () => {
|
|
280
|
+
const canonicalRequest = makeCanonicalRequest();
|
|
281
|
+
const guardianContext = makeTrustedContactContext();
|
|
282
|
+
|
|
283
|
+
bridgeConfirmationRequestToGuardian({
|
|
284
|
+
canonicalRequest,
|
|
285
|
+
guardianContext,
|
|
286
|
+
conversationId: 'conv-1',
|
|
287
|
+
toolName: 'bash',
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
expect(emittedSignals[0].dedupeKey).toBe(`tc-confirmation-request:${canonicalRequest.id}`);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('creates vellum delivery row via onThreadCreated callback', () => {
|
|
294
|
+
const canonicalRequest = makeCanonicalRequest();
|
|
295
|
+
const guardianContext = makeTrustedContactContext();
|
|
296
|
+
|
|
297
|
+
bridgeConfirmationRequestToGuardian({
|
|
298
|
+
canonicalRequest,
|
|
299
|
+
guardianContext,
|
|
300
|
+
conversationId: 'conv-1',
|
|
301
|
+
toolName: 'bash',
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
expect(mockOnThreadCreatedCallbacks).toHaveLength(1);
|
|
305
|
+
|
|
306
|
+
// Simulate the broadcaster invoking onThreadCreated
|
|
307
|
+
mockOnThreadCreatedCallbacks[0]({
|
|
308
|
+
conversationId: 'guardian-thread-1',
|
|
309
|
+
title: 'Guardian question',
|
|
310
|
+
sourceEventName: 'guardian.question',
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const deliveries = listCanonicalGuardianDeliveries(canonicalRequest.id);
|
|
314
|
+
expect(deliveries).toHaveLength(1);
|
|
315
|
+
expect(deliveries[0].destinationChannel).toBe('vellum');
|
|
316
|
+
expect(deliveries[0].destinationConversationId).toBe('guardian-thread-1');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('uses custom assistantId when provided', () => {
|
|
320
|
+
const canonicalRequest = makeCanonicalRequest();
|
|
321
|
+
const guardianContext = makeTrustedContactContext();
|
|
322
|
+
|
|
323
|
+
bridgeConfirmationRequestToGuardian({
|
|
324
|
+
canonicalRequest,
|
|
325
|
+
guardianContext,
|
|
326
|
+
conversationId: 'conv-1',
|
|
327
|
+
toolName: 'bash',
|
|
328
|
+
assistantId: 'custom-assistant',
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// The mock only returns a binding for 'self', so 'custom-assistant'
|
|
332
|
+
// should fail with no_guardian_binding.
|
|
333
|
+
// Actually let's verify the signal uses the right assistantId.
|
|
334
|
+
// Since mock only has binding for 'self', this will skip.
|
|
335
|
+
expect(emittedSignals).toHaveLength(0);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('passes assistantId to notification signal', () => {
|
|
339
|
+
const canonicalRequest = makeCanonicalRequest();
|
|
340
|
+
const guardianContext = makeTrustedContactContext();
|
|
341
|
+
|
|
342
|
+
// Use default assistantId 'self' which has a binding
|
|
343
|
+
bridgeConfirmationRequestToGuardian({
|
|
344
|
+
canonicalRequest,
|
|
345
|
+
guardianContext,
|
|
346
|
+
conversationId: 'conv-1',
|
|
347
|
+
toolName: 'bash',
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
expect(emittedSignals[0].assistantId).toBe('self');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test('includes requesterChatId as null when not provided', () => {
|
|
354
|
+
const canonicalRequest = makeCanonicalRequest();
|
|
355
|
+
const guardianContext = makeTrustedContactContext({
|
|
356
|
+
requesterChatId: undefined,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
bridgeConfirmationRequestToGuardian({
|
|
360
|
+
canonicalRequest,
|
|
361
|
+
guardianContext,
|
|
362
|
+
conversationId: 'conv-1',
|
|
363
|
+
toolName: 'bash',
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const payload = emittedSignals[0].contextPayload as Record<string, unknown>;
|
|
367
|
+
expect(payload.requesterChatId).toBeNull();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test('skips when binding guardian identity does not match canonical request guardian', () => {
|
|
371
|
+
// Create a canonical request where guardianExternalUserId differs from the
|
|
372
|
+
// binding's guardianExternalUserId ('guardian-1' in the mock).
|
|
373
|
+
const canonicalRequest = makeCanonicalRequest({
|
|
374
|
+
guardianExternalUserId: 'old-guardian-who-was-rebound',
|
|
375
|
+
});
|
|
376
|
+
const guardianContext = makeTrustedContactContext();
|
|
377
|
+
|
|
378
|
+
const result = bridgeConfirmationRequestToGuardian({
|
|
379
|
+
canonicalRequest,
|
|
380
|
+
guardianContext,
|
|
381
|
+
conversationId: 'conv-1',
|
|
382
|
+
toolName: 'bash',
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
expect('skipped' in result && result.skipped).toBe(true);
|
|
386
|
+
if ('skipped' in result) {
|
|
387
|
+
expect(result.reason).toBe('binding_identity_mismatch');
|
|
388
|
+
}
|
|
389
|
+
expect(emittedSignals).toHaveLength(0);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test('does not skip when canonical request guardian identity is null', () => {
|
|
393
|
+
// When guardianExternalUserId is null on the canonical request (e.g. desktop
|
|
394
|
+
// flow), the identity check should be skipped and the bridge should proceed.
|
|
395
|
+
const canonicalRequest = makeCanonicalRequest({
|
|
396
|
+
guardianExternalUserId: null,
|
|
397
|
+
});
|
|
398
|
+
const guardianContext = makeTrustedContactContext();
|
|
399
|
+
|
|
400
|
+
const result = bridgeConfirmationRequestToGuardian({
|
|
401
|
+
canonicalRequest,
|
|
402
|
+
guardianContext,
|
|
403
|
+
conversationId: 'conv-1',
|
|
404
|
+
toolName: 'bash',
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
expect('bridged' in result && result.bridged).toBe(true);
|
|
408
|
+
expect(emittedSignals).toHaveLength(1);
|
|
409
|
+
});
|
|
410
|
+
});
|