@vellumai/assistant 0.3.19 → 0.3.21
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 +151 -15
- package/Dockerfile +1 -0
- package/README.md +40 -4
- package/bun.lock +139 -2
- package/docs/architecture/integrations.md +7 -11
- package/package.json +2 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +54 -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 +439 -108
- package/src/__tests__/channel-invite-transport.test.ts +264 -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 +300 -32
- 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 +124 -0
- package/src/__tests__/guardian-grant-minting.test.ts +6 -17
- 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 +57 -0
- package/src/__tests__/notification-decision-fallback.test.ts +88 -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 +6 -6
- package/src/__tests__/scoped-grant-security-matrix.test.ts +5 -4
- 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__/trusted-contact-lifecycle-notifications.test.ts +8 -10
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +46 -84
- 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 +252 -209
- package/src/calls/call-domain.ts +44 -6
- package/src/calls/guardian-dispatch.ts +48 -0
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +46 -30
- package/src/cli/core-commands.ts +0 -4
- package/src/cli/mcp.ts +58 -0
- 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 +1 -1
- 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/mcp-schema.ts +46 -0
- package/src/config/sandbox-schema.ts +0 -39
- package/src/config/schema.ts +18 -2
- package/src/config/skill-state.ts +34 -0
- package/src/config/skills-schema.ts +0 -1
- 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 +6 -5
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +105 -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/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/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-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +4 -0
- package/src/daemon/lifecycle.ts +14 -2
- package/src/daemon/main.ts +1 -0
- package/src/daemon/providers-setup.ts +26 -1
- 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 +258 -432
- 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/shutdown-handlers.ts +11 -0
- package/src/daemon/tool-side-effects.ts +35 -9
- package/src/index.ts +2 -2
- package/src/mcp/client.ts +152 -0
- package/src/mcp/manager.ts +139 -0
- 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 +5 -1
- package/src/memory/embedding-local.ts +13 -8
- package/src/memory/guardian-action-store.ts +125 -2
- package/src/memory/ingress-invite-store.ts +95 -1
- package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
- package/src/memory/migrations/index.ts +2 -1
- package/src/memory/schema.ts +5 -1
- package/src/memory/scoped-approval-grants.ts +14 -5
- 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/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 +92 -35
- 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 -190
- package/src/runtime/routes/identity-routes.ts +73 -0
- package/src/runtime/routes/inbound-message-handler.ts +486 -394
- 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 +1 -1
- 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/mcp/mcp-tool-factory.ts +100 -0
- package/src/tools/network/script-proxy/session-manager.ts +1 -5
- package/src/tools/registry.ts +64 -1
- 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 +10 -2
- 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
|
@@ -1180,6 +1180,20 @@ exports[`IPC message snapshots ClientMessage types heartbeat_checklist_write ser
|
|
|
1180
1180
|
}
|
|
1181
1181
|
`;
|
|
1182
1182
|
|
|
1183
|
+
exports[`IPC message snapshots ClientMessage types voice_config_update serializes to expected JSON 1`] = `
|
|
1184
|
+
{
|
|
1185
|
+
"activationKey": "fn",
|
|
1186
|
+
"type": "voice_config_update",
|
|
1187
|
+
}
|
|
1188
|
+
`;
|
|
1189
|
+
|
|
1190
|
+
exports[`IPC message snapshots ClientMessage types generate_avatar serializes to expected JSON 1`] = `
|
|
1191
|
+
{
|
|
1192
|
+
"description": "a friendly purple cat with green eyes wearing a tiny hat",
|
|
1193
|
+
"type": "generate_avatar",
|
|
1194
|
+
}
|
|
1195
|
+
`;
|
|
1196
|
+
|
|
1183
1197
|
exports[`IPC message snapshots ServerMessage types auth_result serializes to expected JSON 1`] = `
|
|
1184
1198
|
{
|
|
1185
1199
|
"success": true,
|
|
@@ -3142,3 +3156,43 @@ exports[`IPC message snapshots ServerMessage types heartbeat_checklist_write_res
|
|
|
3142
3156
|
"type": "heartbeat_checklist_write_response",
|
|
3143
3157
|
}
|
|
3144
3158
|
`;
|
|
3159
|
+
|
|
3160
|
+
exports[`IPC message snapshots ServerMessage types navigate_settings serializes to expected JSON 1`] = `
|
|
3161
|
+
{
|
|
3162
|
+
"tab": "general",
|
|
3163
|
+
"type": "navigate_settings",
|
|
3164
|
+
}
|
|
3165
|
+
`;
|
|
3166
|
+
|
|
3167
|
+
exports[`IPC message snapshots ServerMessage types client_settings_update serializes to expected JSON 1`] = `
|
|
3168
|
+
{
|
|
3169
|
+
"key": "activationKey",
|
|
3170
|
+
"type": "client_settings_update",
|
|
3171
|
+
"value": "fn",
|
|
3172
|
+
}
|
|
3173
|
+
`;
|
|
3174
|
+
|
|
3175
|
+
exports[`IPC message snapshots ServerMessage types identity_changed serializes to expected JSON 1`] = `
|
|
3176
|
+
{
|
|
3177
|
+
"emoji": "",
|
|
3178
|
+
"home": "",
|
|
3179
|
+
"name": "Vellum",
|
|
3180
|
+
"personality": "friendly",
|
|
3181
|
+
"role": "assistant",
|
|
3182
|
+
"type": "identity_changed",
|
|
3183
|
+
}
|
|
3184
|
+
`;
|
|
3185
|
+
|
|
3186
|
+
exports[`IPC message snapshots ServerMessage types avatar_updated serializes to expected JSON 1`] = `
|
|
3187
|
+
{
|
|
3188
|
+
"avatarPath": "/Users/test/.vellum/workspace/data/avatar/custom-avatar.png",
|
|
3189
|
+
"type": "avatar_updated",
|
|
3190
|
+
}
|
|
3191
|
+
`;
|
|
3192
|
+
|
|
3193
|
+
exports[`IPC message snapshots ServerMessage types generate_avatar_response serializes to expected JSON 1`] = `
|
|
3194
|
+
{
|
|
3195
|
+
"success": true,
|
|
3196
|
+
"type": "generate_avatar_response",
|
|
3197
|
+
}
|
|
3198
|
+
`;
|
|
@@ -0,0 +1,540 @@
|
|
|
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(), 'approval-primitive-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
|
+
import {
|
|
33
|
+
consumeGrantForInvocation,
|
|
34
|
+
mintGrantFromDecision,
|
|
35
|
+
type MintGrantParams,
|
|
36
|
+
} from '../approvals/approval-primitive.js';
|
|
37
|
+
import { getDb, initializeDb, resetDb } from '../memory/db.js';
|
|
38
|
+
import { scopedApprovalGrants } from '../memory/schema.js';
|
|
39
|
+
import { computeToolApprovalDigest } from '../security/tool-approval-digest.js';
|
|
40
|
+
|
|
41
|
+
initializeDb();
|
|
42
|
+
|
|
43
|
+
function clearTables(): void {
|
|
44
|
+
const db = getDb();
|
|
45
|
+
db.delete(scopedApprovalGrants).run();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
afterAll(() => {
|
|
49
|
+
resetDb();
|
|
50
|
+
try {
|
|
51
|
+
rmSync(testDir, { recursive: true });
|
|
52
|
+
} catch {
|
|
53
|
+
/* best effort */
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Helper to build mint params with sensible defaults
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
function mintParams(overrides: Partial<MintGrantParams> = {}): MintGrantParams {
|
|
62
|
+
const futureExpiry = new Date(Date.now() + 60_000).toISOString();
|
|
63
|
+
return {
|
|
64
|
+
assistantId: 'self',
|
|
65
|
+
scopeMode: 'request_id',
|
|
66
|
+
requestChannel: 'telegram',
|
|
67
|
+
decisionChannel: 'telegram',
|
|
68
|
+
expiresAt: futureExpiry,
|
|
69
|
+
...overrides,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ===========================================================================
|
|
74
|
+
// MINT TESTS
|
|
75
|
+
// ===========================================================================
|
|
76
|
+
|
|
77
|
+
describe('approval-primitive / mintGrantFromDecision', () => {
|
|
78
|
+
beforeEach(() => clearTables());
|
|
79
|
+
|
|
80
|
+
test('mints a request_id scoped grant successfully', () => {
|
|
81
|
+
const result = mintGrantFromDecision(
|
|
82
|
+
mintParams({ scopeMode: 'request_id', requestId: 'req-1' }),
|
|
83
|
+
);
|
|
84
|
+
expect(result.ok).toBe(true);
|
|
85
|
+
if (!result.ok) return;
|
|
86
|
+
expect(result.grant.status).toBe('active');
|
|
87
|
+
expect(result.grant.requestId).toBe('req-1');
|
|
88
|
+
expect(result.grant.scopeMode).toBe('request_id');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('mints a tool_signature scoped grant successfully', () => {
|
|
92
|
+
const digest = computeToolApprovalDigest('shell', { command: 'ls' });
|
|
93
|
+
const result = mintGrantFromDecision(
|
|
94
|
+
mintParams({
|
|
95
|
+
scopeMode: 'tool_signature',
|
|
96
|
+
toolName: 'shell',
|
|
97
|
+
inputDigest: digest,
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
expect(result.ok).toBe(true);
|
|
101
|
+
if (!result.ok) return;
|
|
102
|
+
expect(result.grant.toolName).toBe('shell');
|
|
103
|
+
expect(result.grant.inputDigest).toBe(digest);
|
|
104
|
+
expect(result.grant.scopeMode).toBe('tool_signature');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('rejects request_id scope when requestId is missing', () => {
|
|
108
|
+
const result = mintGrantFromDecision(
|
|
109
|
+
mintParams({ scopeMode: 'request_id', requestId: null }),
|
|
110
|
+
);
|
|
111
|
+
expect(result.ok).toBe(false);
|
|
112
|
+
if (result.ok) return;
|
|
113
|
+
expect(result.reason).toBe('missing_request_id');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('rejects tool_signature scope when toolName is missing', () => {
|
|
117
|
+
const result = mintGrantFromDecision(
|
|
118
|
+
mintParams({
|
|
119
|
+
scopeMode: 'tool_signature',
|
|
120
|
+
toolName: null,
|
|
121
|
+
inputDigest: 'abc123',
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
expect(result.ok).toBe(false);
|
|
125
|
+
if (result.ok) return;
|
|
126
|
+
expect(result.reason).toBe('missing_tool_fields');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('rejects tool_signature scope when inputDigest is missing', () => {
|
|
130
|
+
const result = mintGrantFromDecision(
|
|
131
|
+
mintParams({
|
|
132
|
+
scopeMode: 'tool_signature',
|
|
133
|
+
toolName: 'shell',
|
|
134
|
+
inputDigest: null,
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
expect(result.ok).toBe(false);
|
|
138
|
+
if (result.ok) return;
|
|
139
|
+
expect(result.reason).toBe('missing_tool_fields');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('mints grant with full scope context fields', () => {
|
|
143
|
+
const result = mintGrantFromDecision(
|
|
144
|
+
mintParams({
|
|
145
|
+
scopeMode: 'request_id',
|
|
146
|
+
requestId: 'req-full',
|
|
147
|
+
conversationId: 'conv-1',
|
|
148
|
+
callSessionId: 'call-1',
|
|
149
|
+
requesterExternalUserId: 'user-1',
|
|
150
|
+
guardianExternalUserId: 'guardian-1',
|
|
151
|
+
executionChannel: 'voice',
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
expect(result.ok).toBe(true);
|
|
155
|
+
if (!result.ok) return;
|
|
156
|
+
expect(result.grant.conversationId).toBe('conv-1');
|
|
157
|
+
expect(result.grant.callSessionId).toBe('call-1');
|
|
158
|
+
expect(result.grant.requesterExternalUserId).toBe('user-1');
|
|
159
|
+
expect(result.grant.guardianExternalUserId).toBe('guardian-1');
|
|
160
|
+
expect(result.grant.executionChannel).toBe('voice');
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ===========================================================================
|
|
165
|
+
// CONSUME TESTS
|
|
166
|
+
// ===========================================================================
|
|
167
|
+
|
|
168
|
+
describe('approval-primitive / consumeGrantForInvocation', () => {
|
|
169
|
+
beforeEach(() => clearTables());
|
|
170
|
+
|
|
171
|
+
test('consumes a request_id grant when requestId matches', async () => {
|
|
172
|
+
mintGrantFromDecision(mintParams({ scopeMode: 'request_id', requestId: 'req-100' }));
|
|
173
|
+
|
|
174
|
+
const result = await consumeGrantForInvocation({
|
|
175
|
+
requestId: 'req-100',
|
|
176
|
+
toolName: 'shell',
|
|
177
|
+
inputDigest: computeToolApprovalDigest('shell', { command: 'ls' }),
|
|
178
|
+
consumingRequestId: 'consumer-1',
|
|
179
|
+
assistantId: 'self',
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(result.ok).toBe(true);
|
|
183
|
+
if (!result.ok) return;
|
|
184
|
+
expect(result.grant.status).toBe('consumed');
|
|
185
|
+
expect(result.grant.consumedByRequestId).toBe('consumer-1');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('consumes a tool_signature grant when tool+input matches', async () => {
|
|
189
|
+
const digest = computeToolApprovalDigest('shell', { command: 'ls' });
|
|
190
|
+
mintGrantFromDecision(
|
|
191
|
+
mintParams({
|
|
192
|
+
scopeMode: 'tool_signature',
|
|
193
|
+
toolName: 'shell',
|
|
194
|
+
inputDigest: digest,
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const result = await consumeGrantForInvocation({
|
|
199
|
+
toolName: 'shell',
|
|
200
|
+
inputDigest: digest,
|
|
201
|
+
consumingRequestId: 'consumer-2',
|
|
202
|
+
assistantId: 'self',
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(result.ok).toBe(true);
|
|
206
|
+
if (!result.ok) return;
|
|
207
|
+
expect(result.grant.status).toBe('consumed');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('falls back to tool_signature when request_id does not match', async () => {
|
|
211
|
+
const digest = computeToolApprovalDigest('shell', { command: 'ls' });
|
|
212
|
+
// Mint a tool_signature grant (not request_id)
|
|
213
|
+
mintGrantFromDecision(
|
|
214
|
+
mintParams({
|
|
215
|
+
scopeMode: 'tool_signature',
|
|
216
|
+
toolName: 'shell',
|
|
217
|
+
inputDigest: digest,
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const result = await consumeGrantForInvocation({
|
|
222
|
+
requestId: 'nonexistent-req',
|
|
223
|
+
toolName: 'shell',
|
|
224
|
+
inputDigest: digest,
|
|
225
|
+
consumingRequestId: 'consumer-3',
|
|
226
|
+
assistantId: 'self',
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(result.ok).toBe(true);
|
|
230
|
+
if (!result.ok) return;
|
|
231
|
+
expect(result.grant.scopeMode).toBe('tool_signature');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Consume miss scenarios
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
test('miss: no grants exist at all', async () => {
|
|
239
|
+
const result = await consumeGrantForInvocation({
|
|
240
|
+
toolName: 'shell',
|
|
241
|
+
inputDigest: computeToolApprovalDigest('shell', { command: 'ls' }),
|
|
242
|
+
consumingRequestId: 'consumer-miss',
|
|
243
|
+
assistantId: 'self',
|
|
244
|
+
}, { maxWaitMs: 0 });
|
|
245
|
+
|
|
246
|
+
expect(result.ok).toBe(false);
|
|
247
|
+
if (result.ok) return;
|
|
248
|
+
expect(result.reason).toBe('no_match');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('miss: tool name mismatch', async () => {
|
|
252
|
+
const digest = computeToolApprovalDigest('shell', { command: 'ls' });
|
|
253
|
+
mintGrantFromDecision(
|
|
254
|
+
mintParams({
|
|
255
|
+
scopeMode: 'tool_signature',
|
|
256
|
+
toolName: 'shell',
|
|
257
|
+
inputDigest: digest,
|
|
258
|
+
}),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const result = await consumeGrantForInvocation({
|
|
262
|
+
toolName: 'file_write',
|
|
263
|
+
inputDigest: digest,
|
|
264
|
+
consumingRequestId: 'consumer-mismatch-tool',
|
|
265
|
+
assistantId: 'self',
|
|
266
|
+
}, { maxWaitMs: 0 });
|
|
267
|
+
|
|
268
|
+
expect(result.ok).toBe(false);
|
|
269
|
+
if (result.ok) return;
|
|
270
|
+
expect(result.reason).toBe('no_match');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('miss: input digest mismatch', async () => {
|
|
274
|
+
mintGrantFromDecision(
|
|
275
|
+
mintParams({
|
|
276
|
+
scopeMode: 'tool_signature',
|
|
277
|
+
toolName: 'shell',
|
|
278
|
+
inputDigest: computeToolApprovalDigest('shell', { command: 'ls' }),
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const result = await consumeGrantForInvocation({
|
|
283
|
+
toolName: 'shell',
|
|
284
|
+
inputDigest: computeToolApprovalDigest('shell', { command: 'rm -rf /' }),
|
|
285
|
+
consumingRequestId: 'consumer-mismatch-input',
|
|
286
|
+
assistantId: 'self',
|
|
287
|
+
}, { maxWaitMs: 0 });
|
|
288
|
+
|
|
289
|
+
expect(result.ok).toBe(false);
|
|
290
|
+
if (result.ok) return;
|
|
291
|
+
expect(result.reason).toBe('no_match');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('miss: assistant ID mismatch', async () => {
|
|
295
|
+
mintGrantFromDecision(
|
|
296
|
+
mintParams({
|
|
297
|
+
scopeMode: 'request_id',
|
|
298
|
+
requestId: 'req-assist',
|
|
299
|
+
assistantId: 'assistant-A',
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const result = await consumeGrantForInvocation({
|
|
304
|
+
requestId: 'req-assist',
|
|
305
|
+
toolName: 'shell',
|
|
306
|
+
inputDigest: computeToolApprovalDigest('shell', {}),
|
|
307
|
+
consumingRequestId: 'consumer-assist-mismatch',
|
|
308
|
+
assistantId: 'assistant-B',
|
|
309
|
+
}, { maxWaitMs: 0 });
|
|
310
|
+
|
|
311
|
+
expect(result.ok).toBe(false);
|
|
312
|
+
if (result.ok) return;
|
|
313
|
+
expect(result.reason).toBe('no_match');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('miss: grant expired', async () => {
|
|
317
|
+
const pastExpiry = new Date(Date.now() - 60_000).toISOString();
|
|
318
|
+
mintGrantFromDecision(
|
|
319
|
+
mintParams({
|
|
320
|
+
scopeMode: 'request_id',
|
|
321
|
+
requestId: 'req-expired',
|
|
322
|
+
expiresAt: pastExpiry,
|
|
323
|
+
}),
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
const result = await consumeGrantForInvocation({
|
|
327
|
+
requestId: 'req-expired',
|
|
328
|
+
toolName: 'shell',
|
|
329
|
+
inputDigest: computeToolApprovalDigest('shell', {}),
|
|
330
|
+
consumingRequestId: 'consumer-expired',
|
|
331
|
+
assistantId: 'self',
|
|
332
|
+
}, { maxWaitMs: 0 });
|
|
333
|
+
|
|
334
|
+
expect(result.ok).toBe(false);
|
|
335
|
+
if (result.ok) return;
|
|
336
|
+
expect(result.reason).toBe('no_match');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
// One-time consume semantics
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
test('one-time consume: second consume of the same grant fails', async () => {
|
|
344
|
+
mintGrantFromDecision(
|
|
345
|
+
mintParams({ scopeMode: 'request_id', requestId: 'req-once' }),
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
const first = await consumeGrantForInvocation({
|
|
349
|
+
requestId: 'req-once',
|
|
350
|
+
toolName: 'shell',
|
|
351
|
+
inputDigest: computeToolApprovalDigest('shell', {}),
|
|
352
|
+
consumingRequestId: 'consumer-first',
|
|
353
|
+
assistantId: 'self',
|
|
354
|
+
});
|
|
355
|
+
expect(first.ok).toBe(true);
|
|
356
|
+
|
|
357
|
+
const second = await consumeGrantForInvocation({
|
|
358
|
+
requestId: 'req-once',
|
|
359
|
+
toolName: 'shell',
|
|
360
|
+
inputDigest: computeToolApprovalDigest('shell', {}),
|
|
361
|
+
consumingRequestId: 'consumer-second',
|
|
362
|
+
assistantId: 'self',
|
|
363
|
+
}, { maxWaitMs: 0 });
|
|
364
|
+
expect(second.ok).toBe(false);
|
|
365
|
+
if (second.ok) return;
|
|
366
|
+
expect(second.reason).toBe('no_match');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test('one-time consume: tool_signature grant is consumed only once', async () => {
|
|
370
|
+
const digest = computeToolApprovalDigest('shell', { command: 'deploy' });
|
|
371
|
+
mintGrantFromDecision(
|
|
372
|
+
mintParams({
|
|
373
|
+
scopeMode: 'tool_signature',
|
|
374
|
+
toolName: 'shell',
|
|
375
|
+
inputDigest: digest,
|
|
376
|
+
}),
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
const first = await consumeGrantForInvocation({
|
|
380
|
+
toolName: 'shell',
|
|
381
|
+
inputDigest: digest,
|
|
382
|
+
consumingRequestId: 'consumer-sig-first',
|
|
383
|
+
assistantId: 'self',
|
|
384
|
+
});
|
|
385
|
+
expect(first.ok).toBe(true);
|
|
386
|
+
|
|
387
|
+
const second = await consumeGrantForInvocation({
|
|
388
|
+
toolName: 'shell',
|
|
389
|
+
inputDigest: digest,
|
|
390
|
+
consumingRequestId: 'consumer-sig-second',
|
|
391
|
+
assistantId: 'self',
|
|
392
|
+
}, { maxWaitMs: 0 });
|
|
393
|
+
expect(second.ok).toBe(false);
|
|
394
|
+
if (second.ok) return;
|
|
395
|
+
expect(second.reason).toBe('no_match');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
// Context-scoped consume
|
|
400
|
+
// ---------------------------------------------------------------------------
|
|
401
|
+
|
|
402
|
+
test('consumes tool_signature grant with matching conversation context', async () => {
|
|
403
|
+
const digest = computeToolApprovalDigest('shell', { command: 'test' });
|
|
404
|
+
mintGrantFromDecision(
|
|
405
|
+
mintParams({
|
|
406
|
+
scopeMode: 'tool_signature',
|
|
407
|
+
toolName: 'shell',
|
|
408
|
+
inputDigest: digest,
|
|
409
|
+
conversationId: 'conv-ctx',
|
|
410
|
+
callSessionId: 'call-ctx',
|
|
411
|
+
}),
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
const result = await consumeGrantForInvocation({
|
|
415
|
+
toolName: 'shell',
|
|
416
|
+
inputDigest: digest,
|
|
417
|
+
consumingRequestId: 'consumer-ctx',
|
|
418
|
+
assistantId: 'self',
|
|
419
|
+
conversationId: 'conv-ctx',
|
|
420
|
+
callSessionId: 'call-ctx',
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
expect(result.ok).toBe(true);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test('miss: conversation context mismatch on tool_signature grant', async () => {
|
|
427
|
+
const digest = computeToolApprovalDigest('shell', { command: 'test' });
|
|
428
|
+
mintGrantFromDecision(
|
|
429
|
+
mintParams({
|
|
430
|
+
scopeMode: 'tool_signature',
|
|
431
|
+
toolName: 'shell',
|
|
432
|
+
inputDigest: digest,
|
|
433
|
+
conversationId: 'conv-A',
|
|
434
|
+
}),
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
const result = await consumeGrantForInvocation({
|
|
438
|
+
toolName: 'shell',
|
|
439
|
+
inputDigest: digest,
|
|
440
|
+
consumingRequestId: 'consumer-ctx-mismatch',
|
|
441
|
+
assistantId: 'self',
|
|
442
|
+
conversationId: 'conv-B',
|
|
443
|
+
}, { maxWaitMs: 0 });
|
|
444
|
+
|
|
445
|
+
expect(result.ok).toBe(false);
|
|
446
|
+
if (result.ok) return;
|
|
447
|
+
expect(result.reason).toBe('no_match');
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// ===========================================================================
|
|
452
|
+
// RETRY POLLING TESTS
|
|
453
|
+
// ===========================================================================
|
|
454
|
+
|
|
455
|
+
describe('approval-primitive / consumeGrantForInvocation retry', () => {
|
|
456
|
+
beforeEach(() => clearTables());
|
|
457
|
+
|
|
458
|
+
test('succeeds immediately when grant already exists (no retry needed)', async () => {
|
|
459
|
+
const digest = computeToolApprovalDigest('shell', { command: 'ls' });
|
|
460
|
+
mintGrantFromDecision(
|
|
461
|
+
mintParams({
|
|
462
|
+
scopeMode: 'tool_signature',
|
|
463
|
+
toolName: 'shell',
|
|
464
|
+
inputDigest: digest,
|
|
465
|
+
}),
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
const start = Date.now();
|
|
469
|
+
const result = await consumeGrantForInvocation({
|
|
470
|
+
toolName: 'shell',
|
|
471
|
+
inputDigest: digest,
|
|
472
|
+
consumingRequestId: 'consumer-async-immediate',
|
|
473
|
+
assistantId: 'self',
|
|
474
|
+
});
|
|
475
|
+
const elapsed = Date.now() - start;
|
|
476
|
+
|
|
477
|
+
expect(result.ok).toBe(true);
|
|
478
|
+
if (!result.ok) return;
|
|
479
|
+
expect(result.grant.status).toBe('consumed');
|
|
480
|
+
// Should return nearly instantly — well under the retry interval
|
|
481
|
+
expect(elapsed).toBeLessThan(200);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test('retries and succeeds when grant appears after a delay', async () => {
|
|
485
|
+
const digest = computeToolApprovalDigest('shell', { command: 'delayed' });
|
|
486
|
+
|
|
487
|
+
// Mint the grant after 300ms — the async consumer should retry and find it
|
|
488
|
+
setTimeout(() => {
|
|
489
|
+
mintGrantFromDecision(
|
|
490
|
+
mintParams({
|
|
491
|
+
scopeMode: 'tool_signature',
|
|
492
|
+
toolName: 'shell',
|
|
493
|
+
inputDigest: digest,
|
|
494
|
+
}),
|
|
495
|
+
);
|
|
496
|
+
}, 300);
|
|
497
|
+
|
|
498
|
+
const start = Date.now();
|
|
499
|
+
const result = await consumeGrantForInvocation(
|
|
500
|
+
{
|
|
501
|
+
toolName: 'shell',
|
|
502
|
+
inputDigest: digest,
|
|
503
|
+
consumingRequestId: 'consumer-async-delayed',
|
|
504
|
+
assistantId: 'self',
|
|
505
|
+
},
|
|
506
|
+
{ maxWaitMs: 5_000, intervalMs: 100 },
|
|
507
|
+
);
|
|
508
|
+
const elapsed = Date.now() - start;
|
|
509
|
+
|
|
510
|
+
expect(result.ok).toBe(true);
|
|
511
|
+
if (!result.ok) return;
|
|
512
|
+
expect(result.grant.status).toBe('consumed');
|
|
513
|
+
// Should have taken at least ~300ms (the delay) but less than the max wait
|
|
514
|
+
expect(elapsed).toBeGreaterThanOrEqual(250);
|
|
515
|
+
expect(elapsed).toBeLessThan(5_000);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test('returns failure after timeout when no grant appears', async () => {
|
|
519
|
+
const digest = computeToolApprovalDigest('shell', { command: 'never-minted' });
|
|
520
|
+
|
|
521
|
+
const start = Date.now();
|
|
522
|
+
const result = await consumeGrantForInvocation(
|
|
523
|
+
{
|
|
524
|
+
toolName: 'shell',
|
|
525
|
+
inputDigest: digest,
|
|
526
|
+
consumingRequestId: 'consumer-async-timeout',
|
|
527
|
+
assistantId: 'self',
|
|
528
|
+
},
|
|
529
|
+
{ maxWaitMs: 500, intervalMs: 100 },
|
|
530
|
+
);
|
|
531
|
+
const elapsed = Date.now() - start;
|
|
532
|
+
|
|
533
|
+
expect(result.ok).toBe(false);
|
|
534
|
+
if (result.ok) return;
|
|
535
|
+
expect(result.reason).toBe('no_match');
|
|
536
|
+
// Should have waited approximately the max wait time
|
|
537
|
+
expect(elapsed).toBeGreaterThanOrEqual(450);
|
|
538
|
+
expect(elapsed).toBeLessThan(1_500);
|
|
539
|
+
});
|
|
540
|
+
});
|