@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
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
// Mock the credential metadata store so the Telegram adapter can resolve
|
|
4
|
+
// the bot username without touching the filesystem.
|
|
5
|
+
let mockBotUsername: string | undefined = 'test_invite_bot';
|
|
6
|
+
mock.module('../tools/credentials/metadata-store.js', () => ({
|
|
7
|
+
getCredentialMetadata: (service: string, field: string) => {
|
|
8
|
+
if (service === 'telegram' && field === 'bot_token' && mockBotUsername) {
|
|
9
|
+
return { accountInfo: mockBotUsername };
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
},
|
|
13
|
+
upsertCredentialMetadata: () => {},
|
|
14
|
+
deleteCredentialMetadata: () => {},
|
|
15
|
+
listCredentialMetadata: () => [],
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
_resetRegistry,
|
|
20
|
+
type ChannelInviteTransport,
|
|
21
|
+
getTransport,
|
|
22
|
+
registerTransport,
|
|
23
|
+
} from '../runtime/channel-invite-transport.js';
|
|
24
|
+
// Importing the Telegram module auto-registers the transport
|
|
25
|
+
import { telegramInviteTransport } from '../runtime/channel-invite-transports/telegram.js';
|
|
26
|
+
|
|
27
|
+
describe('channel-invite-transport', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
_resetRegistry();
|
|
30
|
+
mockBotUsername = 'test_invite_bot';
|
|
31
|
+
// Re-register after reset so Telegram tests work
|
|
32
|
+
registerTransport(telegramInviteTransport);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// =========================================================================
|
|
36
|
+
// Registry
|
|
37
|
+
// =========================================================================
|
|
38
|
+
|
|
39
|
+
describe('registry', () => {
|
|
40
|
+
test('returns the Telegram transport for telegram channel', () => {
|
|
41
|
+
const transport = getTransport('telegram');
|
|
42
|
+
expect(transport).toBeDefined();
|
|
43
|
+
expect(transport!.channel).toBe('telegram');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('returns undefined for an unregistered channel', () => {
|
|
47
|
+
const transport = getTransport('sms');
|
|
48
|
+
expect(transport).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('overwrites a previously registered transport for the same channel', () => {
|
|
52
|
+
const custom: ChannelInviteTransport = {
|
|
53
|
+
channel: 'telegram',
|
|
54
|
+
buildShareableInvite: () => ({ url: 'custom', displayText: 'custom' }),
|
|
55
|
+
extractInboundToken: () => undefined,
|
|
56
|
+
};
|
|
57
|
+
registerTransport(custom);
|
|
58
|
+
const transport = getTransport('telegram');
|
|
59
|
+
expect(transport!.buildShareableInvite({ rawToken: 'x', sourceChannel: 'telegram' }).url).toBe('custom');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('_resetRegistry clears all transports', () => {
|
|
63
|
+
_resetRegistry();
|
|
64
|
+
expect(getTransport('telegram')).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// =========================================================================
|
|
69
|
+
// Telegram adapter — buildShareableInvite
|
|
70
|
+
// =========================================================================
|
|
71
|
+
|
|
72
|
+
describe('telegram buildShareableInvite', () => {
|
|
73
|
+
test('produces a valid Telegram deep link', () => {
|
|
74
|
+
const result = telegramInviteTransport.buildShareableInvite({
|
|
75
|
+
rawToken: 'abc123_test-token',
|
|
76
|
+
sourceChannel: 'telegram',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(result.url).toBe('https://t.me/test_invite_bot?start=iv_abc123_test-token');
|
|
80
|
+
expect(result.displayText).toContain('https://t.me/test_invite_bot?start=iv_abc123_test-token');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('deep link is deterministic for the same token', () => {
|
|
84
|
+
const a = telegramInviteTransport.buildShareableInvite({ rawToken: 'tok1', sourceChannel: 'telegram' });
|
|
85
|
+
const b = telegramInviteTransport.buildShareableInvite({ rawToken: 'tok1', sourceChannel: 'telegram' });
|
|
86
|
+
expect(a.url).toBe(b.url);
|
|
87
|
+
expect(a.displayText).toBe(b.displayText);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('uses the configured bot username', () => {
|
|
91
|
+
mockBotUsername = 'my_custom_bot';
|
|
92
|
+
const result = telegramInviteTransport.buildShareableInvite({
|
|
93
|
+
rawToken: 'token',
|
|
94
|
+
sourceChannel: 'telegram',
|
|
95
|
+
});
|
|
96
|
+
expect(result.url).toBe('https://t.me/my_custom_bot?start=iv_token');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('throws when bot username is not configured', () => {
|
|
100
|
+
mockBotUsername = undefined;
|
|
101
|
+
// Also clear the env var to ensure no fallback
|
|
102
|
+
const prev = process.env.TELEGRAM_BOT_USERNAME;
|
|
103
|
+
delete process.env.TELEGRAM_BOT_USERNAME;
|
|
104
|
+
try {
|
|
105
|
+
expect(() =>
|
|
106
|
+
telegramInviteTransport.buildShareableInvite({
|
|
107
|
+
rawToken: 'token',
|
|
108
|
+
sourceChannel: 'telegram',
|
|
109
|
+
}),
|
|
110
|
+
).toThrow('bot username is not configured');
|
|
111
|
+
} finally {
|
|
112
|
+
if (prev !== undefined) process.env.TELEGRAM_BOT_USERNAME = prev;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('falls back to TELEGRAM_BOT_USERNAME env var', () => {
|
|
117
|
+
mockBotUsername = undefined;
|
|
118
|
+
const prev = process.env.TELEGRAM_BOT_USERNAME;
|
|
119
|
+
process.env.TELEGRAM_BOT_USERNAME = 'env_bot';
|
|
120
|
+
try {
|
|
121
|
+
const result = telegramInviteTransport.buildShareableInvite({
|
|
122
|
+
rawToken: 'token',
|
|
123
|
+
sourceChannel: 'telegram',
|
|
124
|
+
});
|
|
125
|
+
expect(result.url).toBe('https://t.me/env_bot?start=iv_token');
|
|
126
|
+
} finally {
|
|
127
|
+
if (prev !== undefined) {
|
|
128
|
+
process.env.TELEGRAM_BOT_USERNAME = prev;
|
|
129
|
+
} else {
|
|
130
|
+
delete process.env.TELEGRAM_BOT_USERNAME;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// =========================================================================
|
|
137
|
+
// Telegram adapter — extractInboundToken
|
|
138
|
+
// =========================================================================
|
|
139
|
+
|
|
140
|
+
describe('telegram extractInboundToken', () => {
|
|
141
|
+
test('extracts token from structured commandIntent', () => {
|
|
142
|
+
const token = telegramInviteTransport.extractInboundToken({
|
|
143
|
+
commandIntent: { type: 'start', payload: 'iv_abc123' },
|
|
144
|
+
content: '/start iv_abc123',
|
|
145
|
+
});
|
|
146
|
+
expect(token).toBe('abc123');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('extracts base64url token from commandIntent', () => {
|
|
150
|
+
const token = telegramInviteTransport.extractInboundToken({
|
|
151
|
+
commandIntent: { type: 'start', payload: 'iv_YWJjMTIz-_test' },
|
|
152
|
+
content: '/start iv_YWJjMTIz-_test',
|
|
153
|
+
});
|
|
154
|
+
expect(token).toBe('YWJjMTIz-_test');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('returns undefined when commandIntent has no payload', () => {
|
|
158
|
+
const token = telegramInviteTransport.extractInboundToken({
|
|
159
|
+
commandIntent: { type: 'start' },
|
|
160
|
+
content: '/start',
|
|
161
|
+
});
|
|
162
|
+
expect(token).toBeUndefined();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('returns undefined when commandIntent payload has wrong prefix (gv_)', () => {
|
|
166
|
+
const token = telegramInviteTransport.extractInboundToken({
|
|
167
|
+
commandIntent: { type: 'start', payload: 'gv_abc123' },
|
|
168
|
+
content: '/start gv_abc123',
|
|
169
|
+
});
|
|
170
|
+
expect(token).toBeUndefined();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('returns undefined when commandIntent payload has no prefix', () => {
|
|
174
|
+
const token = telegramInviteTransport.extractInboundToken({
|
|
175
|
+
commandIntent: { type: 'start', payload: 'abc123' },
|
|
176
|
+
content: '/start abc123',
|
|
177
|
+
});
|
|
178
|
+
expect(token).toBeUndefined();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('returns undefined when commandIntent type is not start', () => {
|
|
182
|
+
const token = telegramInviteTransport.extractInboundToken({
|
|
183
|
+
commandIntent: { type: 'help', payload: 'iv_abc123' },
|
|
184
|
+
content: '/help iv_abc123',
|
|
185
|
+
});
|
|
186
|
+
expect(token).toBeUndefined();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('returns undefined when commandIntent payload is iv_ with empty token', () => {
|
|
190
|
+
const token = telegramInviteTransport.extractInboundToken({
|
|
191
|
+
commandIntent: { type: 'start', payload: 'iv_' },
|
|
192
|
+
content: '/start iv_',
|
|
193
|
+
});
|
|
194
|
+
expect(token).toBeUndefined();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('returns undefined when commandIntent payload is iv_ with whitespace-only token', () => {
|
|
198
|
+
const token = telegramInviteTransport.extractInboundToken({
|
|
199
|
+
commandIntent: { type: 'start', payload: 'iv_ ' },
|
|
200
|
+
content: '/start iv_ ',
|
|
201
|
+
});
|
|
202
|
+
expect(token).toBeUndefined();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('extracts token from raw content fallback', () => {
|
|
206
|
+
const token = telegramInviteTransport.extractInboundToken({
|
|
207
|
+
content: '/start iv_abc123',
|
|
208
|
+
});
|
|
209
|
+
expect(token).toBe('abc123');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('extracts token from raw content with extra whitespace', () => {
|
|
213
|
+
const token = telegramInviteTransport.extractInboundToken({
|
|
214
|
+
content: '/start iv_token123',
|
|
215
|
+
});
|
|
216
|
+
expect(token).toBe('token123');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('returns undefined for empty content', () => {
|
|
220
|
+
const token = telegramInviteTransport.extractInboundToken({
|
|
221
|
+
content: '',
|
|
222
|
+
});
|
|
223
|
+
expect(token).toBeUndefined();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('returns undefined for content without /start', () => {
|
|
227
|
+
const token = telegramInviteTransport.extractInboundToken({
|
|
228
|
+
content: 'hello world',
|
|
229
|
+
});
|
|
230
|
+
expect(token).toBeUndefined();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('returns undefined for /start without iv_ prefix in content', () => {
|
|
234
|
+
const token = telegramInviteTransport.extractInboundToken({
|
|
235
|
+
content: '/start gv_abc123',
|
|
236
|
+
});
|
|
237
|
+
expect(token).toBeUndefined();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('returns undefined for malformed /start with only iv_ in content', () => {
|
|
241
|
+
const token = telegramInviteTransport.extractInboundToken({
|
|
242
|
+
content: '/start iv_',
|
|
243
|
+
});
|
|
244
|
+
expect(token).toBeUndefined();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('prefers commandIntent over raw content', () => {
|
|
248
|
+
const token = telegramInviteTransport.extractInboundToken({
|
|
249
|
+
commandIntent: { type: 'start', payload: 'iv_from_intent' },
|
|
250
|
+
content: '/start iv_from_content',
|
|
251
|
+
});
|
|
252
|
+
expect(token).toBe('from_intent');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('returns undefined when commandIntent rejects, even if content has token', () => {
|
|
256
|
+
// commandIntent present but payload has wrong prefix
|
|
257
|
+
const token = telegramInviteTransport.extractInboundToken({
|
|
258
|
+
commandIntent: { type: 'start', payload: 'gv_abc123' },
|
|
259
|
+
content: '/start iv_valid_token',
|
|
260
|
+
});
|
|
261
|
+
expect(token).toBeUndefined();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { describe, expect,test } from 'bun:test';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
formatConfirmationCommandPreview,
|
|
5
|
+
formatConfirmationInputLines,
|
|
6
|
+
sanitizeUrlForDisplay,
|
|
7
|
+
} from '../cli.js';
|
|
4
8
|
|
|
5
9
|
describe('sanitizeUrlForDisplay', () => {
|
|
6
10
|
test('removes userinfo from absolute URLs', () => {
|
|
@@ -25,3 +29,40 @@ describe('sanitizeUrlForDisplay', () => {
|
|
|
25
29
|
expect(sanitizeUrlForDisplay(rawValue)).toBe('not-a-url //[REDACTED]@example.com');
|
|
26
30
|
});
|
|
27
31
|
});
|
|
32
|
+
|
|
33
|
+
describe('formatConfirmationInputLines', () => {
|
|
34
|
+
test('preserves full old_string and new_string values without truncation', () => {
|
|
35
|
+
const oldString = 'old '.repeat(120);
|
|
36
|
+
const newString = 'new '.repeat(120);
|
|
37
|
+
const lines = formatConfirmationInputLines({
|
|
38
|
+
old_string: oldString,
|
|
39
|
+
new_string: newString,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(lines).toContain(`old_string: ${oldString}`);
|
|
43
|
+
expect(lines).toContain(`new_string: ${newString}`);
|
|
44
|
+
expect(lines.some((line) => line.includes('...'))).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('preserves multiline values', () => {
|
|
48
|
+
const lines = formatConfirmationInputLines({
|
|
49
|
+
old_string: 'line1\nline2\nline3',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(lines).toEqual([
|
|
53
|
+
'old_string: line1',
|
|
54
|
+
' line2',
|
|
55
|
+
' line3',
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('formatConfirmationCommandPreview', () => {
|
|
61
|
+
test('shows concise file_edit preview', () => {
|
|
62
|
+
const preview = formatConfirmationCommandPreview({
|
|
63
|
+
toolName: 'file_edit',
|
|
64
|
+
input: { path: '/tmp/sample.txt' },
|
|
65
|
+
});
|
|
66
|
+
expect(preview).toBe('edit /tmp/sample.txt');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -52,7 +52,6 @@ mock.module('../util/platform.js', () => ({
|
|
|
52
52
|
}));
|
|
53
53
|
|
|
54
54
|
import { buildElevenLabsVoiceSpec, resolveVoiceQualityProfile } from '../calls/voice-quality.js';
|
|
55
|
-
import { DEFAULT_CONFIG } from '../config/defaults.js';
|
|
56
55
|
import { invalidateConfigCache,loadConfig } from '../config/loader.js';
|
|
57
56
|
import { AssistantConfigSchema } from '../config/schema.js';
|
|
58
57
|
import { _setStorePath } from '../security/encrypted-store.js';
|
|
@@ -96,15 +95,6 @@ describe('AssistantConfigSchema', () => {
|
|
|
96
95
|
});
|
|
97
96
|
expect(result.sandbox).toEqual({
|
|
98
97
|
enabled: true,
|
|
99
|
-
backend: 'docker',
|
|
100
|
-
docker: {
|
|
101
|
-
image: 'vellum-sandbox:latest',
|
|
102
|
-
shell: 'bash',
|
|
103
|
-
cpus: 1,
|
|
104
|
-
memoryMb: 512,
|
|
105
|
-
pidsLimit: 256,
|
|
106
|
-
network: 'none',
|
|
107
|
-
},
|
|
108
98
|
});
|
|
109
99
|
expect(result.rateLimit).toEqual({ maxRequestsPerMinute: 0, maxTokensPerSession: 0 });
|
|
110
100
|
expect(result.secretDetection).toEqual({ enabled: true, action: 'redact', entropyThreshold: 4.0, allowOneTimeSend: false, blockIngress: true });
|
|
@@ -393,113 +383,22 @@ describe('AssistantConfigSchema', () => {
|
|
|
393
383
|
}
|
|
394
384
|
});
|
|
395
385
|
|
|
396
|
-
|
|
397
|
-
// container-level isolation. Native is available as opt-in fallback.
|
|
398
|
-
test('default sandbox backend is docker', () => {
|
|
399
|
-
const result = AssistantConfigSchema.parse({});
|
|
400
|
-
expect(result.sandbox.backend).toBe('docker');
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
test('DEFAULT_CONFIG sandbox backend is docker', () => {
|
|
404
|
-
expect(DEFAULT_CONFIG.sandbox.backend).toBe('docker');
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
test('backward compatibility: sandbox with only enabled still parses', () => {
|
|
386
|
+
test('sandbox with only enabled still parses', () => {
|
|
408
387
|
const result = AssistantConfigSchema.parse({ sandbox: { enabled: false } });
|
|
409
388
|
expect(result.sandbox.enabled).toBe(false);
|
|
410
|
-
expect(result.sandbox.backend).toBe('docker');
|
|
411
|
-
expect(result.sandbox.docker.memoryMb).toBe(512);
|
|
412
389
|
});
|
|
413
390
|
|
|
414
|
-
test('
|
|
415
|
-
const result = AssistantConfigSchema.parse({
|
|
416
|
-
sandbox: {
|
|
417
|
-
enabled: true,
|
|
418
|
-
backend: 'docker',
|
|
419
|
-
docker: {
|
|
420
|
-
image: 'ubuntu:22.04',
|
|
421
|
-
cpus: 2,
|
|
422
|
-
memoryMb: 1024,
|
|
423
|
-
pidsLimit: 512,
|
|
424
|
-
network: 'bridge',
|
|
425
|
-
},
|
|
426
|
-
},
|
|
427
|
-
});
|
|
428
|
-
expect(result.sandbox.backend).toBe('docker');
|
|
429
|
-
expect(result.sandbox.docker.image).toBe('ubuntu:22.04');
|
|
430
|
-
expect(result.sandbox.docker.cpus).toBe(2);
|
|
431
|
-
expect(result.sandbox.docker.memoryMb).toBe(1024);
|
|
432
|
-
expect(result.sandbox.docker.pidsLimit).toBe(512);
|
|
433
|
-
expect(result.sandbox.docker.network).toBe('bridge');
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
test('applies docker defaults when backend is docker but docker config omitted', () => {
|
|
437
|
-
const result = AssistantConfigSchema.parse({
|
|
438
|
-
sandbox: { backend: 'docker' },
|
|
439
|
-
});
|
|
440
|
-
expect(result.sandbox.backend).toBe('docker');
|
|
441
|
-
expect(result.sandbox.docker.cpus).toBe(1);
|
|
442
|
-
expect(result.sandbox.docker.memoryMb).toBe(512);
|
|
443
|
-
expect(result.sandbox.docker.pidsLimit).toBe(256);
|
|
444
|
-
expect(result.sandbox.docker.network).toBe('none');
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
test('accepts partial docker config with defaults for missing fields', () => {
|
|
448
|
-
const result = AssistantConfigSchema.parse({
|
|
449
|
-
sandbox: {
|
|
450
|
-
backend: 'docker',
|
|
451
|
-
docker: { memoryMb: 2048 },
|
|
452
|
-
},
|
|
453
|
-
});
|
|
454
|
-
expect(result.sandbox.docker.memoryMb).toBe(2048);
|
|
455
|
-
expect(result.sandbox.docker.cpus).toBe(1);
|
|
456
|
-
expect(result.sandbox.docker.pidsLimit).toBe(256);
|
|
457
|
-
expect(result.sandbox.docker.network).toBe('none');
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
test('rejects invalid sandbox.backend', () => {
|
|
391
|
+
test('rejects unknown sandbox fields', () => {
|
|
461
392
|
const result = AssistantConfigSchema.safeParse({
|
|
462
|
-
sandbox: { backend: '
|
|
463
|
-
});
|
|
464
|
-
expect(result.success).toBe(false);
|
|
465
|
-
if (!result.success) {
|
|
466
|
-
const msgs = result.error.issues.map(i => i.message);
|
|
467
|
-
expect(msgs.some(m => m.includes('sandbox.backend'))).toBe(true);
|
|
468
|
-
}
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
test('rejects invalid docker.network', () => {
|
|
472
|
-
const result = AssistantConfigSchema.safeParse({
|
|
473
|
-
sandbox: { docker: { network: 'host' } },
|
|
393
|
+
sandbox: { backend: 'docker' },
|
|
474
394
|
});
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
expect(
|
|
395
|
+
// Unknown keys are stripped by Zod passthrough/strip, so parse should still succeed
|
|
396
|
+
// but the unknown field should not appear in the output
|
|
397
|
+
if (result.success) {
|
|
398
|
+
expect((result.data.sandbox as Record<string, unknown>)['backend']).toBeUndefined();
|
|
479
399
|
}
|
|
480
400
|
});
|
|
481
401
|
|
|
482
|
-
test('rejects non-positive docker.memoryMb', () => {
|
|
483
|
-
const result = AssistantConfigSchema.safeParse({
|
|
484
|
-
sandbox: { docker: { memoryMb: 0 } },
|
|
485
|
-
});
|
|
486
|
-
expect(result.success).toBe(false);
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
test('rejects non-integer docker.pidsLimit', () => {
|
|
490
|
-
const result = AssistantConfigSchema.safeParse({
|
|
491
|
-
sandbox: { docker: { pidsLimit: 3.5 } },
|
|
492
|
-
});
|
|
493
|
-
expect(result.success).toBe(false);
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
test('rejects negative docker.cpus', () => {
|
|
497
|
-
const result = AssistantConfigSchema.safeParse({
|
|
498
|
-
sandbox: { docker: { cpus: -1 } },
|
|
499
|
-
});
|
|
500
|
-
expect(result.success).toBe(false);
|
|
501
|
-
});
|
|
502
|
-
|
|
503
402
|
test('defaults permissions.mode to workspace', () => {
|
|
504
403
|
const result = AssistantConfigSchema.parse({});
|
|
505
404
|
expect(result.permissions).toEqual({ mode: 'workspace' });
|
|
@@ -1277,28 +1176,13 @@ describe('loadConfig with schema validation', () => {
|
|
|
1277
1176
|
writeConfig({ sandbox: { enabled: false } });
|
|
1278
1177
|
const config = loadConfig();
|
|
1279
1178
|
expect(config.sandbox.enabled).toBe(false);
|
|
1280
|
-
expect(config.sandbox.backend).toBe('docker');
|
|
1281
|
-
expect(config.sandbox.docker.memoryMb).toBe(512);
|
|
1282
1179
|
});
|
|
1283
1180
|
|
|
1284
|
-
test('
|
|
1285
|
-
writeConfig({
|
|
1286
|
-
sandbox: {
|
|
1287
|
-
backend: 'docker',
|
|
1288
|
-
docker: { memoryMb: 2048, network: 'bridge' },
|
|
1289
|
-
},
|
|
1290
|
-
});
|
|
1181
|
+
test('strips unknown sandbox fields', () => {
|
|
1182
|
+
writeConfig({ sandbox: { enabled: true, backend: 'docker' } });
|
|
1291
1183
|
const config = loadConfig();
|
|
1292
|
-
expect(config.sandbox.
|
|
1293
|
-
expect(config.sandbox
|
|
1294
|
-
expect(config.sandbox.docker.network).toBe('bridge');
|
|
1295
|
-
expect(config.sandbox.docker.cpus).toBe(1);
|
|
1296
|
-
});
|
|
1297
|
-
|
|
1298
|
-
test('falls back for invalid sandbox.backend', () => {
|
|
1299
|
-
writeConfig({ sandbox: { backend: 'podman' } });
|
|
1300
|
-
const config = loadConfig();
|
|
1301
|
-
expect(config.sandbox.backend).toBe('docker');
|
|
1184
|
+
expect(config.sandbox.enabled).toBe(true);
|
|
1185
|
+
expect('backend' in config.sandbox).toBe(false);
|
|
1302
1186
|
});
|
|
1303
1187
|
|
|
1304
1188
|
test('falls back for invalid contextWindow relationship', () => {
|
|
@@ -193,14 +193,6 @@ describe('ConfigWatcher workspace file handlers', () => {
|
|
|
193
193
|
expect(evictCallCount).toBe(1);
|
|
194
194
|
});
|
|
195
195
|
|
|
196
|
-
test('LOOKS.md change triggers onSessionEvict', async () => {
|
|
197
|
-
watcher.start(onSessionEvict);
|
|
198
|
-
simulateFileChange(WORKSPACE_DIR, 'LOOKS.md');
|
|
199
|
-
|
|
200
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
201
|
-
expect(evictCallCount).toBe(1);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
196
|
test('UPDATES.md change triggers onSessionEvict', async () => {
|
|
205
197
|
watcher.start(onSessionEvict);
|
|
206
198
|
simulateFileChange(WORKSPACE_DIR, 'UPDATES.md');
|
|
@@ -41,6 +41,7 @@ class MockSession {
|
|
|
41
41
|
public readonly conversationId: string;
|
|
42
42
|
public memoryPolicy: MockMemoryPolicy;
|
|
43
43
|
public updateClientCalls = 0;
|
|
44
|
+
public ensureActorScopedHistoryCalls = 0;
|
|
44
45
|
public lastUpdateClientHasNoClient: boolean | undefined;
|
|
45
46
|
public lastUpdateClientSender: ((msg: Record<string, unknown>) => void) | undefined;
|
|
46
47
|
public lastRunAgentLoopOptions: { skipPreMessageRollback?: boolean; isInteractive?: boolean } | undefined;
|
|
@@ -69,6 +70,10 @@ class MockSession {
|
|
|
69
70
|
|
|
70
71
|
async loadFromDb(): Promise<void> {}
|
|
71
72
|
|
|
73
|
+
async ensureActorScopedHistory(): Promise<void> {
|
|
74
|
+
this.ensureActorScopedHistoryCalls += 1;
|
|
75
|
+
}
|
|
76
|
+
|
|
72
77
|
updateClient(sender?: (msg: Record<string, unknown>) => void, hasNoClient = false): void {
|
|
73
78
|
this.updateClientCalls += 1;
|
|
74
79
|
this.lastUpdateClientSender = sender;
|
|
@@ -165,8 +170,8 @@ class MockSession {
|
|
|
165
170
|
}
|
|
166
171
|
}
|
|
167
172
|
|
|
168
|
-
// Mock child_process to prevent getScreenDimensions() from running
|
|
169
|
-
// where
|
|
173
|
+
// Mock child_process to prevent getScreenDimensions() from running osascript on Linux CI
|
|
174
|
+
// where AppKit/NSScreen is not available and the execSync call would fail.
|
|
170
175
|
mock.module('node:child_process', () => ({
|
|
171
176
|
execSync: () => '1920x1080',
|
|
172
177
|
execFileSync: () => '',
|
|
@@ -634,6 +639,7 @@ describe('DaemonServer initial session hydration', () => {
|
|
|
634
639
|
const session = internal.sessions.get(conversation.id);
|
|
635
640
|
expect(session).toBeDefined();
|
|
636
641
|
expect(session!.lastRunAgentLoopOptions?.isInteractive).toBe(true);
|
|
642
|
+
expect(session!.ensureActorScopedHistoryCalls).toBeGreaterThanOrEqual(1);
|
|
637
643
|
|
|
638
644
|
// Verify the session was marked interactive during the loop, then restored.
|
|
639
645
|
// updateClientHistory: [0] = initial no-socket creation (hasNoClient: true),
|
|
@@ -91,6 +91,18 @@ describe('formatDiff', () => {
|
|
|
91
91
|
expect(result).toContain('-old content');
|
|
92
92
|
expect(result).toContain('-line 2');
|
|
93
93
|
});
|
|
94
|
+
|
|
95
|
+
test('uses a full fallback diff for oversized files without truncation markers', () => {
|
|
96
|
+
const old = Array.from({ length: 6 }, (_, i) => `old-${i + 1}`).join('\n');
|
|
97
|
+
const updated = Array.from({ length: 6 }, (_, i) => (i === 3 ? 'new-4' : `old-${i + 1}`)).join('\n');
|
|
98
|
+
const result = stripAnsi(formatDiff(old, updated, 'oversized.ts', { maxExactLines: 2 }));
|
|
99
|
+
|
|
100
|
+
expect(result).toContain('--- a/oversized.ts');
|
|
101
|
+
expect(result).toContain('+++ b/oversized.ts');
|
|
102
|
+
expect(result).toContain('-old-4');
|
|
103
|
+
expect(result).toContain('+new-4');
|
|
104
|
+
expect(result).not.toContain('Diff too large to display');
|
|
105
|
+
});
|
|
94
106
|
});
|
|
95
107
|
|
|
96
108
|
describe('formatNewFileDiff', () => {
|
|
@@ -119,4 +131,14 @@ describe('formatNewFileDiff', () => {
|
|
|
119
131
|
const result = stripAnsi(formatNewFileDiff(content, 'small.ts'));
|
|
120
132
|
expect(result).not.toContain('more lines');
|
|
121
133
|
});
|
|
134
|
+
|
|
135
|
+
test('allows unbounded output when maxLines is null', () => {
|
|
136
|
+
const lines = Array.from({ length: 50 }, (_, i) => `line${i + 1}`);
|
|
137
|
+
const content = lines.join('\n');
|
|
138
|
+
const result = stripAnsi(formatNewFileDiff(content, 'all-lines.ts', null));
|
|
139
|
+
|
|
140
|
+
expect(result).toContain('+line1');
|
|
141
|
+
expect(result).toContain('+line50');
|
|
142
|
+
expect(result).not.toContain('more lines');
|
|
143
|
+
});
|
|
122
144
|
});
|
|
@@ -23,11 +23,16 @@ const ALL_SCENARIOS: GuardianActionMessageScenario[] = [
|
|
|
23
23
|
'guardian_followup_failed',
|
|
24
24
|
'guardian_followup_declined_ack',
|
|
25
25
|
'guardian_followup_clarification',
|
|
26
|
+
'guardian_pending_disambiguation',
|
|
26
27
|
'guardian_expired_disambiguation',
|
|
27
28
|
'guardian_followup_disambiguation',
|
|
28
29
|
'guardian_stale_answered',
|
|
29
30
|
'guardian_stale_expired',
|
|
30
31
|
'guardian_stale_followup',
|
|
32
|
+
'guardian_stale_superseded',
|
|
33
|
+
'guardian_superseded_remap',
|
|
34
|
+
'guardian_unknown_code',
|
|
35
|
+
'guardian_auto_matched',
|
|
31
36
|
'outbound_message_copy',
|
|
32
37
|
'followup_message_sent',
|
|
33
38
|
'followup_call_started',
|