@vellumai/assistant 0.3.28 → 0.4.1
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 +33 -3
- package/bun.lock +4 -1
- package/docs/trusted-contact-access.md +9 -2
- package/package.json +6 -3
- package/scripts/ipc/generate-swift.ts +3 -3
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
- package/src/__tests__/agent-loop-thinking.test.ts +1 -1
- package/src/__tests__/approval-routes-http.test.ts +13 -5
- package/src/__tests__/asset-materialize-tool.test.ts +2 -0
- package/src/__tests__/asset-search-tool.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
- package/src/__tests__/attachments-store.test.ts +2 -0
- package/src/__tests__/browser-skill-endstate.test.ts +3 -3
- package/src/__tests__/call-controller.test.ts +30 -29
- package/src/__tests__/call-routes-http.test.ts +34 -32
- package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
- package/src/__tests__/channel-invite-transport.test.ts +6 -6
- package/src/__tests__/channel-reply-delivery.test.ts +19 -0
- package/src/__tests__/channel-retry-sweep.test.ts +130 -0
- package/src/__tests__/clarification-resolver.test.ts +2 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
- package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
- package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
- package/src/__tests__/config-schema.test.ts +5 -5
- package/src/__tests__/config-watcher.test.ts +3 -1
- package/src/__tests__/connection-policy.test.ts +14 -5
- package/src/__tests__/contacts-tools.test.ts +3 -1
- package/src/__tests__/contradiction-checker.test.ts +2 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-routes.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +16 -6
- package/src/__tests__/credential-vault-unit.test.ts +2 -2
- package/src/__tests__/credential-vault.test.ts +5 -4
- package/src/__tests__/daemon-lifecycle.test.ts +9 -0
- package/src/__tests__/daemon-server-session-init.test.ts +27 -0
- package/src/__tests__/elevenlabs-config.test.ts +2 -0
- package/src/__tests__/encrypted-store.test.ts +10 -5
- package/src/__tests__/followup-tools.test.ts +3 -1
- package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
- package/src/__tests__/gmail-integration.test.ts +0 -1
- package/src/__tests__/guardian-control-plane-policy.test.ts +25 -21
- package/src/__tests__/guardian-dispatch.test.ts +2 -0
- package/src/__tests__/guardian-grant-minting.test.ts +68 -1
- package/src/__tests__/guardian-outbound-http.test.ts +12 -9
- package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
- package/src/__tests__/handlers-slack-config.test.ts +3 -1
- package/src/__tests__/handlers-telegram-config.test.ts +3 -1
- package/src/__tests__/handlers-twilio-config.test.ts +3 -1
- package/src/__tests__/handlers-twitter-config.test.ts +3 -1
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
- package/src/__tests__/heartbeat-service.test.ts +20 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
- package/src/__tests__/ingress-reconcile.test.ts +3 -1
- package/src/__tests__/ingress-routes-http.test.ts +231 -4
- package/src/__tests__/intent-routing.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +13 -0
- package/src/__tests__/media-generate-image.test.ts +21 -0
- package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
- package/src/__tests__/memory-regressions.test.ts +20 -20
- package/src/__tests__/non-member-access-request.test.ts +183 -9
- package/src/__tests__/notification-decision-fallback.test.ts +2 -0
- package/src/__tests__/notification-decision-strategy.test.ts +61 -0
- package/src/__tests__/notification-guardian-path.test.ts +2 -0
- package/src/__tests__/oauth-connect-handler.test.ts +3 -1
- package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
- package/src/__tests__/pairing-routes.test.ts +171 -0
- package/src/__tests__/playbook-execution.test.ts +3 -1
- package/src/__tests__/playbook-tools.test.ts +3 -1
- package/src/__tests__/provider-error-scenarios.test.ts +59 -8
- package/src/__tests__/proxy-approval-callback.test.ts +2 -0
- package/src/__tests__/recording-handler.test.ts +11 -0
- package/src/__tests__/recording-intent-handler.test.ts +15 -0
- package/src/__tests__/recording-state-machine.test.ts +13 -2
- package/src/__tests__/registry.test.ts +7 -3
- package/src/__tests__/relay-server.test.ts +148 -28
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
- package/src/__tests__/runtime-events-sse.test.ts +4 -2
- package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
- package/src/__tests__/schedule-tools.test.ts +3 -1
- package/src/__tests__/send-endpoint-busy.test.ts +288 -0
- package/src/__tests__/session-abort-tool-results.test.ts +23 -0
- package/src/__tests__/session-agent-loop.test.ts +16 -0
- package/src/__tests__/session-conflict-gate.test.ts +21 -0
- package/src/__tests__/session-load-history-repair.test.ts +27 -17
- package/src/__tests__/session-pre-run-repair.test.ts +23 -0
- package/src/__tests__/session-profile-injection.test.ts +21 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
- package/src/__tests__/session-queue.test.ts +23 -0
- package/src/__tests__/session-runtime-assembly.test.ts +50 -12
- package/src/__tests__/session-skill-tools.test.ts +27 -5
- package/src/__tests__/session-slash-known.test.ts +23 -0
- package/src/__tests__/session-slash-queue.test.ts +23 -0
- package/src/__tests__/session-slash-unknown.test.ts +23 -0
- package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
- package/src/__tests__/session-workspace-injection.test.ts +21 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
- package/src/__tests__/shell-credential-ref.test.ts +2 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
- package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
- package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
- package/src/__tests__/skills.test.ts +8 -4
- package/src/__tests__/slack-channel-config.test.ts +3 -1
- package/src/__tests__/subagent-tools.test.ts +19 -0
- package/src/__tests__/swarm-recursion.test.ts +2 -0
- package/src/__tests__/swarm-session-integration.test.ts +2 -0
- package/src/__tests__/swarm-tool.test.ts +2 -0
- package/src/__tests__/system-prompt.test.ts +3 -1
- package/src/__tests__/task-compiler.test.ts +3 -1
- package/src/__tests__/task-management-tools.test.ts +3 -1
- package/src/__tests__/task-tools.test.ts +3 -1
- package/src/__tests__/terminal-sandbox.test.ts +13 -12
- package/src/__tests__/terminal-tools.test.ts +2 -0
- package/src/__tests__/tool-approval-handler.test.ts +15 -15
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
- package/src/__tests__/tool-grant-request-escalation.test.ts +7 -7
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
- package/src/__tests__/trusted-contact-verification.test.ts +91 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
- package/src/__tests__/twitter-auth-handler.test.ts +3 -1
- package/src/__tests__/twitter-cli-routing.test.ts +3 -1
- package/src/__tests__/view-image-tool.test.ts +3 -1
- package/src/__tests__/voice-invite-redemption.test.ts +329 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
- package/src/__tests__/voice-session-bridge.test.ts +10 -10
- package/src/__tests__/work-item-output.test.ts +3 -1
- package/src/__tests__/workspace-lifecycle.test.ts +13 -2
- package/src/calls/call-controller.ts +26 -23
- package/src/calls/guardian-action-sweep.ts +10 -2
- package/src/calls/relay-server.ts +216 -27
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +3 -3
- package/src/cli.ts +12 -0
- package/src/config/agent-schema.ts +14 -3
- package/src/config/calls-schema.ts +6 -6
- package/src/config/core-schema.ts +3 -3
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/mcp-schema.ts +1 -1
- package/src/config/memory-schema.ts +27 -19
- package/src/config/schema.ts +21 -21
- package/src/config/skills-schema.ts +7 -7
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +139 -16
- package/src/daemon/handlers/config-inbox.ts +4 -4
- package/src/daemon/handlers/sessions.ts +148 -4
- package/src/daemon/ipc-contract/messages.ts +16 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +19 -0
- package/src/daemon/pairing-store.ts +86 -3
- package/src/daemon/response-tier.ts +6 -5
- package/src/daemon/session-agent-loop.ts +5 -5
- package/src/daemon/session-lifecycle.ts +25 -17
- package/src/daemon/session-memory.ts +2 -2
- package/src/daemon/session-process.ts +1 -20
- package/src/daemon/session-runtime-assembly.ts +28 -22
- package/src/daemon/session-tool-setup.ts +2 -2
- package/src/daemon/session.ts +3 -3
- package/src/memory/canonical-guardian-store.ts +63 -1
- package/src/memory/channel-guardian-store.ts +1 -0
- package/src/memory/conversation-crud.ts +7 -7
- package/src/memory/db-init.ts +4 -0
- package/src/memory/embedding-local.ts +257 -39
- package/src/memory/embedding-runtime-manager.ts +471 -0
- package/src/memory/guardian-bindings.ts +25 -1
- package/src/memory/indexer.ts +3 -3
- package/src/memory/ingress-invite-store.ts +45 -0
- package/src/memory/job-handlers/backfill.ts +16 -9
- package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/qdrant-client.ts +31 -22
- package/src/memory/schema.ts +4 -0
- package/src/notifications/copy-composer.ts +15 -0
- package/src/runtime/access-request-helper.ts +43 -7
- package/src/runtime/actor-trust-resolver.ts +46 -50
- package/src/runtime/channel-invite-transports/voice.ts +58 -0
- package/src/runtime/channel-retry-sweep.ts +18 -6
- package/src/runtime/guardian-context-resolver.ts +38 -96
- package/src/runtime/guardian-reply-router.ts +31 -1
- package/src/runtime/ingress-service.ts +80 -3
- package/src/runtime/invite-redemption-service.ts +141 -2
- package/src/runtime/routes/channel-route-shared.ts +1 -1
- package/src/runtime/routes/channel-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +166 -2
- package/src/runtime/routes/guardian-approval-interception.ts +17 -6
- package/src/runtime/routes/inbound-message-handler.ts +41 -10
- package/src/runtime/routes/ingress-routes.ts +52 -4
- package/src/runtime/routes/pairing-routes.ts +3 -0
- package/src/tools/guardian-control-plane-policy.ts +2 -2
- package/src/tools/reminder/reminder-store.ts +10 -14
- package/src/tools/tool-approval-handler.ts +11 -11
- package/src/tools/types.ts +2 -2
- package/src/util/logger.ts +20 -8
- package/src/util/platform.ts +10 -0
- package/src/util/voice-code.ts +29 -0
- package/src/daemon/guardian-invite-intent.ts +0 -124
|
@@ -31,10 +31,31 @@ const DECLARED_LEGACY_KEY = 'skills.hatch-new-assistant.enabled';
|
|
|
31
31
|
|
|
32
32
|
mock.module('../config/skills.js', () => ({
|
|
33
33
|
loadSkillCatalog: () => mockCatalog,
|
|
34
|
+
checkSkillRequirements: () => ({ satisfied: true, missing: [] }),
|
|
34
35
|
}));
|
|
35
36
|
|
|
36
37
|
mock.module('../config/loader.js', () => ({
|
|
37
38
|
getConfig: () => currentConfig,
|
|
39
|
+
loadConfig: () => currentConfig,
|
|
40
|
+
invalidateConfigCache: () => {},
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
mock.module('../config/assistant-feature-flags.js', () => ({
|
|
44
|
+
isAssistantFeatureFlagEnabled: (key: string, config: Record<string, unknown>) => {
|
|
45
|
+
const vals = (config as { assistantFeatureFlagValues?: Record<string, boolean> }).assistantFeatureFlagValues;
|
|
46
|
+
if (vals && typeof vals[key] === 'boolean') return vals[key];
|
|
47
|
+
// Check legacy featureFlags too
|
|
48
|
+
const legacy = (config as { featureFlags?: Record<string, boolean> }).featureFlags;
|
|
49
|
+
if (legacy && typeof legacy[key] === 'boolean') return legacy[key];
|
|
50
|
+
return true; // default enabled
|
|
51
|
+
},
|
|
52
|
+
loadDefaultsRegistry: () => ({}),
|
|
53
|
+
getAssistantFeatureFlagDefaults: () => ({}),
|
|
54
|
+
_resetDefaultsCache: () => {},
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
mock.module('../config/skill-state.js', () => ({
|
|
58
|
+
skillFlagKey: (skillId: string) => `skills.${skillId}.enabled`,
|
|
38
59
|
}));
|
|
39
60
|
|
|
40
61
|
mock.module('../skills/active-skill-tools.js', () => {
|
|
@@ -184,6 +205,7 @@ mock.module('../util/logger.js', () => ({
|
|
|
184
205
|
debug: () => {},
|
|
185
206
|
error: () => {},
|
|
186
207
|
}),
|
|
208
|
+
isDebug: () => false,
|
|
187
209
|
}));
|
|
188
210
|
|
|
189
211
|
// ---------------------------------------------------------------------------
|
|
@@ -518,14 +518,14 @@ describe('bundled browser skill', () => {
|
|
|
518
518
|
expect(browserSkill!.disableModelInvocation).toBe(false);
|
|
519
519
|
});
|
|
520
520
|
|
|
521
|
-
test('browser skill has a valid tool manifest with
|
|
521
|
+
test('browser skill has a valid tool manifest with 14 tools', () => {
|
|
522
522
|
const catalog = loadSkillCatalog();
|
|
523
523
|
const browserSkill = catalog.find((s) => s.id === 'browser');
|
|
524
524
|
expect(browserSkill).toBeDefined();
|
|
525
525
|
expect(browserSkill!.toolManifest).toBeDefined();
|
|
526
526
|
expect(browserSkill!.toolManifest!.present).toBe(true);
|
|
527
527
|
expect(browserSkill!.toolManifest!.valid).toBe(true);
|
|
528
|
-
expect(browserSkill!.toolManifest!.toolCount).toBe(
|
|
528
|
+
expect(browserSkill!.toolManifest!.toolCount).toBe(14);
|
|
529
529
|
expect(browserSkill!.toolManifest!.toolNames).toEqual([
|
|
530
530
|
'browser_navigate',
|
|
531
531
|
'browser_snapshot',
|
|
@@ -534,8 +534,12 @@ describe('bundled browser skill', () => {
|
|
|
534
534
|
'browser_click',
|
|
535
535
|
'browser_type',
|
|
536
536
|
'browser_press_key',
|
|
537
|
+
'browser_scroll',
|
|
538
|
+
'browser_select_option',
|
|
539
|
+
'browser_hover',
|
|
537
540
|
'browser_wait_for',
|
|
538
541
|
'browser_extract',
|
|
542
|
+
'browser_wait_for_download',
|
|
539
543
|
'browser_fill_credential',
|
|
540
544
|
]);
|
|
541
545
|
});
|
|
@@ -618,10 +622,10 @@ describe('ingress-dependent setup skills declare public-ingress', () => {
|
|
|
618
622
|
expect(includes).toContain('public-ingress');
|
|
619
623
|
});
|
|
620
624
|
|
|
621
|
-
test('slack-oauth-setup includes
|
|
625
|
+
test('slack-oauth-setup includes browser', () => {
|
|
622
626
|
const includes = readSkillIncludes(VELLUM_SKILLS_DIR, 'slack-oauth-setup');
|
|
623
627
|
expect(includes).toBeDefined();
|
|
624
|
-
expect(includes).toContain('
|
|
628
|
+
expect(includes).toContain('browser');
|
|
625
629
|
});
|
|
626
630
|
});
|
|
627
631
|
|
|
@@ -7,7 +7,9 @@ import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
|
7
7
|
const testDir = mkdtempSync(join(tmpdir(), 'slack-channel-cfg-test-'));
|
|
8
8
|
|
|
9
9
|
mock.module('../config/loader.js', () => ({
|
|
10
|
-
getConfig: () => ({
|
|
10
|
+
getConfig: () => ({
|
|
11
|
+
ui: {},
|
|
12
|
+
}),
|
|
11
13
|
loadConfig: () => ({}),
|
|
12
14
|
loadRawConfig: () => ({}),
|
|
13
15
|
saveRawConfig: () => {},
|
|
@@ -6,6 +6,25 @@ import { describe, expect, mock, test } from 'bun:test';
|
|
|
6
6
|
// Mock conversation-store before importing tool executors that depend on it.
|
|
7
7
|
let mockGetMessages: (conversationId: string) => Array<{ role: string; content: string }> | null = () => null;
|
|
8
8
|
mock.module('../memory/conversation-store.js', () => ({
|
|
9
|
+
getConversationThreadType: () => 'default',
|
|
10
|
+
setConversationOriginChannelIfUnset: () => {},
|
|
11
|
+
updateConversationContextWindow: () => {},
|
|
12
|
+
deleteMessageById: () => {},
|
|
13
|
+
updateConversationTitle: () => {},
|
|
14
|
+
updateConversationUsage: () => {},
|
|
15
|
+
addMessage: () => ({ id: 'mock-msg-id' }),
|
|
16
|
+
getConversation: () => ({
|
|
17
|
+
id: 'conv-1',
|
|
18
|
+
contextSummary: null,
|
|
19
|
+
contextCompactedMessageCount: 0,
|
|
20
|
+
totalInputTokens: 0,
|
|
21
|
+
totalOutputTokens: 0,
|
|
22
|
+
totalEstimatedCost: 0,
|
|
23
|
+
title: null,
|
|
24
|
+
}),
|
|
25
|
+
provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
|
|
26
|
+
getConversationOriginInterface: () => null,
|
|
27
|
+
getConversationOriginChannel: () => null,
|
|
9
28
|
getMessages: (conversationId: string) => mockGetMessages(conversationId),
|
|
10
29
|
createConversation: () => ({ id: 'mock-conv' }),
|
|
11
30
|
}));
|
|
@@ -49,6 +49,8 @@ mock.module('../util/logger.js', () => ({
|
|
|
49
49
|
|
|
50
50
|
mock.module('../config/loader.js', () => ({
|
|
51
51
|
getConfig: () => ({
|
|
52
|
+
ui: {},
|
|
53
|
+
|
|
52
54
|
sandbox: { enabled: true },
|
|
53
55
|
}),
|
|
54
56
|
}));
|
|
@@ -203,7 +205,7 @@ describe('buildSystemPrompt', () => {
|
|
|
203
205
|
|
|
204
206
|
test('config section uses workspace directory from platform util', () => {
|
|
205
207
|
const result = buildSystemPrompt();
|
|
206
|
-
expect(result).toContain(`Your
|
|
208
|
+
expect(result).toContain(`Your configuration directory is \`${TEST_DIR}/\`.`);
|
|
207
209
|
});
|
|
208
210
|
|
|
209
211
|
test('omits user skills from catalog when none are configured', () => {
|
|
@@ -177,20 +177,21 @@ describe('terminal sandbox — macOS sandbox-exec behavior', () => {
|
|
|
177
177
|
expect(result.args.slice(2)).toEqual(['bash', '-c', '--', 'echo hello']);
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
-
test('
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
expect(
|
|
180
|
+
test('escapes SBPL metacharacters in working dirs instead of throwing', () => {
|
|
181
|
+
// The sandbox now escapes metacharacters rather than rejecting them
|
|
182
|
+
const result1 = wrapCommand('pwd', '/tmp/bad"dir', nativeConfig());
|
|
183
|
+
expect(result1.sandboxed).toBe(true);
|
|
184
|
+
const result2 = wrapCommand('pwd', '/tmp/bad(dir', nativeConfig());
|
|
185
|
+
expect(result2.sandboxed).toBe(true);
|
|
186
|
+
const result3 = wrapCommand('pwd', '/tmp/bad;dir', nativeConfig());
|
|
187
|
+
expect(result3.sandboxed).toBe(true);
|
|
184
188
|
});
|
|
185
189
|
|
|
186
|
-
test('SBPL
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
expect(err).toBeInstanceOf(ToolError);
|
|
192
|
-
expect((err as Error).message).toContain('SBPL metacharacters');
|
|
193
|
-
}
|
|
190
|
+
test('SBPL profile escapes metacharacters in working dir path', () => {
|
|
191
|
+
// Verify the sandbox profile is written with escaped chars
|
|
192
|
+
wrapCommand('pwd', '/tmp/bad"dir', nativeConfig());
|
|
193
|
+
const profileContent = writeFileSyncMock.mock.calls[0]?.[1] as string;
|
|
194
|
+
expect(profileContent).toContain('bad\\"dir');
|
|
194
195
|
});
|
|
195
196
|
});
|
|
196
197
|
|
|
@@ -100,7 +100,7 @@ function makeContext(overrides: Partial<ToolContext> = {}): ToolContext {
|
|
|
100
100
|
conversationId: 'conv-1',
|
|
101
101
|
assistantId: 'self',
|
|
102
102
|
requestId: 'req-1',
|
|
103
|
-
|
|
103
|
+
guardianTrustClass: 'trusted_contact',
|
|
104
104
|
...overrides,
|
|
105
105
|
};
|
|
106
106
|
}
|
|
@@ -134,7 +134,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
|
|
|
134
134
|
);
|
|
135
135
|
expect(mintResult.ok).toBe(true);
|
|
136
136
|
|
|
137
|
-
const context = makeContext({
|
|
137
|
+
const context = makeContext({ guardianTrustClass: 'trusted_contact' });
|
|
138
138
|
const result = await handler.checkPreExecutionGates(
|
|
139
139
|
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
140
140
|
);
|
|
@@ -149,7 +149,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
|
|
|
149
149
|
const toolName = 'bash';
|
|
150
150
|
const input = { command: 'rm -rf /' };
|
|
151
151
|
|
|
152
|
-
const context = makeContext({
|
|
152
|
+
const context = makeContext({ guardianTrustClass: 'trusted_contact' });
|
|
153
153
|
const result = await handler.checkPreExecutionGates(
|
|
154
154
|
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
155
155
|
);
|
|
@@ -177,7 +177,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
|
|
|
177
177
|
}),
|
|
178
178
|
);
|
|
179
179
|
|
|
180
|
-
const context = makeContext({
|
|
180
|
+
const context = makeContext({ guardianTrustClass: 'unknown' });
|
|
181
181
|
const result = await handler.checkPreExecutionGates(
|
|
182
182
|
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
183
183
|
);
|
|
@@ -189,7 +189,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
|
|
|
189
189
|
const toolName = 'bash';
|
|
190
190
|
const input = { command: 'deploy' };
|
|
191
191
|
|
|
192
|
-
const context = makeContext({
|
|
192
|
+
const context = makeContext({ guardianTrustClass: 'unknown' });
|
|
193
193
|
const result = await handler.checkPreExecutionGates(
|
|
194
194
|
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
195
195
|
);
|
|
@@ -212,7 +212,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
|
|
|
212
212
|
}),
|
|
213
213
|
);
|
|
214
214
|
|
|
215
|
-
const context = makeContext({
|
|
215
|
+
const context = makeContext({ guardianTrustClass: 'trusted_contact' });
|
|
216
216
|
|
|
217
217
|
// First invocation — should consume the grant and allow
|
|
218
218
|
const first = await handler.checkPreExecutionGates(
|
|
@@ -241,7 +241,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
|
|
|
241
241
|
}),
|
|
242
242
|
);
|
|
243
243
|
|
|
244
|
-
const context = makeContext({
|
|
244
|
+
const context = makeContext({ guardianTrustClass: 'trusted_contact' });
|
|
245
245
|
const result = await handler.checkPreExecutionGates(
|
|
246
246
|
toolName, invokeInput, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
247
247
|
);
|
|
@@ -264,7 +264,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
|
|
|
264
264
|
}),
|
|
265
265
|
);
|
|
266
266
|
|
|
267
|
-
const context = makeContext({
|
|
267
|
+
const context = makeContext({ guardianTrustClass: 'trusted_contact' });
|
|
268
268
|
const result = await handler.checkPreExecutionGates(
|
|
269
269
|
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
270
270
|
);
|
|
@@ -277,7 +277,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
|
|
|
277
277
|
const input = { command: 'deploy' };
|
|
278
278
|
|
|
279
279
|
// No grants minted at all
|
|
280
|
-
const context = makeContext({
|
|
280
|
+
const context = makeContext({ guardianTrustClass: 'guardian' });
|
|
281
281
|
const result = await handler.checkPreExecutionGates(
|
|
282
282
|
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
283
283
|
);
|
|
@@ -290,7 +290,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
|
|
|
290
290
|
const toolName = 'bash';
|
|
291
291
|
const input = { command: 'deploy' };
|
|
292
292
|
|
|
293
|
-
const context = makeContext({
|
|
293
|
+
const context = makeContext({ guardianTrustClass: undefined });
|
|
294
294
|
const result = await handler.checkPreExecutionGates(
|
|
295
295
|
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
296
296
|
);
|
|
@@ -309,7 +309,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
|
|
|
309
309
|
}),
|
|
310
310
|
);
|
|
311
311
|
|
|
312
|
-
const context = makeContext({
|
|
312
|
+
const context = makeContext({ guardianTrustClass: 'trusted_contact', requestId: 'req-1' });
|
|
313
313
|
const result = await handler.checkPreExecutionGates(
|
|
314
314
|
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
315
315
|
);
|
|
@@ -333,7 +333,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
|
|
|
333
333
|
|
|
334
334
|
// Context conversationId does not match the grant's conversationId
|
|
335
335
|
const context = makeContext({
|
|
336
|
-
|
|
336
|
+
guardianTrustClass: 'trusted_contact',
|
|
337
337
|
conversationId: 'conv-1',
|
|
338
338
|
});
|
|
339
339
|
const result = await handler.checkPreExecutionGates(
|
|
@@ -349,7 +349,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
|
|
|
349
349
|
|
|
350
350
|
// executionChannel defaults to undefined (non-voice)
|
|
351
351
|
const context = makeContext({
|
|
352
|
-
|
|
352
|
+
guardianTrustClass: 'trusted_contact',
|
|
353
353
|
executionChannel: 'telegram',
|
|
354
354
|
});
|
|
355
355
|
|
|
@@ -383,7 +383,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
|
|
|
383
383
|
}, 300);
|
|
384
384
|
|
|
385
385
|
const context = makeContext({
|
|
386
|
-
|
|
386
|
+
guardianTrustClass: 'trusted_contact',
|
|
387
387
|
executionChannel: 'voice',
|
|
388
388
|
});
|
|
389
389
|
|
|
@@ -408,7 +408,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
|
|
|
408
408
|
setTimeout(() => controller.abort(), 200);
|
|
409
409
|
|
|
410
410
|
const context = makeContext({
|
|
411
|
-
|
|
411
|
+
guardianTrustClass: 'trusted_contact',
|
|
412
412
|
executionChannel: 'voice',
|
|
413
413
|
signal: controller.signal,
|
|
414
414
|
});
|
|
@@ -59,6 +59,8 @@ mock.module('../permissions/trust-store.js', () => ({
|
|
|
59
59
|
|
|
60
60
|
mock.module('../config/loader.js', () => ({
|
|
61
61
|
getConfig: () => ({
|
|
62
|
+
ui: {},
|
|
63
|
+
|
|
62
64
|
provider: 'mock-provider',
|
|
63
65
|
timeouts: { permissionTimeoutSec: 5, toolExecutionTimeoutSec: 120 },
|
|
64
66
|
permissions: { mode: 'legacy' },
|
|
@@ -147,7 +147,7 @@ function makeContext(overrides: Partial<ToolContext> = {}): ToolContext {
|
|
|
147
147
|
conversationId: 'conv-1',
|
|
148
148
|
assistantId: 'self',
|
|
149
149
|
requestId: 'req-1',
|
|
150
|
-
|
|
150
|
+
guardianTrustClass: 'trusted_contact',
|
|
151
151
|
executionChannel: 'telegram',
|
|
152
152
|
requesterExternalUserId: 'requester-1',
|
|
153
153
|
...overrides,
|
|
@@ -204,7 +204,7 @@ describe('ToolApprovalHandler / grant-miss escalation', () => {
|
|
|
204
204
|
const toolName = 'bash';
|
|
205
205
|
const input = { command: 'cat /etc/passwd' };
|
|
206
206
|
|
|
207
|
-
const context = makeContext({
|
|
207
|
+
const context = makeContext({ guardianTrustClass: 'trusted_contact' });
|
|
208
208
|
const result = await handler.checkPreExecutionGates(
|
|
209
209
|
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
210
210
|
);
|
|
@@ -231,7 +231,7 @@ describe('ToolApprovalHandler / grant-miss escalation', () => {
|
|
|
231
231
|
const toolName = 'bash';
|
|
232
232
|
const input = { command: 'deploy' };
|
|
233
233
|
|
|
234
|
-
const context = makeContext({
|
|
234
|
+
const context = makeContext({ guardianTrustClass: 'trusted_contact' });
|
|
235
235
|
const result = await handler.checkPreExecutionGates(
|
|
236
236
|
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
237
237
|
);
|
|
@@ -247,7 +247,7 @@ describe('ToolApprovalHandler / grant-miss escalation', () => {
|
|
|
247
247
|
const toolName = 'bash';
|
|
248
248
|
const input = { command: 'rm -rf /' };
|
|
249
249
|
|
|
250
|
-
const context = makeContext({
|
|
250
|
+
const context = makeContext({ guardianTrustClass: 'trusted_contact' });
|
|
251
251
|
|
|
252
252
|
// First invocation creates the request
|
|
253
253
|
await handler.checkPreExecutionGates(
|
|
@@ -288,7 +288,7 @@ describe('ToolApprovalHandler / grant-miss escalation', () => {
|
|
|
288
288
|
const input = { command: 'ls' };
|
|
289
289
|
|
|
290
290
|
const context = makeContext({
|
|
291
|
-
|
|
291
|
+
guardianTrustClass: 'unknown',
|
|
292
292
|
executionChannel: 'telegram',
|
|
293
293
|
requesterExternalUserId: 'unknown-user',
|
|
294
294
|
});
|
|
@@ -314,7 +314,7 @@ describe('ToolApprovalHandler / grant-miss escalation', () => {
|
|
|
314
314
|
const input = { command: 'deploy' };
|
|
315
315
|
|
|
316
316
|
const context = makeContext({
|
|
317
|
-
|
|
317
|
+
guardianTrustClass: 'trusted_contact',
|
|
318
318
|
executionChannel: undefined, // no channel info
|
|
319
319
|
});
|
|
320
320
|
const result = await handler.checkPreExecutionGates(
|
|
@@ -449,7 +449,7 @@ describe('end-to-end: tool grant escalation -> approval -> consume', () => {
|
|
|
449
449
|
const input = { command: 'echo secret' };
|
|
450
450
|
const _inputDigest = computeToolApprovalDigest(toolName, input);
|
|
451
451
|
|
|
452
|
-
const context = makeContext({
|
|
452
|
+
const context = makeContext({ guardianTrustClass: 'trusted_contact' });
|
|
453
453
|
|
|
454
454
|
// Step 1: First invocation is denied, but a tool_grant_request is created
|
|
455
455
|
const firstResult = await handler.checkPreExecutionGates(
|
|
@@ -419,6 +419,54 @@ describe('trusted contact activated notification signal', () => {
|
|
|
419
419
|
expect(hints.urgency).toBe('low');
|
|
420
420
|
});
|
|
421
421
|
|
|
422
|
+
test('re-verification preserves an existing guardian-managed member display name', async () => {
|
|
423
|
+
createBinding({
|
|
424
|
+
assistantId: 'self',
|
|
425
|
+
channel: 'telegram',
|
|
426
|
+
guardianExternalUserId: 'guardian-user-789',
|
|
427
|
+
guardianDeliveryChatId: 'guardian-chat-789',
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
upsertMember({
|
|
431
|
+
assistantId: 'self',
|
|
432
|
+
sourceChannel: 'telegram',
|
|
433
|
+
externalUserId: 'requester-user-456',
|
|
434
|
+
externalChatId: 'chat-123',
|
|
435
|
+
status: 'revoked',
|
|
436
|
+
policy: 'allow',
|
|
437
|
+
displayName: 'Jeff',
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const session = createOutboundSession({
|
|
441
|
+
assistantId: 'self',
|
|
442
|
+
channel: 'telegram',
|
|
443
|
+
expectedExternalUserId: 'requester-user-456',
|
|
444
|
+
expectedChatId: 'chat-123',
|
|
445
|
+
identityBindingStatus: 'bound',
|
|
446
|
+
destinationAddress: 'chat-123',
|
|
447
|
+
verificationPurpose: 'trusted_contact',
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const verifyReq = buildInboundRequest({
|
|
451
|
+
content: session.secret,
|
|
452
|
+
externalChatId: 'chat-123',
|
|
453
|
+
senderExternalUserId: 'requester-user-456',
|
|
454
|
+
senderName: 'Noa Flaherty',
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
await handleChannelInbound(verifyReq, undefined, TEST_BEARER_TOKEN);
|
|
458
|
+
|
|
459
|
+
const member = findMember({
|
|
460
|
+
assistantId: 'self',
|
|
461
|
+
sourceChannel: 'telegram',
|
|
462
|
+
externalUserId: 'requester-user-456',
|
|
463
|
+
externalChatId: 'chat-123',
|
|
464
|
+
});
|
|
465
|
+
expect(member).not.toBeNull();
|
|
466
|
+
expect(member!.status).toBe('active');
|
|
467
|
+
expect(member!.displayName).toBe('Jeff');
|
|
468
|
+
});
|
|
469
|
+
|
|
422
470
|
test('guardian verification does NOT emit activated signal', async () => {
|
|
423
471
|
// Create an inbound challenge (guardian flow, not trusted contact)
|
|
424
472
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -59,6 +59,19 @@ mock.module('../notifications/emit-signal.js', () => ({
|
|
|
59
59
|
deliveryResults: [],
|
|
60
60
|
};
|
|
61
61
|
},
|
|
62
|
+
registerBroadcastFn: () => {},
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
// Mock access-request-helper directly to capture notification calls.
|
|
66
|
+
// Bun's mock.module does not intercept transitive imports reliably, so
|
|
67
|
+
// mocking emit-signal.js alone is not sufficient — access-request-helper
|
|
68
|
+
// imports emit-signal before the mock takes effect.
|
|
69
|
+
const notifyGuardianCalls: Array<Record<string, unknown>> = [];
|
|
70
|
+
mock.module('../runtime/access-request-helper.js', () => ({
|
|
71
|
+
notifyGuardianOfAccessRequest: (params: Record<string, unknown>) => {
|
|
72
|
+
notifyGuardianCalls.push(params);
|
|
73
|
+
return { notified: true, created: true, requestId: `mock-req-${Date.now()}` };
|
|
74
|
+
},
|
|
62
75
|
}));
|
|
63
76
|
|
|
64
77
|
const deliverReplyCalls: Array<{ url: string; payload: Record<string, unknown> }> = [];
|
|
@@ -75,7 +88,6 @@ mock.module('../runtime/approval-message-composer.js', () => ({
|
|
|
75
88
|
|
|
76
89
|
import {
|
|
77
90
|
createBinding,
|
|
78
|
-
findPendingAccessRequestForRequester,
|
|
79
91
|
} from '../memory/channel-guardian-store.js';
|
|
80
92
|
import { getDb, initializeDb, resetDb } from '../memory/db.js';
|
|
81
93
|
import { findMember, upsertMember } from '../memory/ingress-member-store.js';
|
|
@@ -109,6 +121,7 @@ function resetState(): void {
|
|
|
109
121
|
db.run('DELETE FROM notification_events');
|
|
110
122
|
db.run('DELETE FROM assistant_ingress_members');
|
|
111
123
|
emitSignalCalls.length = 0;
|
|
124
|
+
notifyGuardianCalls.length = 0;
|
|
112
125
|
deliverReplyCalls.length = 0;
|
|
113
126
|
}
|
|
114
127
|
|
|
@@ -186,7 +199,10 @@ for (const config of CHANNEL_CONFIGS) {
|
|
|
186
199
|
expect(json.denied).toBe(true);
|
|
187
200
|
expect(json.reason).toBe('not_a_member');
|
|
188
201
|
expect(deliverReplyCalls.length).toBe(1);
|
|
189
|
-
|
|
202
|
+
const replyText = (deliverReplyCalls[0].payload as Record<string, unknown>).text as string;
|
|
203
|
+
expect(
|
|
204
|
+
replyText.includes("you haven't been approved") || replyText.includes("you don't have access"),
|
|
205
|
+
).toBe(true);
|
|
190
206
|
});
|
|
191
207
|
|
|
192
208
|
test('guardian is notified when a non-member messages', async () => {
|
|
@@ -203,23 +219,10 @@ for (const config of CHANNEL_CONFIGS) {
|
|
|
203
219
|
|
|
204
220
|
expect(json.denied).toBe(true);
|
|
205
221
|
|
|
206
|
-
//
|
|
207
|
-
expect(
|
|
208
|
-
expect(
|
|
209
|
-
expect(
|
|
210
|
-
|
|
211
|
-
const payload = emitSignalCalls[0].contextPayload as Record<string, unknown>;
|
|
212
|
-
expect(payload.senderExternalUserId).toBe(config.senderExternalUserId);
|
|
213
|
-
|
|
214
|
-
// Approval request was created for the correct channel
|
|
215
|
-
const pending = findPendingAccessRequestForRequester(
|
|
216
|
-
'self',
|
|
217
|
-
config.channel,
|
|
218
|
-
config.senderExternalUserId,
|
|
219
|
-
'ingress_access_request',
|
|
220
|
-
);
|
|
221
|
-
expect(pending).not.toBeNull();
|
|
222
|
-
expect(pending!.channel).toBe(config.channel);
|
|
222
|
+
// Guardian notification helper was called for the correct channel
|
|
223
|
+
expect(notifyGuardianCalls.length).toBe(1);
|
|
224
|
+
expect(notifyGuardianCalls[0].sourceChannel).toBe(config.channel);
|
|
225
|
+
expect(notifyGuardianCalls[0].senderExternalUserId).toBe(config.senderExternalUserId);
|
|
223
226
|
});
|
|
224
227
|
|
|
225
228
|
test('verification creates active member for channel', () => {
|