@vellumai/assistant 0.3.18 → 0.3.20
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 +155 -15
- package/Dockerfile +1 -0
- package/README.md +40 -4
- package/docs/architecture/integrations.md +7 -11
- package/docs/architecture/security.md +80 -0
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -0
- package/src/__tests__/approval-primitive.test.ts +540 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
- package/src/__tests__/call-controller.test.ts +605 -104
- package/src/__tests__/channel-invite-transport.test.ts +264 -0
- package/src/__tests__/checker.test.ts +60 -0
- package/src/__tests__/cli.test.ts +42 -1
- package/src/__tests__/config-schema.test.ts +11 -127
- package/src/__tests__/config-watcher.test.ts +0 -8
- package/src/__tests__/daemon-lifecycle.test.ts +1 -0
- package/src/__tests__/daemon-server-session-init.test.ts +8 -2
- package/src/__tests__/diff.test.ts +22 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +779 -0
- package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
- package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
- package/src/__tests__/guardian-dispatch.test.ts +185 -1
- package/src/__tests__/guardian-grant-minting.test.ts +532 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
- package/src/__tests__/invite-redemption-service.test.ts +306 -0
- package/src/__tests__/ipc-snapshot.test.ts +58 -0
- package/src/__tests__/notification-decision-fallback.test.ts +88 -0
- package/src/__tests__/remote-skill-policy.test.ts +215 -0
- package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
- package/src/__tests__/sandbox-host-parity.test.ts +6 -13
- package/src/__tests__/scoped-approval-grants.test.ts +521 -0
- package/src/__tests__/scoped-grant-security-matrix.test.ts +444 -0
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
- package/src/__tests__/session-load-history-repair.test.ts +169 -2
- package/src/__tests__/session-runtime-assembly.test.ts +33 -5
- package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
- package/src/__tests__/skill-feature-flags.test.ts +188 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
- package/src/__tests__/skill-mirror-parity.test.ts +1 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
- package/src/__tests__/system-prompt.test.ts +1 -1
- package/src/__tests__/terminal-sandbox.test.ts +142 -9
- package/src/__tests__/terminal-tools.test.ts +2 -93
- package/src/__tests__/thread-seed-composer.test.ts +18 -0
- package/src/__tests__/tool-approval-handler.test.ts +350 -0
- package/src/__tests__/trust-store.test.ts +2 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +533 -0
- package/src/agent/loop.ts +36 -1
- package/src/approvals/approval-primitive.ts +381 -0
- package/src/approvals/guardian-decision-primitive.ts +191 -0
- package/src/calls/call-controller.ts +276 -212
- package/src/calls/call-domain.ts +56 -6
- package/src/calls/guardian-dispatch.ts +56 -0
- package/src/calls/relay-server.ts +13 -0
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +59 -4
- package/src/cli/core-commands.ts +0 -4
- package/src/cli.ts +76 -34
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
- package/src/config/assistant-feature-flags.ts +162 -0
- package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
- package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
- package/src/config/bundled-skills/notifications/SKILL.md +18 -0
- package/src/config/bundled-skills/reminder/SKILL.md +49 -2
- package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
- package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
- package/src/config/core-schema.ts +1 -1
- package/src/config/env-registry.ts +10 -0
- package/src/config/feature-flag-registry.json +61 -0
- package/src/config/loader.ts +22 -1
- package/src/config/sandbox-schema.ts +0 -39
- package/src/config/schema.ts +12 -2
- package/src/config/skill-state.ts +34 -0
- package/src/config/skills-schema.ts +26 -0
- package/src/config/skills.ts +9 -0
- package/src/config/system-prompt.ts +110 -46
- package/src/config/templates/SOUL.md +1 -1
- package/src/config/types.ts +19 -1
- package/src/config/vellum-skills/catalog.json +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
- package/src/config/vellum-skills/telegram-setup/SKILL.md +1 -1
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +104 -3
- package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
- package/src/daemon/config-watcher.ts +0 -1
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/guardian-invite-intent.ts +124 -0
- package/src/daemon/handlers/avatar.ts +68 -0
- package/src/daemon/handlers/browser.ts +2 -2
- package/src/daemon/handlers/config-channels.ts +18 -0
- package/src/daemon/handlers/guardian-actions.ts +120 -0
- package/src/daemon/handlers/index.ts +4 -0
- package/src/daemon/handlers/sessions.ts +19 -0
- package/src/daemon/handlers/shared.ts +3 -1
- package/src/daemon/handlers/skills.ts +45 -2
- package/src/daemon/install-cli-launchers.ts +58 -13
- package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -2
- package/src/daemon/ipc-contract/settings.ts +25 -2
- package/src/daemon/ipc-contract/skills.ts +1 -0
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +4 -0
- package/src/daemon/lifecycle.ts +6 -2
- package/src/daemon/main.ts +1 -0
- package/src/daemon/server.ts +1 -0
- package/src/daemon/session-lifecycle.ts +52 -7
- package/src/daemon/session-memory.ts +45 -0
- package/src/daemon/session-process.ts +260 -422
- package/src/daemon/session-runtime-assembly.ts +12 -0
- package/src/daemon/session-skill-tools.ts +14 -1
- package/src/daemon/session-tool-setup.ts +5 -0
- package/src/daemon/session.ts +11 -0
- package/src/daemon/tool-side-effects.ts +35 -9
- package/src/index.ts +0 -2
- package/src/memory/conversation-display-order-migration.ts +44 -0
- package/src/memory/conversation-queries.ts +2 -0
- package/src/memory/conversation-store.ts +91 -0
- package/src/memory/db-init.ts +13 -1
- package/src/memory/embedding-local.ts +22 -8
- package/src/memory/guardian-action-store.ts +133 -2
- package/src/memory/guardian-verification.ts +1 -1
- package/src/memory/ingress-invite-store.ts +95 -1
- package/src/memory/migrations/033-scoped-approval-grants.ts +51 -0
- package/src/memory/migrations/034-guardian-action-tool-metadata.ts +12 -0
- package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/schema.ts +35 -1
- package/src/memory/scoped-approval-grants.ts +518 -0
- package/src/messaging/providers/slack/client.ts +12 -0
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/notifications/decision-engine.ts +49 -12
- package/src/notifications/emit-signal.ts +7 -0
- package/src/notifications/signal.ts +7 -0
- package/src/notifications/thread-seed-composer.ts +2 -1
- package/src/permissions/checker.ts +27 -0
- package/src/runtime/channel-approval-types.ts +16 -6
- package/src/runtime/channel-approvals.ts +19 -15
- package/src/runtime/channel-invite-transport.ts +85 -0
- package/src/runtime/channel-invite-transports/telegram.ts +105 -0
- package/src/runtime/guardian-action-grant-minter.ts +154 -0
- package/src/runtime/guardian-action-message-composer.ts +30 -0
- package/src/runtime/guardian-decision-types.ts +91 -0
- package/src/runtime/http-server.ts +23 -1
- package/src/runtime/ingress-service.ts +22 -0
- package/src/runtime/invite-redemption-service.ts +181 -0
- package/src/runtime/invite-redemption-templates.ts +39 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/guardian-action-routes.ts +206 -0
- package/src/runtime/routes/guardian-approval-interception.ts +66 -74
- package/src/runtime/routes/inbound-message-handler.ts +568 -409
- package/src/runtime/routes/pairing-routes.ts +4 -0
- package/src/security/encrypted-store.ts +31 -17
- package/src/security/keychain.ts +176 -2
- package/src/security/secure-keys.ts +97 -0
- package/src/security/tool-approval-digest.ts +67 -0
- package/src/skills/remote-skill-policy.ts +131 -0
- package/src/tools/browser/browser-execution.ts +2 -2
- package/src/tools/browser/browser-manager.ts +46 -32
- package/src/tools/browser/browser-screencast.ts +2 -2
- package/src/tools/calls/call-start.ts +1 -1
- package/src/tools/executor.ts +22 -17
- package/src/tools/network/script-proxy/session-manager.ts +1 -5
- package/src/tools/skills/load.ts +22 -8
- package/src/tools/system/avatar-generator.ts +119 -0
- package/src/tools/system/navigate-settings.ts +65 -0
- package/src/tools/system/open-system-settings.ts +75 -0
- package/src/tools/system/voice-config.ts +121 -32
- package/src/tools/terminal/backends/native.ts +40 -19
- package/src/tools/terminal/backends/types.ts +3 -3
- package/src/tools/terminal/parser.ts +1 -1
- package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
- package/src/tools/terminal/sandbox.ts +1 -12
- package/src/tools/terminal/shell.ts +3 -31
- package/src/tools/tool-approval-handler.ts +141 -3
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +6 -0
- package/src/util/diff.ts +36 -13
- package/Dockerfile.sandbox +0 -5
- package/src/__tests__/doordash-client.test.ts +0 -187
- package/src/__tests__/doordash-session.test.ts +0 -154
- package/src/__tests__/signup-e2e.test.ts +0 -354
- package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
- package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
- package/src/cli/doordash.ts +0 -1057
- package/src/config/bundled-skills/doordash/SKILL.md +0 -163
- package/src/config/templates/LOOKS.md +0 -25
- package/src/doordash/cart-queries.ts +0 -787
- package/src/doordash/client.ts +0 -1016
- package/src/doordash/order-queries.ts +0 -85
- package/src/doordash/queries.ts +0 -13
- package/src/doordash/query-extractor.ts +0 -94
- package/src/doordash/search-queries.ts +0 -203
- package/src/doordash/session.ts +0 -84
- package/src/doordash/store-queries.ts +0 -246
- package/src/doordash/types.ts +0 -367
- package/src/tools/terminal/backends/docker.ts +0 -379
|
@@ -175,6 +175,24 @@ describe('composeThreadSeed', () => {
|
|
|
175
175
|
expect(seed).toContain('Action required');
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
+
test('does not duplicate "Action required" when copy already includes it', () => {
|
|
179
|
+
const signal = makeSignal({
|
|
180
|
+
attentionHints: {
|
|
181
|
+
requiresAction: true,
|
|
182
|
+
urgency: 'high',
|
|
183
|
+
isAsyncBackground: false,
|
|
184
|
+
visibleInSourceNow: false,
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
const copy = makeCopy({
|
|
188
|
+
title: 'Guardian Question',
|
|
189
|
+
body: 'Action required: What is the gate code?',
|
|
190
|
+
});
|
|
191
|
+
const seed = composeThreadSeed(signal, 'vellum' as NotificationChannel, copy);
|
|
192
|
+
const markerCount = (seed.match(/action required/gi) ?? []).length;
|
|
193
|
+
expect(markerCount).toBe(1);
|
|
194
|
+
});
|
|
195
|
+
|
|
178
196
|
test('omits "Notification" generic title', () => {
|
|
179
197
|
const signal = makeSignal();
|
|
180
198
|
const copy = makeCopy({ title: 'Notification', body: 'Something new.' });
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
6
|
+
|
|
7
|
+
const testDir = mkdtempSync(join(tmpdir(), 'tool-approval-handler-test-'));
|
|
8
|
+
|
|
9
|
+
mock.module('../util/platform.js', () => ({
|
|
10
|
+
getDataDir: () => testDir,
|
|
11
|
+
isMacOS: () => process.platform === 'darwin',
|
|
12
|
+
isLinux: () => process.platform === 'linux',
|
|
13
|
+
isWindows: () => process.platform === 'win32',
|
|
14
|
+
getSocketPath: () => join(testDir, 'test.sock'),
|
|
15
|
+
getPidPath: () => join(testDir, 'test.pid'),
|
|
16
|
+
getDbPath: () => join(testDir, 'test.db'),
|
|
17
|
+
getLogPath: () => join(testDir, 'test.log'),
|
|
18
|
+
ensureDataDir: () => {},
|
|
19
|
+
migrateToDataLayout: () => {},
|
|
20
|
+
migrateToWorkspaceLayout: () => {},
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
mock.module('../util/logger.js', () => ({
|
|
24
|
+
getLogger: () =>
|
|
25
|
+
new Proxy({} as Record<string, unknown>, {
|
|
26
|
+
get: () => () => {},
|
|
27
|
+
}),
|
|
28
|
+
isDebug: () => false,
|
|
29
|
+
truncateForLog: (value: string) => value,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
// Mock parental controls — no tools blocked by default
|
|
33
|
+
mock.module('../security/parental-control-store.js', () => ({
|
|
34
|
+
isToolBlocked: () => false,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
// Mock guardian control-plane policy — not targeting control-plane by default
|
|
38
|
+
mock.module('../tools/guardian-control-plane-policy.js', () => ({
|
|
39
|
+
enforceGuardianOnlyPolicy: () => ({ denied: false }),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
// Mock task run rules — no task run rules by default
|
|
43
|
+
mock.module('../tasks/ephemeral-permissions.js', () => ({
|
|
44
|
+
getTaskRunRules: () => [],
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
// Mock tool registry — return a fake tool for 'bash'
|
|
48
|
+
const fakeTool = {
|
|
49
|
+
name: 'bash',
|
|
50
|
+
description: 'Run a shell command',
|
|
51
|
+
category: 'shell',
|
|
52
|
+
defaultRiskLevel: 'high',
|
|
53
|
+
getDefinition: () => ({ name: 'bash', description: 'Run a shell command', input_schema: {} }),
|
|
54
|
+
execute: async () => ({ content: 'ok', isError: false }),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
mock.module('../tools/registry.js', () => ({
|
|
58
|
+
getTool: (name: string) => (name === 'bash' ? fakeTool : undefined),
|
|
59
|
+
getAllTools: () => [fakeTool],
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
import { mintGrantFromDecision, type MintGrantParams } from '../approvals/approval-primitive.js';
|
|
63
|
+
import { getDb, initializeDb, resetDb } from '../memory/db.js';
|
|
64
|
+
import { scopedApprovalGrants } from '../memory/schema.js';
|
|
65
|
+
import { computeToolApprovalDigest } from '../security/tool-approval-digest.js';
|
|
66
|
+
import { ToolApprovalHandler } from '../tools/tool-approval-handler.js';
|
|
67
|
+
import type { ToolContext, ToolLifecycleEvent } from '../tools/types.js';
|
|
68
|
+
|
|
69
|
+
initializeDb();
|
|
70
|
+
|
|
71
|
+
function clearTables(): void {
|
|
72
|
+
const db = getDb();
|
|
73
|
+
db.delete(scopedApprovalGrants).run();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
afterAll(() => {
|
|
77
|
+
resetDb();
|
|
78
|
+
try {
|
|
79
|
+
rmSync(testDir, { recursive: true });
|
|
80
|
+
} catch {
|
|
81
|
+
/* best effort */
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Helpers
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
function mintParams(overrides: Partial<MintGrantParams> = {}): MintGrantParams {
|
|
90
|
+
const futureExpiry = new Date(Date.now() + 60_000).toISOString();
|
|
91
|
+
return {
|
|
92
|
+
assistantId: 'self',
|
|
93
|
+
scopeMode: 'tool_signature',
|
|
94
|
+
requestChannel: 'telegram',
|
|
95
|
+
decisionChannel: 'telegram',
|
|
96
|
+
expiresAt: futureExpiry,
|
|
97
|
+
...overrides,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function makeContext(overrides: Partial<ToolContext> = {}): ToolContext {
|
|
102
|
+
return {
|
|
103
|
+
workingDir: testDir,
|
|
104
|
+
sessionId: 'session-1',
|
|
105
|
+
conversationId: 'conv-1',
|
|
106
|
+
assistantId: 'self',
|
|
107
|
+
requestId: 'req-1',
|
|
108
|
+
guardianActorRole: 'non-guardian',
|
|
109
|
+
...overrides,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ===========================================================================
|
|
114
|
+
// TESTS
|
|
115
|
+
// ===========================================================================
|
|
116
|
+
|
|
117
|
+
describe('ToolApprovalHandler / pre-exec gate grant check', () => {
|
|
118
|
+
const handler = new ToolApprovalHandler();
|
|
119
|
+
const events: ToolLifecycleEvent[] = [];
|
|
120
|
+
const emitLifecycleEvent = (event: ToolLifecycleEvent) => { events.push(event); };
|
|
121
|
+
|
|
122
|
+
beforeEach(() => {
|
|
123
|
+
clearTables();
|
|
124
|
+
events.length = 0;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('untrusted actor + matching tool_signature grant -> allow', async () => {
|
|
128
|
+
const toolName = 'bash';
|
|
129
|
+
const input = { command: 'ls -la' };
|
|
130
|
+
const digest = computeToolApprovalDigest(toolName, input);
|
|
131
|
+
|
|
132
|
+
// Mint a grant that matches the invocation
|
|
133
|
+
const mintResult = mintGrantFromDecision(
|
|
134
|
+
mintParams({
|
|
135
|
+
scopeMode: 'tool_signature',
|
|
136
|
+
toolName,
|
|
137
|
+
inputDigest: digest,
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
expect(mintResult.ok).toBe(true);
|
|
141
|
+
|
|
142
|
+
const context = makeContext({ guardianActorRole: 'non-guardian' });
|
|
143
|
+
const result = await handler.checkPreExecutionGates(
|
|
144
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
expect(result.allowed).toBe(true);
|
|
148
|
+
// No permission_denied events should have been emitted
|
|
149
|
+
const deniedEvents = events.filter((e) => e.type === 'permission_denied');
|
|
150
|
+
expect(deniedEvents.length).toBe(0);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('untrusted actor + no matching grant -> deny with guardian_approval_required', async () => {
|
|
154
|
+
const toolName = 'bash';
|
|
155
|
+
const input = { command: 'rm -rf /' };
|
|
156
|
+
|
|
157
|
+
const context = makeContext({ guardianActorRole: 'non-guardian' });
|
|
158
|
+
const result = await handler.checkPreExecutionGates(
|
|
159
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
expect(result.allowed).toBe(false);
|
|
163
|
+
if (result.allowed) return;
|
|
164
|
+
expect(result.result.isError).toBe(true);
|
|
165
|
+
expect(result.result.content).toContain('guardian approval');
|
|
166
|
+
|
|
167
|
+
// A permission_denied event should have been emitted
|
|
168
|
+
const deniedEvents = events.filter((e) => e.type === 'permission_denied');
|
|
169
|
+
expect(deniedEvents.length).toBe(1);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('unverified_channel actor + matching grant -> allow', async () => {
|
|
173
|
+
const toolName = 'bash';
|
|
174
|
+
const input = { command: 'echo hello' };
|
|
175
|
+
const digest = computeToolApprovalDigest(toolName, input);
|
|
176
|
+
|
|
177
|
+
mintGrantFromDecision(
|
|
178
|
+
mintParams({
|
|
179
|
+
scopeMode: 'tool_signature',
|
|
180
|
+
toolName,
|
|
181
|
+
inputDigest: digest,
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const context = makeContext({ guardianActorRole: 'unverified_channel' });
|
|
186
|
+
const result = await handler.checkPreExecutionGates(
|
|
187
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
expect(result.allowed).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('unverified_channel actor + no grant -> deny', async () => {
|
|
194
|
+
const toolName = 'bash';
|
|
195
|
+
const input = { command: 'deploy' };
|
|
196
|
+
|
|
197
|
+
const context = makeContext({ guardianActorRole: 'unverified_channel' });
|
|
198
|
+
const result = await handler.checkPreExecutionGates(
|
|
199
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
expect(result.allowed).toBe(false);
|
|
203
|
+
if (result.allowed) return;
|
|
204
|
+
expect(result.result.content).toContain('verified channel identity');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('grant is one-time: second invocation with same input denied', async () => {
|
|
208
|
+
const toolName = 'bash';
|
|
209
|
+
const input = { command: 'ls' };
|
|
210
|
+
const digest = computeToolApprovalDigest(toolName, input);
|
|
211
|
+
|
|
212
|
+
mintGrantFromDecision(
|
|
213
|
+
mintParams({
|
|
214
|
+
scopeMode: 'tool_signature',
|
|
215
|
+
toolName,
|
|
216
|
+
inputDigest: digest,
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const context = makeContext({ guardianActorRole: 'non-guardian' });
|
|
221
|
+
|
|
222
|
+
// First invocation — should consume the grant and allow
|
|
223
|
+
const first = await handler.checkPreExecutionGates(
|
|
224
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
225
|
+
);
|
|
226
|
+
expect(first.allowed).toBe(true);
|
|
227
|
+
|
|
228
|
+
// Second invocation — grant already consumed, should deny
|
|
229
|
+
const second = await handler.checkPreExecutionGates(
|
|
230
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
231
|
+
);
|
|
232
|
+
expect(second.allowed).toBe(false);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('grant with mismatched input digest -> deny', async () => {
|
|
236
|
+
const toolName = 'bash';
|
|
237
|
+
const grantInput = { command: 'ls' };
|
|
238
|
+
const invokeInput = { command: 'rm -rf /' };
|
|
239
|
+
const grantDigest = computeToolApprovalDigest(toolName, grantInput);
|
|
240
|
+
|
|
241
|
+
mintGrantFromDecision(
|
|
242
|
+
mintParams({
|
|
243
|
+
scopeMode: 'tool_signature',
|
|
244
|
+
toolName,
|
|
245
|
+
inputDigest: grantDigest,
|
|
246
|
+
}),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const context = makeContext({ guardianActorRole: 'non-guardian' });
|
|
250
|
+
const result = await handler.checkPreExecutionGates(
|
|
251
|
+
toolName, invokeInput, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
expect(result.allowed).toBe(false);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('expired grant -> deny', async () => {
|
|
258
|
+
const toolName = 'bash';
|
|
259
|
+
const input = { command: 'ls' };
|
|
260
|
+
const digest = computeToolApprovalDigest(toolName, input);
|
|
261
|
+
const pastExpiry = new Date(Date.now() - 60_000).toISOString();
|
|
262
|
+
|
|
263
|
+
mintGrantFromDecision(
|
|
264
|
+
mintParams({
|
|
265
|
+
scopeMode: 'tool_signature',
|
|
266
|
+
toolName,
|
|
267
|
+
inputDigest: digest,
|
|
268
|
+
expiresAt: pastExpiry,
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const context = makeContext({ guardianActorRole: 'non-guardian' });
|
|
273
|
+
const result = await handler.checkPreExecutionGates(
|
|
274
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
expect(result.allowed).toBe(false);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('guardian actor bypasses grant check entirely (no grant needed)', async () => {
|
|
281
|
+
const toolName = 'bash';
|
|
282
|
+
const input = { command: 'deploy' };
|
|
283
|
+
|
|
284
|
+
// No grants minted at all
|
|
285
|
+
const context = makeContext({ guardianActorRole: 'guardian' });
|
|
286
|
+
const result = await handler.checkPreExecutionGates(
|
|
287
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// Guardian should pass through — the untrusted gate is not triggered
|
|
291
|
+
expect(result.allowed).toBe(true);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('undefined actor role (desktop/trusted) bypasses grant check', async () => {
|
|
295
|
+
const toolName = 'bash';
|
|
296
|
+
const input = { command: 'deploy' };
|
|
297
|
+
|
|
298
|
+
const context = makeContext({ guardianActorRole: undefined });
|
|
299
|
+
const result = await handler.checkPreExecutionGates(
|
|
300
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
expect(result.allowed).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test('grant with matching request_id scope -> allow', async () => {
|
|
307
|
+
const toolName = 'bash';
|
|
308
|
+
const input = { command: 'ls' };
|
|
309
|
+
|
|
310
|
+
mintGrantFromDecision(
|
|
311
|
+
mintParams({
|
|
312
|
+
scopeMode: 'request_id',
|
|
313
|
+
requestId: 'req-1',
|
|
314
|
+
}),
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const context = makeContext({ guardianActorRole: 'non-guardian', requestId: 'req-1' });
|
|
318
|
+
const result = await handler.checkPreExecutionGates(
|
|
319
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
expect(result.allowed).toBe(true);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('grant with context fields (conversationId) must match', async () => {
|
|
326
|
+
const toolName = 'bash';
|
|
327
|
+
const input = { command: 'ls' };
|
|
328
|
+
const digest = computeToolApprovalDigest(toolName, input);
|
|
329
|
+
|
|
330
|
+
mintGrantFromDecision(
|
|
331
|
+
mintParams({
|
|
332
|
+
scopeMode: 'tool_signature',
|
|
333
|
+
toolName,
|
|
334
|
+
inputDigest: digest,
|
|
335
|
+
conversationId: 'conv-other',
|
|
336
|
+
}),
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// Context conversationId does not match the grant's conversationId
|
|
340
|
+
const context = makeContext({
|
|
341
|
+
guardianActorRole: 'non-guardian',
|
|
342
|
+
conversationId: 'conv-1',
|
|
343
|
+
});
|
|
344
|
+
const result = await handler.checkPreExecutionGates(
|
|
345
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
expect(result.allowed).toBe(false);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
@@ -838,6 +838,7 @@ describe('Trust Store', () => {
|
|
|
838
838
|
expect(match).not.toBeNull();
|
|
839
839
|
expect(match!.id).toBe('default:allow-bash-rm-bootstrap');
|
|
840
840
|
expect(match!.decision).toBe('allow');
|
|
841
|
+
expect(match!.allowHighRisk).toBe(true);
|
|
841
842
|
// Outside workspace, the bootstrap rule doesn't match — the global
|
|
842
843
|
// default:allow-bash-global rule matches instead (not the bootstrap rule).
|
|
843
844
|
const other = findHighestPriorityRule('bash', ['rm BOOTSTRAP.md'], '/tmp/other-project');
|
|
@@ -852,6 +853,7 @@ describe('Trust Store', () => {
|
|
|
852
853
|
expect(match).not.toBeNull();
|
|
853
854
|
expect(match!.id).toBe('default:allow-bash-rm-updates');
|
|
854
855
|
expect(match!.decision).toBe('allow');
|
|
856
|
+
expect(match!.allowHighRisk).toBe(true);
|
|
855
857
|
// Outside workspace, should NOT match the updates rule
|
|
856
858
|
const other = findHighestPriorityRule('bash', ['rm UPDATES.md'], '/tmp/other-project');
|
|
857
859
|
expect(other).not.toBeNull();
|
|
@@ -276,7 +276,9 @@ describe('trusted contact lifecycle notification signals', () => {
|
|
|
276
276
|
|
|
277
277
|
await handleChannelInbound(guardianReq, undefined, TEST_BEARER_TOKEN);
|
|
278
278
|
|
|
279
|
-
//
|
|
279
|
+
// guardian_decision should NOT fire at approval time when verification
|
|
280
|
+
// is still pending — it would cause the notification pipeline to send a
|
|
281
|
+
// premature "approved" message to the guardian's chat.
|
|
280
282
|
const guardianDecisionSignals = emitSignalCalls.filter(
|
|
281
283
|
(c) => c.sourceEventName === 'ingress.trusted_contact.guardian_decision',
|
|
282
284
|
);
|
|
@@ -284,19 +286,15 @@ describe('trusted contact lifecycle notification signals', () => {
|
|
|
284
286
|
(c) => c.sourceEventName === 'ingress.trusted_contact.verification_sent',
|
|
285
287
|
);
|
|
286
288
|
|
|
287
|
-
expect(guardianDecisionSignals.length).toBe(
|
|
289
|
+
expect(guardianDecisionSignals.length).toBe(0);
|
|
288
290
|
expect(verificationSentSignals.length).toBe(1);
|
|
289
291
|
|
|
290
|
-
// Verify
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
expect(gdPayload.requesterExternalUserId).toBe('requester-user-456');
|
|
294
|
-
expect(gdPayload.decidedByExternalUserId).toBe('guardian-user-789');
|
|
295
|
-
|
|
296
|
-
// Verify verification_sent payload
|
|
297
|
-
const vsPayload = verificationSentSignals[0].contextPayload as Record<string, unknown>;
|
|
292
|
+
// Verify verification_sent payload and that it's suppressed from delivery
|
|
293
|
+
const vsSignal = verificationSentSignals[0];
|
|
294
|
+
const vsPayload = vsSignal.contextPayload as Record<string, unknown>;
|
|
298
295
|
expect(vsPayload.requesterExternalUserId).toBe('requester-user-456');
|
|
299
296
|
expect(vsPayload.verificationSessionId).toBeDefined();
|
|
297
|
+
expect((vsSignal.attentionHints as Record<string, unknown>).visibleInSourceNow).toBe(true);
|
|
300
298
|
|
|
301
299
|
// Should NOT emit denied signal
|
|
302
300
|
const deniedSignals = emitSignalCalls.filter(
|