@vellumai/assistant 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -21
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +16 -0
- package/src/__tests__/app-git-history.test.ts +22 -27
- package/src/__tests__/app-git-service.test.ts +44 -78
- package/src/__tests__/call-orchestrator.test.ts +321 -0
- package/src/__tests__/channel-approval-routes.test.ts +1267 -93
- package/src/__tests__/channel-approval.test.ts +2 -0
- package/src/__tests__/channel-approvals.test.ts +51 -2
- package/src/__tests__/channel-delivery-store.test.ts +130 -1
- package/src/__tests__/channel-guardian.test.ts +371 -1
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-lifecycle.test.ts +635 -0
- package/src/__tests__/daemon-server-session-init.test.ts +5 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +106 -21
- package/src/__tests__/handlers-telegram-config.test.ts +82 -0
- package/src/__tests__/handlers-twilio-config.test.ts +738 -5
- package/src/__tests__/ingress-url-consistency.test.ts +64 -0
- package/src/__tests__/ipc-snapshot.test.ts +10 -0
- package/src/__tests__/run-orchestrator.test.ts +1 -1
- package/src/__tests__/secret-scanner.test.ts +223 -0
- package/src/__tests__/session-process-bridge.test.ts +2 -0
- package/src/__tests__/shell-parser-property.test.ts +357 -2
- package/src/__tests__/system-prompt.test.ts +25 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
- package/src/__tests__/tool-permission-simulate-handler.test.ts +2 -2
- package/src/__tests__/user-reference.test.ts +68 -0
- package/src/calls/call-orchestrator.ts +63 -11
- package/src/calls/twilio-config.ts +10 -1
- package/src/calls/twilio-rest.ts +70 -0
- package/src/cli/map.ts +6 -0
- package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
- package/src/commands/cc-command-registry.ts +14 -1
- package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
- package/src/config/bundled-skills/email-setup/SKILL.md +56 -0
- package/src/config/bundled-skills/messaging/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
- package/src/config/defaults.ts +1 -1
- package/src/config/schema.ts +6 -3
- package/src/config/skills.ts +5 -32
- package/src/config/system-prompt.ts +16 -0
- package/src/config/user-reference.ts +29 -0
- package/src/config/vellum-skills/catalog.json +52 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +49 -4
- package/src/daemon/auth-manager.ts +103 -0
- package/src/daemon/computer-use-session.ts +8 -1
- package/src/daemon/config-watcher.ts +253 -0
- package/src/daemon/handlers/config.ts +193 -17
- package/src/daemon/handlers/sessions.ts +5 -3
- package/src/daemon/handlers/skills.ts +60 -17
- package/src/daemon/ipc-contract-inventory.json +4 -0
- package/src/daemon/ipc-contract.ts +16 -0
- package/src/daemon/ipc-handler.ts +87 -0
- package/src/daemon/lifecycle.ts +16 -4
- package/src/daemon/ride-shotgun-handler.ts +11 -1
- package/src/daemon/server.ts +105 -502
- package/src/daemon/session-agent-loop.ts +9 -14
- package/src/daemon/session-process.ts +20 -3
- package/src/daemon/session-runtime-assembly.ts +60 -44
- package/src/daemon/session-slash.ts +50 -2
- package/src/daemon/session-surfaces.ts +17 -1
- package/src/daemon/session.ts +8 -1
- package/src/inbound/public-ingress-urls.ts +20 -3
- package/src/index.ts +1 -23
- package/src/memory/app-git-service.ts +24 -0
- package/src/memory/app-store.ts +0 -21
- package/src/memory/channel-delivery-store.ts +74 -3
- package/src/memory/channel-guardian-store.ts +54 -26
- package/src/memory/conversation-key-store.ts +20 -0
- package/src/memory/conversation-store.ts +14 -2
- package/src/memory/db-connection.ts +28 -0
- package/src/memory/db-init.ts +1019 -0
- package/src/memory/db.ts +2 -1995
- package/src/memory/embedding-backend.ts +79 -11
- package/src/memory/indexer.ts +2 -0
- package/src/memory/job-utils.ts +64 -4
- package/src/memory/jobs-worker.ts +7 -1
- package/src/memory/recall-cache.ts +107 -0
- package/src/memory/retriever.ts +30 -1
- package/src/memory/schema-migration.ts +984 -0
- package/src/memory/schema.ts +6 -0
- package/src/memory/search/types.ts +2 -0
- package/src/permissions/prompter.ts +14 -3
- package/src/permissions/trust-store.ts +7 -0
- package/src/runtime/channel-approvals.ts +17 -3
- package/src/runtime/gateway-client.ts +2 -1
- package/src/runtime/http-server.ts +28 -9
- package/src/runtime/routes/channel-routes.ts +279 -100
- package/src/runtime/routes/run-routes.ts +7 -1
- package/src/runtime/run-orchestrator.ts +8 -1
- package/src/security/secret-scanner.ts +218 -0
- package/src/skills/clawhub.ts +6 -2
- package/src/skills/frontmatter.ts +63 -0
- package/src/skills/slash-commands.ts +23 -0
- package/src/skills/vellum-catalog-remote.ts +107 -0
- package/src/subagent/manager.ts +4 -1
- package/src/subagent/types.ts +2 -0
- package/src/tools/browser/auto-navigate.ts +132 -24
- package/src/tools/browser/browser-manager.ts +67 -61
- package/src/tools/claude-code/claude-code.ts +55 -3
- package/src/tools/executor.ts +10 -2
- package/src/tools/skills/vellum-catalog.ts +75 -127
- package/src/tools/subagent/spawn.ts +2 -0
- package/src/tools/terminal/parser.ts +21 -5
- package/src/util/platform.ts +8 -1
- package/src/util/retry.ts +4 -4
|
@@ -71,6 +71,9 @@ mock.module('../config/loader.js', () => ({
|
|
|
71
71
|
ingress: {
|
|
72
72
|
publicBaseUrl: 'https://test.example.com',
|
|
73
73
|
},
|
|
74
|
+
sms: {
|
|
75
|
+
phoneNumber: '+15550001111',
|
|
76
|
+
},
|
|
74
77
|
}),
|
|
75
78
|
getConfig: () => ({
|
|
76
79
|
model: 'test',
|
|
@@ -82,6 +85,9 @@ mock.module('../config/loader.js', () => ({
|
|
|
82
85
|
ingress: {
|
|
83
86
|
publicBaseUrl: 'https://test.example.com',
|
|
84
87
|
},
|
|
88
|
+
sms: {
|
|
89
|
+
phoneNumber: '+15550001111',
|
|
90
|
+
},
|
|
85
91
|
}),
|
|
86
92
|
invalidateConfigCache: () => {},
|
|
87
93
|
}));
|
|
@@ -96,31 +102,28 @@ mock.module('../calls/twilio-provider.js', () => ({
|
|
|
96
102
|
},
|
|
97
103
|
}));
|
|
98
104
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
phoneNumber: '+15550001111',
|
|
105
|
-
webhookBaseUrl: 'https://test.example.com',
|
|
106
|
-
wssBaseUrl: 'wss://test.example.com',
|
|
107
|
-
}),
|
|
108
|
-
}));
|
|
105
|
+
const secureKeyStore: Record<string, string | undefined> = {
|
|
106
|
+
'credential:twilio:account_sid': 'AC_test',
|
|
107
|
+
'credential:twilio:auth_token': 'test_token',
|
|
108
|
+
'credential:twilio:phone_number': '+15550001111',
|
|
109
|
+
};
|
|
109
110
|
|
|
110
111
|
mock.module('../security/secure-keys.js', () => ({
|
|
111
|
-
getSecureKey: () => null,
|
|
112
|
-
setSecureKey: () =>
|
|
113
|
-
|
|
112
|
+
getSecureKey: (key: string) => secureKeyStore[key] ?? null,
|
|
113
|
+
setSecureKey: (key: string, value: string) => {
|
|
114
|
+
secureKeyStore[key] = value;
|
|
115
|
+
return true;
|
|
116
|
+
},
|
|
117
|
+
deleteSecureKey: (key: string) => {
|
|
118
|
+
delete secureKeyStore[key];
|
|
119
|
+
},
|
|
114
120
|
}));
|
|
115
121
|
|
|
116
|
-
mock
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
getTwilioConnectActionUrl: () => 'https://test.example.com/webhooks/twilio/connect-action',
|
|
122
|
-
getOAuthCallbackUrl: () => 'https://test.example.com/webhooks/oauth/callback',
|
|
123
|
-
}));
|
|
122
|
+
// NOTE: Do NOT mock '../inbound/public-ingress-urls.js' here.
|
|
123
|
+
// Those are pure functions that derive URLs from the config object returned by
|
|
124
|
+
// loadConfig() (which is already mocked above). Mocking them at the module level
|
|
125
|
+
// leaks into other test files (e.g. ingress-url-consistency.test.ts) that need
|
|
126
|
+
// the real implementations, causing cross-test contamination.
|
|
124
127
|
|
|
125
128
|
// Mock the oauth callback registry
|
|
126
129
|
mock.module('../security/oauth-callback-registry.js', () => ({
|
|
@@ -289,6 +292,49 @@ describe('gateway-only ingress enforcement', () => {
|
|
|
289
292
|
});
|
|
290
293
|
});
|
|
291
294
|
|
|
295
|
+
// ── SMS-specific direct webhook routes blocked ──────────────────────
|
|
296
|
+
|
|
297
|
+
describe('SMS webhook routes are blocked at the runtime (gateway-only)', () => {
|
|
298
|
+
|
|
299
|
+
test('POST /webhooks/twilio/sms returns 410 (cannot bypass gateway)', async () => {
|
|
300
|
+
const res = await fetch(`http://127.0.0.1:${port}/webhooks/twilio/sms`, {
|
|
301
|
+
method: 'POST',
|
|
302
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
303
|
+
body: makeFormBody({ Body: 'hello', From: '+15551234567', To: '+15559876543', MessageSid: 'SM123' }),
|
|
304
|
+
});
|
|
305
|
+
expect(res.status).toBe(410);
|
|
306
|
+
const body = await res.json() as { error: string; code: string };
|
|
307
|
+
expect(body.code).toBe('GATEWAY_ONLY');
|
|
308
|
+
expect(body.error).toContain('Direct webhook access disabled');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test('POST /v1/calls/twilio/sms returns 410 (legacy path also blocked)', async () => {
|
|
312
|
+
const res = await fetch(`http://127.0.0.1:${port}/v1/calls/twilio/sms`, {
|
|
313
|
+
method: 'POST',
|
|
314
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
315
|
+
body: makeFormBody({ Body: 'hello', From: '+15551234567', MessageSid: 'SM456' }),
|
|
316
|
+
});
|
|
317
|
+
expect(res.status).toBe(410);
|
|
318
|
+
const body = await res.json() as { error: string; code: string };
|
|
319
|
+
expect(body.code).toBe('GATEWAY_ONLY');
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('POST /webhooks/twilio/sms with valid auth still returns 410 (auth does not bypass gateway-only)', async () => {
|
|
323
|
+
const res = await fetch(`http://127.0.0.1:${port}/webhooks/twilio/sms`, {
|
|
324
|
+
method: 'POST',
|
|
325
|
+
headers: {
|
|
326
|
+
...AUTH_HEADERS,
|
|
327
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
328
|
+
},
|
|
329
|
+
body: makeFormBody({ Body: 'sneaky', From: '+15551234567', MessageSid: 'SM789' }),
|
|
330
|
+
});
|
|
331
|
+
// The gateway-only guard runs before auth for Twilio webhook paths
|
|
332
|
+
expect(res.status).toBe(410);
|
|
333
|
+
const body = await res.json() as { error: string; code: string };
|
|
334
|
+
expect(body.code).toBe('GATEWAY_ONLY');
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
292
338
|
// ── Internal forwarding routes still work ─────
|
|
293
339
|
|
|
294
340
|
describe('internal forwarding routes are not blocked', () => {
|
|
@@ -601,6 +647,45 @@ describe('gateway-only ingress enforcement', () => {
|
|
|
601
647
|
// header, the request is rejected if bearer auth is missing.
|
|
602
648
|
expect(res.status).toBe(401);
|
|
603
649
|
});
|
|
650
|
+
|
|
651
|
+
test('POST /v1/channels/inbound with SMS sourceChannel requires X-Gateway-Origin', async () => {
|
|
652
|
+
const res = await fetch(`http://127.0.0.1:${port}/v1/channels/inbound`, {
|
|
653
|
+
method: 'POST',
|
|
654
|
+
headers: {
|
|
655
|
+
...AUTH_HEADERS,
|
|
656
|
+
'Content-Type': 'application/json',
|
|
657
|
+
},
|
|
658
|
+
body: JSON.stringify({
|
|
659
|
+
sourceChannel: 'sms',
|
|
660
|
+
externalChatId: '+15551234567',
|
|
661
|
+
externalMessageId: 'SM-test-gw-1',
|
|
662
|
+
content: 'hello via SMS',
|
|
663
|
+
}),
|
|
664
|
+
});
|
|
665
|
+
// SMS messages must also go through the gateway — missing X-Gateway-Origin is rejected.
|
|
666
|
+
expect(res.status).toBe(403);
|
|
667
|
+
const body = await res.json() as { error: string; code: string };
|
|
668
|
+
expect(body.code).toBe('GATEWAY_ORIGIN_REQUIRED');
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
test('POST /v1/channels/inbound with SMS sourceChannel and valid X-Gateway-Origin passes', async () => {
|
|
672
|
+
const res = await fetch(`http://127.0.0.1:${port}/v1/channels/inbound`, {
|
|
673
|
+
method: 'POST',
|
|
674
|
+
headers: {
|
|
675
|
+
...AUTH_HEADERS,
|
|
676
|
+
'Content-Type': 'application/json',
|
|
677
|
+
'X-Gateway-Origin': TEST_TOKEN,
|
|
678
|
+
},
|
|
679
|
+
body: JSON.stringify({
|
|
680
|
+
sourceChannel: 'sms',
|
|
681
|
+
externalChatId: '+15551234567',
|
|
682
|
+
externalMessageId: 'SM-test-gw-2',
|
|
683
|
+
content: 'hello via SMS',
|
|
684
|
+
}),
|
|
685
|
+
});
|
|
686
|
+
// Should NOT be 403 — the gateway-origin check passes.
|
|
687
|
+
expect(res.status).not.toBe(403);
|
|
688
|
+
});
|
|
604
689
|
});
|
|
605
690
|
|
|
606
691
|
// ── Startup warning for non-loopback host ──────────────────────────
|
|
@@ -905,6 +905,7 @@ describe('Telegram config handler', () => {
|
|
|
905
905
|
|
|
906
906
|
import { handleGuardianVerification } from '../daemon/handlers/config.js';
|
|
907
907
|
import type { GuardianVerificationRequest } from '../daemon/ipc-contract.js';
|
|
908
|
+
import { createBinding } from '../memory/channel-guardian-store.js';
|
|
908
909
|
|
|
909
910
|
describe('Guardian verification IPC actions', () => {
|
|
910
911
|
beforeEach(() => {
|
|
@@ -965,4 +966,85 @@ describe('Guardian verification IPC actions', () => {
|
|
|
965
966
|
expect(res.success).toBe(false);
|
|
966
967
|
expect(res.error).toContain('Unknown action');
|
|
967
968
|
});
|
|
969
|
+
|
|
970
|
+
test('create_challenge with explicit assistantId scopes challenge to that assistant', () => {
|
|
971
|
+
const msg: GuardianVerificationRequest = {
|
|
972
|
+
type: 'guardian_verification',
|
|
973
|
+
action: 'create_challenge',
|
|
974
|
+
channel: 'telegram',
|
|
975
|
+
assistantId: 'asst-ipc-X',
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
const { ctx, sent } = createTestContext();
|
|
979
|
+
handleGuardianVerification(msg, {} as net.Socket, ctx);
|
|
980
|
+
|
|
981
|
+
expect(sent).toHaveLength(1);
|
|
982
|
+
const res = sent[0] as { type: string; success: boolean; secret?: string; instruction?: string };
|
|
983
|
+
expect(res.success).toBe(true);
|
|
984
|
+
expect(res.secret).toBeDefined();
|
|
985
|
+
expect(res.instruction).toContain('/guardian_verify');
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
test('status action with explicit assistantId checks binding for that assistant', () => {
|
|
989
|
+
// Create a control binding for a known assistant so we can verify
|
|
990
|
+
// that querying a *different* assistantId actually returns bound=false
|
|
991
|
+
// (not just because no bindings exist at all).
|
|
992
|
+
createBinding({
|
|
993
|
+
assistantId: 'asst-ipc-bound',
|
|
994
|
+
channel: 'telegram',
|
|
995
|
+
guardianExternalUserId: 'guardian-user-1',
|
|
996
|
+
guardianDeliveryChatId: 'guardian-chat-1',
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
// Querying a different assistant should return bound=false
|
|
1000
|
+
const unboundMsg: GuardianVerificationRequest = {
|
|
1001
|
+
type: 'guardian_verification',
|
|
1002
|
+
action: 'status',
|
|
1003
|
+
channel: 'telegram',
|
|
1004
|
+
assistantId: 'asst-ipc-Y',
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
const { ctx: ctx1, sent: sent1 } = createTestContext();
|
|
1008
|
+
handleGuardianVerification(unboundMsg, {} as net.Socket, ctx1);
|
|
1009
|
+
|
|
1010
|
+
expect(sent1).toHaveLength(1);
|
|
1011
|
+
const unboundRes = sent1[0] as { type: string; success: boolean; bound: boolean };
|
|
1012
|
+
expect(unboundRes.success).toBe(true);
|
|
1013
|
+
expect(unboundRes.bound).toBe(false);
|
|
1014
|
+
|
|
1015
|
+
// Querying the bound assistant should return bound=true
|
|
1016
|
+
const boundMsg: GuardianVerificationRequest = {
|
|
1017
|
+
type: 'guardian_verification',
|
|
1018
|
+
action: 'status',
|
|
1019
|
+
channel: 'telegram',
|
|
1020
|
+
assistantId: 'asst-ipc-bound',
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
const { ctx: ctx2, sent: sent2 } = createTestContext();
|
|
1024
|
+
handleGuardianVerification(boundMsg, {} as net.Socket, ctx2);
|
|
1025
|
+
|
|
1026
|
+
expect(sent2).toHaveLength(1);
|
|
1027
|
+
const boundRes = sent2[0] as { type: string; success: boolean; bound: boolean; guardianExternalUserId?: string };
|
|
1028
|
+
expect(boundRes.success).toBe(true);
|
|
1029
|
+
expect(boundRes.bound).toBe(true);
|
|
1030
|
+
expect(boundRes.guardianExternalUserId).toBe('guardian-user-1');
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
test('assistantId defaults to "self" when not provided', () => {
|
|
1034
|
+
// create_challenge without assistantId should scope to 'self'
|
|
1035
|
+
const createMsg: GuardianVerificationRequest = {
|
|
1036
|
+
type: 'guardian_verification',
|
|
1037
|
+
action: 'create_challenge',
|
|
1038
|
+
channel: 'telegram',
|
|
1039
|
+
// assistantId intentionally omitted
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
const { ctx: ctx1, sent: sent1 } = createTestContext();
|
|
1043
|
+
handleGuardianVerification(createMsg, {} as net.Socket, ctx1);
|
|
1044
|
+
|
|
1045
|
+
expect(sent1).toHaveLength(1);
|
|
1046
|
+
const createRes = sent1[0] as { type: string; success: boolean; secret?: string };
|
|
1047
|
+
expect(createRes.success).toBe(true);
|
|
1048
|
+
expect(createRes.secret).toBeDefined();
|
|
1049
|
+
});
|
|
968
1050
|
});
|