@vellumai/assistant 0.5.14 → 0.5.16
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 +2 -2
- package/docs/architecture/integrations.md +15 -14
- package/knip.json +3 -1
- package/openapi.yaml +11 -43
- package/package.json +1 -1
- package/src/__tests__/assistant-feature-flags-integration.test.ts +3 -375
- package/src/__tests__/ces-rpc-credential-backend.test.ts +4 -1
- package/src/__tests__/checker.test.ts +59 -0
- package/src/__tests__/cli-command-risk-guard.test.ts +98 -10
- package/src/__tests__/cli-memory.test.ts +372 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +12 -2
- package/src/__tests__/config-schema.test.ts +0 -2
- package/src/__tests__/config-watcher-feature-flags.test.ts +211 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +7 -4
- package/src/__tests__/conversation-slash-commands.test.ts +2 -6
- package/src/__tests__/conversation-usage.test.ts +1 -0
- package/src/__tests__/credential-security-e2e.test.ts +4 -1
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +7 -73
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -7
- package/src/__tests__/guardian-routing-invariants.test.ts +151 -0
- package/src/__tests__/heartbeat-service.test.ts +1 -3
- package/src/__tests__/intent-routing.test.ts +6 -18
- package/src/__tests__/log-export-workspace.test.ts +2 -28
- package/src/__tests__/managed-skill-lifecycle.test.ts +7 -37
- package/src/__tests__/managed-store.test.ts +2 -10
- package/src/__tests__/messaging-send-tool.test.ts +6 -6
- package/src/__tests__/migration-cross-version-compatibility.test.ts +1 -29
- package/src/__tests__/migration-export-http.test.ts +3 -34
- package/src/__tests__/migration-import-commit-http.test.ts +1 -29
- package/src/__tests__/migration-import-preflight-http.test.ts +3 -34
- package/src/__tests__/no-domain-routing-in-prompt-guard.test.ts +2 -1
- package/src/__tests__/oauth-apps-routes.test.ts +120 -10
- package/src/__tests__/oauth-connect-orchestrator.test.ts +709 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +2 -1
- package/src/__tests__/oauth-provider-visibility.test.ts +149 -0
- package/src/__tests__/oauth-providers-routes.test.ts +5 -2
- package/src/__tests__/oauth-store.test.ts +0 -5
- package/src/__tests__/outlook-messaging-provider.test.ts +576 -0
- package/src/__tests__/path-policy.test.ts +2 -17
- package/src/__tests__/permission-types.test.ts +0 -1
- package/src/__tests__/platform-callback-registration.test.ts +3 -7
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
- package/src/__tests__/provider-error-scenarios.test.ts +0 -2
- package/src/__tests__/qdrant-manager.test.ts +68 -21
- package/src/__tests__/require-fresh-approval.test.ts +0 -1
- package/src/__tests__/sandbox-diagnostics.test.ts +20 -29
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +2 -10
- package/src/__tests__/secret-allowlist.test.ts +20 -35
- package/src/__tests__/shell-credential-ref.test.ts +0 -5
- package/src/__tests__/skill-load-feature-flag.test.ts +2 -43
- package/src/__tests__/skill-load-inline-command.test.ts +3 -65
- package/src/__tests__/skill-load-inline-includes.test.ts +3 -65
- package/src/__tests__/skill-load-tool.test.ts +3 -67
- package/src/__tests__/skill-memory.test.ts +362 -119
- package/src/__tests__/skills.test.ts +22 -49
- package/src/__tests__/slack-channel-config.test.ts +2 -21
- package/src/__tests__/starter-bundle.test.ts +2 -8
- package/src/__tests__/stt-hints.test.ts +7 -2
- package/src/__tests__/system-prompt.test.ts +25 -45
- package/src/__tests__/task-compiler.test.ts +0 -21
- package/src/__tests__/task-management-tools.test.ts +0 -21
- package/src/__tests__/task-memory-cleanup.test.ts +0 -21
- package/src/__tests__/task-runner.test.ts +0 -21
- package/src/__tests__/task-scheduler.test.ts +0 -21
- package/src/__tests__/terminal-tools.test.ts +1 -17
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +0 -79
- package/src/__tests__/tool-approval-handler.test.ts +1 -20
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -11
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -25
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
- package/src/__tests__/tool-executor.test.ts +0 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +1 -20
- package/src/__tests__/tool-preview-lifecycle.test.ts +0 -20
- package/src/__tests__/trust-store.test.ts +9 -41
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -30
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -21
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -22
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -22
- package/src/__tests__/trusted-contact-verification.test.ts +0 -22
- package/src/__tests__/turn-boundary-resolution.test.ts +0 -28
- package/src/__tests__/twilio-provider.test.ts +0 -16
- package/src/__tests__/twilio-routes-twiml.test.ts +7 -12
- package/src/__tests__/twilio-routes.test.ts +0 -24
- package/src/__tests__/update-bulletin.test.ts +17 -89
- package/src/__tests__/usage-cache-backfill-migration.test.ts +0 -20
- package/src/__tests__/usage-routes.test.ts +0 -21
- package/src/__tests__/user-reference.test.ts +1 -5
- package/src/__tests__/vbundle-pax-and-symlink.test.ts +4 -34
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +2 -53
- package/src/__tests__/voice-invite-redemption.test.ts +0 -21
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -24
- package/src/__tests__/voice-session-bridge.test.ts +0 -21
- package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -23
- package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +2 -2
- package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +2 -23
- package/src/__tests__/workspace-migration-down-functions.test.ts +0 -6
- package/src/acp/client-handler.ts +1 -2
- package/src/cli/__tests__/notifications.test.ts +0 -22
- package/src/cli/cli-memory.ts +176 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
- package/src/cli/commands/oauth/connect.ts +15 -0
- package/src/cli/commands/oauth/providers.ts +49 -42
- package/src/cli/commands/platform/__tests__/connect.test.ts +2 -48
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +2 -48
- package/src/cli/commands/platform/__tests__/status.test.ts +0 -50
- package/src/config/bundled-skills/computer-use/TOOLS.json +7 -7
- package/src/config/bundled-skills/messaging/SKILL.md +17 -2
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/loader.ts +4 -0
- package/src/config/schemas/security.ts +0 -6
- package/src/config/schemas/services.ts +8 -0
- package/src/context/window-manager.ts +28 -9
- package/src/credential-execution/approval-bridge.ts +0 -1
- package/src/daemon/config-watcher.ts +51 -0
- package/src/daemon/conversation-agent-loop.ts +3 -2
- package/src/daemon/conversation-process.ts +1 -0
- package/src/daemon/conversation-usage.ts +1 -0
- package/src/daemon/handlers/skills.ts +9 -1
- package/src/daemon/lifecycle.ts +13 -4
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/providers-setup.ts +2 -0
- package/src/daemon/server.ts +26 -22
- package/src/events/domain-events.ts +1 -2
- package/src/memory/db-init.ts +9 -0
- package/src/memory/job-handlers/batch-extraction.ts +16 -4
- package/src/memory/job-handlers/embedding.test.ts +3 -27
- package/src/memory/job-handlers/journal-carry-forward.test.ts +1 -29
- package/src/memory/llm-usage-store.ts +35 -2
- package/src/memory/migrations/201-oauth-providers-feature-flag.ts +11 -0
- package/src/memory/migrations/202-drop-callback-transport-column.ts +13 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/qdrant-manager.ts +26 -5
- package/src/memory/query-expansion.ts +1 -1
- package/src/memory/retriever.test.ts +22 -20
- package/src/memory/retriever.ts +10 -2
- package/src/memory/schema/oauth.ts +1 -1
- package/src/memory/search/mmr.ts +8 -5
- package/src/memory/slack-thread-store.ts +17 -0
- package/src/messaging/providers/outlook/adapter.ts +193 -0
- package/src/messaging/providers/outlook/client.ts +311 -0
- package/src/messaging/providers/outlook/types.ts +83 -0
- package/src/notifications/adapters/slack.ts +1 -1
- package/src/oauth/__tests__/identity-verifier.test.ts +1 -1
- package/src/oauth/connect-orchestrator.ts +10 -3
- package/src/oauth/oauth-store.ts +10 -11
- package/src/oauth/provider-serializer.ts +3 -0
- package/src/oauth/provider-visibility.ts +16 -0
- package/src/oauth/seed-providers.ts +49 -17
- package/src/permissions/checker.ts +39 -7
- package/src/permissions/types.ts +2 -4
- package/src/prompts/journal-context.ts +9 -11
- package/src/prompts/system-prompt.ts +3 -64
- package/src/prompts/templates/UPDATES.md +6 -0
- package/src/runtime/auth/__tests__/credential-service.test.ts +1 -27
- package/src/runtime/auth/__tests__/token-service.test.ts +1 -25
- package/src/runtime/auth/route-policy.ts +0 -4
- package/src/runtime/guardian-reply-router.ts +6 -2
- package/src/runtime/routes/conversation-query-routes.ts +2 -58
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +43 -2
- package/src/runtime/routes/memory-item-routes.test.ts +0 -17
- package/src/runtime/routes/memory-item-routes.ts +103 -12
- package/src/runtime/routes/oauth-apps.ts +18 -1
- package/src/runtime/routes/oauth-providers.ts +13 -1
- package/src/runtime/routes/settings-routes.ts +1 -0
- package/src/runtime/routes/usage-routes.ts +19 -2
- package/src/runtime/routes/work-items-routes.test.ts +0 -21
- package/src/runtime/routes/workspace-routes.test.ts +3 -27
- package/src/security/secret-allowlist.ts +4 -4
- package/src/skills/skill-memory.ts +62 -23
- package/src/tools/memory/handlers.test.ts +1 -29
- package/src/tools/permission-checker.ts +0 -18
- package/src/tools/skills/skill-script-runner.ts +1 -1
- package/src/util/device-id.ts +3 -65
- package/src/workspace/git-service.ts +27 -6
|
@@ -5,11 +5,11 @@ import {
|
|
|
5
5
|
readdirSync,
|
|
6
6
|
readFileSync,
|
|
7
7
|
rmSync,
|
|
8
|
+
symlinkSync,
|
|
8
9
|
writeFileSync,
|
|
9
10
|
} from "node:fs";
|
|
10
11
|
import { join } from "node:path";
|
|
11
12
|
import {
|
|
12
|
-
afterAll,
|
|
13
13
|
afterEach,
|
|
14
14
|
beforeAll,
|
|
15
15
|
beforeEach,
|
|
@@ -19,11 +19,7 @@ import {
|
|
|
19
19
|
test,
|
|
20
20
|
} from "bun:test";
|
|
21
21
|
|
|
22
|
-
const testDataDir =
|
|
23
|
-
|
|
24
|
-
mock.module("../util/platform.js", () => ({
|
|
25
|
-
getDataDir: () => testDataDir,
|
|
26
|
-
}));
|
|
22
|
+
const testDataDir = process.env.VELLUM_WORKSPACE_DIR!;
|
|
27
23
|
|
|
28
24
|
mock.module("../util/logger.js", () => ({
|
|
29
25
|
getLogger: () =>
|
|
@@ -42,7 +38,7 @@ const FAST_TIMEOUTS = {
|
|
|
42
38
|
} as const;
|
|
43
39
|
|
|
44
40
|
function placeFakeBinary(script: string): string {
|
|
45
|
-
const binaryPath = join(testDataDir, "qdrant", "bin", "qdrant");
|
|
41
|
+
const binaryPath = join(testDataDir, "data", "qdrant", "bin", "qdrant");
|
|
46
42
|
writeFileSync(binaryPath, script);
|
|
47
43
|
chmodSync(binaryPath, 0o755);
|
|
48
44
|
return binaryPath;
|
|
@@ -53,7 +49,7 @@ function getTestPort(): number {
|
|
|
53
49
|
return nextPort++;
|
|
54
50
|
}
|
|
55
51
|
|
|
56
|
-
const qdrantDir = join(testDataDir, "qdrant");
|
|
52
|
+
const qdrantDir = join(testDataDir, "data", "qdrant");
|
|
57
53
|
const qdrantBinDir = join(qdrantDir, "bin");
|
|
58
54
|
|
|
59
55
|
beforeAll(() => {
|
|
@@ -79,10 +75,6 @@ afterEach(() => {
|
|
|
79
75
|
delete process.env.QDRANT_URL;
|
|
80
76
|
});
|
|
81
77
|
|
|
82
|
-
afterAll(() => {
|
|
83
|
-
rmSync(testDataDir, { recursive: true, force: true });
|
|
84
|
-
});
|
|
85
|
-
|
|
86
78
|
describe("QdrantManager", () => {
|
|
87
79
|
// ── Constructor ──────────────────────────────────────────────
|
|
88
80
|
|
|
@@ -147,7 +139,7 @@ describe("QdrantManager", () => {
|
|
|
147
139
|
|
|
148
140
|
describe("stop() without running process", () => {
|
|
149
141
|
test("removes stale PID file", async () => {
|
|
150
|
-
const pidPath = join(testDataDir, "qdrant", "qdrant.pid");
|
|
142
|
+
const pidPath = join(testDataDir, "data", "qdrant", "qdrant.pid");
|
|
151
143
|
writeFileSync(pidPath, "99999");
|
|
152
144
|
|
|
153
145
|
const mgr = new QdrantManager({ url: "http://127.0.0.1:6333" });
|
|
@@ -166,7 +158,7 @@ describe("QdrantManager", () => {
|
|
|
166
158
|
|
|
167
159
|
describe("stale PID cleanup during start()", () => {
|
|
168
160
|
test("removes PID file for non-existent process", async () => {
|
|
169
|
-
const pidPath = join(testDataDir, "qdrant", "qdrant.pid");
|
|
161
|
+
const pidPath = join(testDataDir, "data", "qdrant", "qdrant.pid");
|
|
170
162
|
writeFileSync(pidPath, "2147483647");
|
|
171
163
|
|
|
172
164
|
placeFakeBinary("#!/bin/sh\nexit 1");
|
|
@@ -187,7 +179,7 @@ describe("QdrantManager", () => {
|
|
|
187
179
|
}, 10_000);
|
|
188
180
|
|
|
189
181
|
test("handles invalid PID file contents", async () => {
|
|
190
|
-
const pidPath = join(testDataDir, "qdrant", "qdrant.pid");
|
|
182
|
+
const pidPath = join(testDataDir, "data", "qdrant", "qdrant.pid");
|
|
191
183
|
writeFileSync(pidPath, "garbage");
|
|
192
184
|
|
|
193
185
|
placeFakeBinary("#!/bin/sh\nexit 1");
|
|
@@ -208,7 +200,7 @@ describe("QdrantManager", () => {
|
|
|
208
200
|
}, 10_000);
|
|
209
201
|
|
|
210
202
|
test("handles empty PID file", async () => {
|
|
211
|
-
const pidPath = join(testDataDir, "qdrant", "qdrant.pid");
|
|
203
|
+
const pidPath = join(testDataDir, "data", "qdrant", "qdrant.pid");
|
|
212
204
|
writeFileSync(pidPath, "");
|
|
213
205
|
|
|
214
206
|
placeFakeBinary("#!/bin/sh\nexit 1");
|
|
@@ -233,7 +225,7 @@ describe("QdrantManager", () => {
|
|
|
233
225
|
|
|
234
226
|
describe("process lifecycle", () => {
|
|
235
227
|
test("writes PID file after spawning", async () => {
|
|
236
|
-
const pidPath = join(testDataDir, "qdrant", "qdrant.pid");
|
|
228
|
+
const pidPath = join(testDataDir, "data", "qdrant", "qdrant.pid");
|
|
237
229
|
|
|
238
230
|
// Binary that stays alive. We'll stop it before readyz times out.
|
|
239
231
|
placeFakeBinary("#!/bin/sh\nexec sleep 300");
|
|
@@ -265,7 +257,7 @@ describe("QdrantManager", () => {
|
|
|
265
257
|
}, 10_000);
|
|
266
258
|
|
|
267
259
|
test("stop() escalates to SIGKILL after grace period", async () => {
|
|
268
|
-
const pidPath = join(testDataDir, "qdrant", "qdrant.pid");
|
|
260
|
+
const pidPath = join(testDataDir, "data", "qdrant", "qdrant.pid");
|
|
269
261
|
|
|
270
262
|
// Binary that ignores SIGTERM
|
|
271
263
|
placeFakeBinary('#!/bin/sh\ntrap "" TERM\nexec sleep 300');
|
|
@@ -297,7 +289,7 @@ describe("QdrantManager", () => {
|
|
|
297
289
|
|
|
298
290
|
describe("start failure cleanup", () => {
|
|
299
291
|
test("cleans up process on readyz timeout", async () => {
|
|
300
|
-
const pidPath = join(testDataDir, "qdrant", "qdrant.pid");
|
|
292
|
+
const pidPath = join(testDataDir, "data", "qdrant", "qdrant.pid");
|
|
301
293
|
|
|
302
294
|
// Binary that stays alive but never serves readyz
|
|
303
295
|
placeFakeBinary("#!/bin/sh\nexec sleep 300");
|
|
@@ -313,7 +305,7 @@ describe("QdrantManager", () => {
|
|
|
313
305
|
}, 10_000);
|
|
314
306
|
|
|
315
307
|
test("fails fast with exit code when process exits immediately", async () => {
|
|
316
|
-
const pidPath = join(testDataDir, "qdrant", "qdrant.pid");
|
|
308
|
+
const pidPath = join(testDataDir, "data", "qdrant", "qdrant.pid");
|
|
317
309
|
|
|
318
310
|
// GIVEN a Qdrant binary that exits immediately with code 1
|
|
319
311
|
placeFakeBinary("#!/bin/sh\nexit 1");
|
|
@@ -372,8 +364,63 @@ describe("QdrantManager", () => {
|
|
|
372
364
|
/* readyz timeout */
|
|
373
365
|
}
|
|
374
366
|
|
|
375
|
-
const binaryPath = join(testDataDir, "qdrant", "bin", "qdrant");
|
|
367
|
+
const binaryPath = join(testDataDir, "data", "qdrant", "bin", "qdrant");
|
|
376
368
|
expect(existsSync(binaryPath)).toBe(true);
|
|
377
369
|
}, 10_000);
|
|
378
370
|
});
|
|
371
|
+
|
|
372
|
+
// ── Symlink Safety ────────────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
describe("vellum-qdrant symlink safety", () => {
|
|
375
|
+
test("ignores pre-existing non-symlink vellum-qdrant file", async () => {
|
|
376
|
+
const realMarkerPath = join(qdrantDir, "real-executed.txt");
|
|
377
|
+
const hijackMarkerPath = join(qdrantDir, "hijack-executed.txt");
|
|
378
|
+
|
|
379
|
+
placeFakeBinary(`#!/bin/sh\necho real > "${realMarkerPath}"\nexit 1`);
|
|
380
|
+
|
|
381
|
+
const hijackPath = join(qdrantBinDir, "vellum-qdrant");
|
|
382
|
+
writeFileSync(
|
|
383
|
+
hijackPath,
|
|
384
|
+
`#!/bin/sh\necho hijack > "${hijackMarkerPath}"\nexit 0`,
|
|
385
|
+
);
|
|
386
|
+
chmodSync(hijackPath, 0o755);
|
|
387
|
+
|
|
388
|
+
const port = getTestPort();
|
|
389
|
+
const mgr = new QdrantManager({
|
|
390
|
+
url: `http://127.0.0.1:${port}`,
|
|
391
|
+
...FAST_TIMEOUTS,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
await expect(mgr.start()).rejects.toThrow("before becoming ready");
|
|
395
|
+
expect(existsSync(realMarkerPath)).toBe(true);
|
|
396
|
+
expect(existsSync(hijackMarkerPath)).toBe(false);
|
|
397
|
+
}, 10_000);
|
|
398
|
+
|
|
399
|
+
test("ignores symlink that does not point to real qdrant binary", async () => {
|
|
400
|
+
const realMarkerPath = join(qdrantDir, "real-executed.txt");
|
|
401
|
+
const hijackMarkerPath = join(qdrantDir, "hijack-executed.txt");
|
|
402
|
+
|
|
403
|
+
placeFakeBinary(`#!/bin/sh\necho real > "${realMarkerPath}"\nexit 1`);
|
|
404
|
+
|
|
405
|
+
const evilBinaryPath = join(qdrantBinDir, "evil-qdrant");
|
|
406
|
+
writeFileSync(
|
|
407
|
+
evilBinaryPath,
|
|
408
|
+
`#!/bin/sh\necho hijack > "${hijackMarkerPath}"\nexit 0`,
|
|
409
|
+
);
|
|
410
|
+
chmodSync(evilBinaryPath, 0o755);
|
|
411
|
+
|
|
412
|
+
const vellumQdrantPath = join(qdrantBinDir, "vellum-qdrant");
|
|
413
|
+
symlinkSync(evilBinaryPath, vellumQdrantPath);
|
|
414
|
+
|
|
415
|
+
const port = getTestPort();
|
|
416
|
+
const mgr = new QdrantManager({
|
|
417
|
+
url: `http://127.0.0.1:${port}`,
|
|
418
|
+
...FAST_TIMEOUTS,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
await expect(mgr.start()).rejects.toThrow("before becoming ready");
|
|
422
|
+
expect(existsSync(realMarkerPath)).toBe(true);
|
|
423
|
+
expect(existsSync(hijackMarkerPath)).toBe(false);
|
|
424
|
+
}, 10_000);
|
|
425
|
+
});
|
|
379
426
|
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as realChildProcess from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
4
3
|
|
|
5
4
|
const execSyncMock = mock(
|
|
6
5
|
(_command: string, _opts?: unknown): unknown => undefined,
|
|
@@ -11,25 +10,6 @@ mock.module("node:child_process", () => ({
|
|
|
11
10
|
execSync: execSyncMock,
|
|
12
11
|
}));
|
|
13
12
|
|
|
14
|
-
// Mock platform detection — default to macOS
|
|
15
|
-
let mockIsMacOS = true;
|
|
16
|
-
let mockIsLinux = false;
|
|
17
|
-
|
|
18
|
-
mock.module("../util/platform.js", () => ({
|
|
19
|
-
isMacOS: () => mockIsMacOS,
|
|
20
|
-
isLinux: () => mockIsLinux,
|
|
21
|
-
getProtectedDir: () => join("/tmp/vellum-test", "protected"),
|
|
22
|
-
getDataDir: () => "/tmp/vellum-test/data",
|
|
23
|
-
getDbPath: () => "/tmp/vellum-test/data/db/assistant.db",
|
|
24
|
-
getLogPath: () => "/tmp/vellum-test/data/logs/daemon.log",
|
|
25
|
-
getSandboxRootDir: () => "/tmp/vellum-test/sandbox",
|
|
26
|
-
getSandboxWorkingDir: () => "/tmp/vellum-test/workspace",
|
|
27
|
-
ensureDataDir: () => {},
|
|
28
|
-
getHistoryPath: () => "/tmp/vellum-test/data/history",
|
|
29
|
-
getHooksDir: () => "/tmp/vellum-test/hooks",
|
|
30
|
-
getPidPath: () => "/tmp/vellum-test/data/daemon.pid",
|
|
31
|
-
}));
|
|
32
|
-
|
|
33
13
|
// Mock config loader — return a config with sandbox settings
|
|
34
14
|
let mockSandboxConfig: {
|
|
35
15
|
enabled: boolean;
|
|
@@ -58,10 +38,17 @@ mock.module("../util/logger.js", () => ({
|
|
|
58
38
|
const { runSandboxDiagnostics } =
|
|
59
39
|
await import("../tools/terminal/sandbox-diagnostics.js");
|
|
60
40
|
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Helper: temporarily override process.platform for a test
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
const originalPlatform = process.platform;
|
|
45
|
+
|
|
46
|
+
function setPlatform(platform: string): void {
|
|
47
|
+
Object.defineProperty(process, "platform", { value: platform });
|
|
48
|
+
}
|
|
49
|
+
|
|
61
50
|
beforeEach(() => {
|
|
62
51
|
execSyncMock.mockReset();
|
|
63
|
-
mockIsMacOS = true;
|
|
64
|
-
mockIsLinux = false;
|
|
65
52
|
mockSandboxConfig = {
|
|
66
53
|
enabled: true,
|
|
67
54
|
};
|
|
@@ -69,6 +56,11 @@ beforeEach(() => {
|
|
|
69
56
|
execSyncMock.mockImplementation(() => undefined);
|
|
70
57
|
});
|
|
71
58
|
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
// Restore the real platform after every test
|
|
61
|
+
setPlatform(originalPlatform);
|
|
62
|
+
});
|
|
63
|
+
|
|
72
64
|
describe("runSandboxDiagnostics — config reporting", () => {
|
|
73
65
|
test("reports sandbox enabled state", () => {
|
|
74
66
|
const result = runSandboxDiagnostics();
|
|
@@ -97,6 +89,7 @@ describe("runSandboxDiagnostics — active backend reason", () => {
|
|
|
97
89
|
|
|
98
90
|
describe("runSandboxDiagnostics — native backend check (macOS)", () => {
|
|
99
91
|
test("passes when sandbox-exec works on macOS", () => {
|
|
92
|
+
setPlatform("darwin");
|
|
100
93
|
const result = runSandboxDiagnostics();
|
|
101
94
|
const nativeCheck = result.checks.find((c) =>
|
|
102
95
|
c.label.includes("Native sandbox"),
|
|
@@ -107,6 +100,7 @@ describe("runSandboxDiagnostics — native backend check (macOS)", () => {
|
|
|
107
100
|
});
|
|
108
101
|
|
|
109
102
|
test("fails when sandbox-exec does not work on macOS", () => {
|
|
103
|
+
setPlatform("darwin");
|
|
110
104
|
execSyncMock.mockImplementation((cmd: string) => {
|
|
111
105
|
if (typeof cmd === "string" && cmd.includes("sandbox-exec")) {
|
|
112
106
|
throw new Error("not available");
|
|
@@ -124,8 +118,7 @@ describe("runSandboxDiagnostics — native backend check (macOS)", () => {
|
|
|
124
118
|
|
|
125
119
|
describe("runSandboxDiagnostics — native backend check (Linux)", () => {
|
|
126
120
|
test("passes when bwrap works on Linux", () => {
|
|
127
|
-
|
|
128
|
-
mockIsLinux = true;
|
|
121
|
+
setPlatform("linux");
|
|
129
122
|
const result = runSandboxDiagnostics();
|
|
130
123
|
const nativeCheck = result.checks.find((c) =>
|
|
131
124
|
c.label.includes("Native sandbox"),
|
|
@@ -136,8 +129,7 @@ describe("runSandboxDiagnostics — native backend check (Linux)", () => {
|
|
|
136
129
|
});
|
|
137
130
|
|
|
138
131
|
test("fails when bwrap is not available on Linux", () => {
|
|
139
|
-
|
|
140
|
-
mockIsLinux = true;
|
|
132
|
+
setPlatform("linux");
|
|
141
133
|
execSyncMock.mockImplementation((cmd: string) => {
|
|
142
134
|
if (typeof cmd === "string" && cmd.includes("bwrap")) {
|
|
143
135
|
throw new Error("not found");
|
|
@@ -156,8 +148,7 @@ describe("runSandboxDiagnostics — native backend check (Linux)", () => {
|
|
|
156
148
|
|
|
157
149
|
describe("runSandboxDiagnostics — native backend check (unsupported OS)", () => {
|
|
158
150
|
test("reports unsupported when neither macOS nor Linux", () => {
|
|
159
|
-
|
|
160
|
-
mockIsLinux = false;
|
|
151
|
+
setPlatform("win32");
|
|
161
152
|
const result = runSandboxDiagnostics();
|
|
162
153
|
const nativeCheck = result.checks.find((c) =>
|
|
163
154
|
c.label.includes("Native sandbox"),
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
-
import { mkdtempSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
2
|
import { join } from "node:path";
|
|
5
3
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
mock.module("../util/platform.js", () => ({
|
|
10
|
-
getProtectedDir: () => join(TEST_DIR, "protected"),
|
|
11
|
-
getWorkspaceSkillsDir: () => join(TEST_DIR, "skills"),
|
|
12
|
-
}));
|
|
5
|
+
const TEST_DIR = process.env.VELLUM_WORKSPACE_DIR!;
|
|
13
6
|
|
|
14
7
|
mock.module("../util/logger.js", () => ({
|
|
15
8
|
getLogger: () =>
|
|
@@ -30,12 +23,11 @@ function makeContext(): ToolContext {
|
|
|
30
23
|
}
|
|
31
24
|
|
|
32
25
|
beforeEach(() => {
|
|
33
|
-
TEST_DIR = mkdtempSync(join(tmpdir(), "scaffold-tool-test-"));
|
|
34
26
|
mkdirSync(join(TEST_DIR, "skills"), { recursive: true });
|
|
35
27
|
});
|
|
36
28
|
|
|
37
29
|
afterEach(() => {
|
|
38
|
-
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
30
|
+
rmSync(join(TEST_DIR, "skills"), { recursive: true, force: true });
|
|
39
31
|
});
|
|
40
32
|
|
|
41
33
|
describe("scaffold_managed_skill tool", () => {
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
1
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
let testDir: string;
|
|
5
|
+
const testDir = process.env.VELLUM_WORKSPACE_DIR!;
|
|
8
6
|
|
|
9
7
|
mock.module("../util/logger.js", () => ({
|
|
10
8
|
getLogger: () =>
|
|
@@ -13,11 +11,6 @@ mock.module("../util/logger.js", () => ({
|
|
|
13
11
|
}),
|
|
14
12
|
}));
|
|
15
13
|
|
|
16
|
-
mock.module("../util/platform.js", () => ({
|
|
17
|
-
getProtectedDir: () => join(testDir, "protected"),
|
|
18
|
-
getDataDir: () => testDir,
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
14
|
import {
|
|
22
15
|
isAllowlisted,
|
|
23
16
|
loadAllowlist,
|
|
@@ -27,21 +20,13 @@ import { scanText } from "../security/secret-scanner.js";
|
|
|
27
20
|
|
|
28
21
|
describe("secret-allowlist", () => {
|
|
29
22
|
beforeEach(() => {
|
|
30
|
-
testDir
|
|
31
|
-
tmpdir(),
|
|
32
|
-
`vellum-allowlist-test-${Date.now()}-${Math.random()
|
|
33
|
-
.toString(36)
|
|
34
|
-
.slice(2)}`,
|
|
35
|
-
);
|
|
36
|
-
mkdirSync(join(testDir, "protected"), { recursive: true });
|
|
23
|
+
mkdirSync(join(testDir, "data"), { recursive: true });
|
|
37
24
|
resetAllowlist();
|
|
38
25
|
});
|
|
39
26
|
|
|
40
27
|
afterEach(() => {
|
|
41
28
|
resetAllowlist();
|
|
42
|
-
|
|
43
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
44
|
-
}
|
|
29
|
+
rmSync(join(testDir, "data"), { recursive: true, force: true });
|
|
45
30
|
});
|
|
46
31
|
|
|
47
32
|
// -----------------------------------------------------------------------
|
|
@@ -56,7 +41,7 @@ describe("secret-allowlist", () => {
|
|
|
56
41
|
// -----------------------------------------------------------------------
|
|
57
42
|
test("[experimental] suppresses exact values", () => {
|
|
58
43
|
writeFileSync(
|
|
59
|
-
join(testDir, "
|
|
44
|
+
join(testDir, "data", "secret-allowlist.json"),
|
|
60
45
|
JSON.stringify({ values: ["my-test-api-key-12345"] }),
|
|
61
46
|
);
|
|
62
47
|
expect(isAllowlisted("my-test-api-key-12345")).toBe(true);
|
|
@@ -65,7 +50,7 @@ describe("secret-allowlist", () => {
|
|
|
65
50
|
|
|
66
51
|
test("[experimental] exact values are case-sensitive", () => {
|
|
67
52
|
writeFileSync(
|
|
68
|
-
join(testDir, "
|
|
53
|
+
join(testDir, "data", "secret-allowlist.json"),
|
|
69
54
|
JSON.stringify({ values: ["MyTestKey"] }),
|
|
70
55
|
);
|
|
71
56
|
expect(isAllowlisted("MyTestKey")).toBe(true);
|
|
@@ -77,7 +62,7 @@ describe("secret-allowlist", () => {
|
|
|
77
62
|
// -----------------------------------------------------------------------
|
|
78
63
|
test("[experimental] suppresses values matching a prefix", () => {
|
|
79
64
|
writeFileSync(
|
|
80
|
-
join(testDir, "
|
|
65
|
+
join(testDir, "data", "secret-allowlist.json"),
|
|
81
66
|
JSON.stringify({ prefixes: ["my-internal-"] }),
|
|
82
67
|
);
|
|
83
68
|
expect(isAllowlisted("my-internal-key-abc123")).toBe(true);
|
|
@@ -89,7 +74,7 @@ describe("secret-allowlist", () => {
|
|
|
89
74
|
// -----------------------------------------------------------------------
|
|
90
75
|
test("[experimental] suppresses values matching a regex pattern", () => {
|
|
91
76
|
writeFileSync(
|
|
92
|
-
join(testDir, "
|
|
77
|
+
join(testDir, "data", "secret-allowlist.json"),
|
|
93
78
|
JSON.stringify({ patterns: ["^ci-test-[a-z0-9]+$"] }),
|
|
94
79
|
);
|
|
95
80
|
expect(isAllowlisted("ci-test-abc123")).toBe(true);
|
|
@@ -98,7 +83,7 @@ describe("secret-allowlist", () => {
|
|
|
98
83
|
|
|
99
84
|
test("[experimental] invalid regex is skipped without crashing", () => {
|
|
100
85
|
writeFileSync(
|
|
101
|
-
join(testDir, "
|
|
86
|
+
join(testDir, "data", "secret-allowlist.json"),
|
|
102
87
|
JSON.stringify({ patterns: ["[invalid", "^valid$"] }),
|
|
103
88
|
);
|
|
104
89
|
loadAllowlist();
|
|
@@ -113,7 +98,7 @@ describe("secret-allowlist", () => {
|
|
|
113
98
|
// -----------------------------------------------------------------------
|
|
114
99
|
test("[experimental] combines values, prefixes, and patterns", () => {
|
|
115
100
|
writeFileSync(
|
|
116
|
-
join(testDir, "
|
|
101
|
+
join(testDir, "data", "secret-allowlist.json"),
|
|
117
102
|
JSON.stringify({
|
|
118
103
|
values: ["exact-match-value"],
|
|
119
104
|
prefixes: ["test-prefix-"],
|
|
@@ -131,7 +116,7 @@ describe("secret-allowlist", () => {
|
|
|
131
116
|
// -----------------------------------------------------------------------
|
|
132
117
|
test("handles malformed JSON gracefully", () => {
|
|
133
118
|
writeFileSync(
|
|
134
|
-
join(testDir, "
|
|
119
|
+
join(testDir, "data", "secret-allowlist.json"),
|
|
135
120
|
"not json{{{",
|
|
136
121
|
);
|
|
137
122
|
loadAllowlist();
|
|
@@ -141,7 +126,7 @@ describe("secret-allowlist", () => {
|
|
|
141
126
|
|
|
142
127
|
test("handles non-array fields gracefully", () => {
|
|
143
128
|
writeFileSync(
|
|
144
|
-
join(testDir, "
|
|
129
|
+
join(testDir, "data", "secret-allowlist.json"),
|
|
145
130
|
JSON.stringify({ values: "not-an-array", prefixes: 42 }),
|
|
146
131
|
);
|
|
147
132
|
loadAllowlist();
|
|
@@ -154,7 +139,7 @@ describe("secret-allowlist", () => {
|
|
|
154
139
|
test("[experimental] allowlisted values are suppressed by scanText", () => {
|
|
155
140
|
const awsKey = "AKIAIOSFODNN7REALKEY";
|
|
156
141
|
writeFileSync(
|
|
157
|
-
join(testDir, "
|
|
142
|
+
join(testDir, "data", "secret-allowlist.json"),
|
|
158
143
|
JSON.stringify({ values: [awsKey] }),
|
|
159
144
|
);
|
|
160
145
|
resetAllowlist();
|
|
@@ -166,7 +151,7 @@ describe("secret-allowlist", () => {
|
|
|
166
151
|
|
|
167
152
|
test("non-allowlisted values are still detected by scanText", () => {
|
|
168
153
|
writeFileSync(
|
|
169
|
-
join(testDir, "
|
|
154
|
+
join(testDir, "data", "secret-allowlist.json"),
|
|
170
155
|
JSON.stringify({ values: ["AKIAIOSFODNN7OTHERKE"] }),
|
|
171
156
|
);
|
|
172
157
|
resetAllowlist();
|
|
@@ -178,7 +163,7 @@ describe("secret-allowlist", () => {
|
|
|
178
163
|
|
|
179
164
|
test("[experimental] prefix allowlist suppresses pattern matches", () => {
|
|
180
165
|
writeFileSync(
|
|
181
|
-
join(testDir, "
|
|
166
|
+
join(testDir, "data", "secret-allowlist.json"),
|
|
182
167
|
JSON.stringify({ prefixes: ["ghp_AAAA"] }),
|
|
183
168
|
);
|
|
184
169
|
resetAllowlist();
|
|
@@ -198,7 +183,7 @@ describe("secret-allowlist", () => {
|
|
|
198
183
|
|
|
199
184
|
// Create the file — but fileChecked is cached, so it won't be seen
|
|
200
185
|
writeFileSync(
|
|
201
|
-
join(testDir, "
|
|
186
|
+
join(testDir, "data", "secret-allowlist.json"),
|
|
202
187
|
JSON.stringify({ values: ["test-key"] }),
|
|
203
188
|
);
|
|
204
189
|
expect(isAllowlisted("test-key")).toBe(false);
|
|
@@ -211,7 +196,7 @@ describe("secret-allowlist", () => {
|
|
|
211
196
|
test("[experimental] retries loading when file was malformed on first call", () => {
|
|
212
197
|
// First call with malformed JSON
|
|
213
198
|
writeFileSync(
|
|
214
|
-
join(testDir, "
|
|
199
|
+
join(testDir, "data", "secret-allowlist.json"),
|
|
215
200
|
"not json{{{",
|
|
216
201
|
);
|
|
217
202
|
loadAllowlist();
|
|
@@ -219,7 +204,7 @@ describe("secret-allowlist", () => {
|
|
|
219
204
|
|
|
220
205
|
// Fix the file
|
|
221
206
|
writeFileSync(
|
|
222
|
-
join(testDir, "
|
|
207
|
+
join(testDir, "data", "secret-allowlist.json"),
|
|
223
208
|
JSON.stringify({ values: ["test-key"] }),
|
|
224
209
|
);
|
|
225
210
|
|
|
@@ -232,7 +217,7 @@ describe("secret-allowlist", () => {
|
|
|
232
217
|
// -----------------------------------------------------------------------
|
|
233
218
|
test("[experimental] resetAllowlist clears cached state", () => {
|
|
234
219
|
writeFileSync(
|
|
235
|
-
join(testDir, "
|
|
220
|
+
join(testDir, "data", "secret-allowlist.json"),
|
|
236
221
|
JSON.stringify({ values: ["test-value"] }),
|
|
237
222
|
);
|
|
238
223
|
loadAllowlist();
|
|
@@ -240,7 +225,7 @@ describe("secret-allowlist", () => {
|
|
|
240
225
|
|
|
241
226
|
// Reset and remove file — should no longer be allowlisted
|
|
242
227
|
resetAllowlist();
|
|
243
|
-
rmSync(join(testDir, "
|
|
228
|
+
rmSync(join(testDir, "data", "secret-allowlist.json"));
|
|
244
229
|
expect(isAllowlisted("test-value")).toBe(false);
|
|
245
230
|
});
|
|
246
231
|
});
|
|
@@ -45,11 +45,6 @@ mock.module("../tools/terminal/safe-env.js", () => ({
|
|
|
45
45
|
buildSanitizedEnv: () => ({ PATH: "/usr/bin" }),
|
|
46
46
|
}));
|
|
47
47
|
|
|
48
|
-
// Mock platform
|
|
49
|
-
mock.module("../util/platform.js", () => ({
|
|
50
|
-
getDataDir: () => "/tmp/test-data",
|
|
51
|
-
}));
|
|
52
|
-
|
|
53
48
|
// Mock proxy session manager
|
|
54
49
|
const mockGetOrStartSession = mock((_convId: string, _credIds: string[]) =>
|
|
55
50
|
Promise.resolve({
|
|
@@ -2,57 +2,19 @@
|
|
|
2
2
|
* Tests that skill_load rejects loading a skill whose feature flag is OFF
|
|
3
3
|
* with a deterministic error message.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import { tmpdir } from "node:os";
|
|
5
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
7
6
|
import { join } from "node:path";
|
|
8
7
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
9
8
|
|
|
10
9
|
import { _setOverridesForTesting } from "../config/assistant-feature-flags.js";
|
|
11
10
|
|
|
12
|
-
const TEST_DIR =
|
|
13
|
-
tmpdir(),
|
|
14
|
-
`vellum-skill-load-flag-test-${crypto.randomUUID()}`,
|
|
15
|
-
);
|
|
11
|
+
const TEST_DIR = process.env.VELLUM_WORKSPACE_DIR!;
|
|
16
12
|
|
|
17
13
|
let currentConfig: Record<string, unknown> = {};
|
|
18
14
|
|
|
19
15
|
const DECLARED_SKILL_ID = "contacts";
|
|
20
16
|
const DECLARED_FLAG_KEY = "contacts";
|
|
21
17
|
|
|
22
|
-
const platformOverrides: Record<string, (...args: unknown[]) => unknown> = {
|
|
23
|
-
getRootDir: () => TEST_DIR,
|
|
24
|
-
getDataDir: () => TEST_DIR,
|
|
25
|
-
ensureDataDir: () => {},
|
|
26
|
-
getPidPath: () => join(TEST_DIR, "vellum.pid"),
|
|
27
|
-
getDbPath: () => join(TEST_DIR, "data", "assistant.db"),
|
|
28
|
-
getLogPath: () => join(TEST_DIR, "logs", "vellum.log"),
|
|
29
|
-
getWorkspaceDir: () => join(TEST_DIR, "workspace"),
|
|
30
|
-
getWorkspaceSkillsDir: () => join(TEST_DIR, "skills"),
|
|
31
|
-
getWorkspaceConfigPath: () => join(TEST_DIR, "workspace", "config.json"),
|
|
32
|
-
getWorkspaceHooksDir: () => join(TEST_DIR, "workspace", "hooks"),
|
|
33
|
-
getWorkspacePromptPath: (f: unknown) =>
|
|
34
|
-
join(TEST_DIR, "workspace", String(f)),
|
|
35
|
-
getInterfacesDir: () => join(TEST_DIR, "interfaces"),
|
|
36
|
-
getHooksDir: () => join(TEST_DIR, "hooks"),
|
|
37
|
-
|
|
38
|
-
getSandboxRootDir: () => join(TEST_DIR, "sandbox"),
|
|
39
|
-
getSandboxWorkingDir: () => join(TEST_DIR, "sandbox", "work"),
|
|
40
|
-
getHistoryPath: () => join(TEST_DIR, "history"),
|
|
41
|
-
getSessionTokenPath: () => join(TEST_DIR, "session-token"),
|
|
42
|
-
readSessionToken: () => null,
|
|
43
|
-
getClipboardCommand: () => null,
|
|
44
|
-
isMacOS: () => process.platform === "darwin",
|
|
45
|
-
isLinux: () => process.platform === "linux",
|
|
46
|
-
isWindows: () => process.platform === "win32",
|
|
47
|
-
getPlatformName: () => process.platform,
|
|
48
|
-
};
|
|
49
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
50
|
-
const realPlatform = require("../util/platform.js");
|
|
51
|
-
mock.module("../util/platform.js", () => ({
|
|
52
|
-
...realPlatform,
|
|
53
|
-
...platformOverrides,
|
|
54
|
-
}));
|
|
55
|
-
|
|
56
18
|
const noopLogger = new Proxy({} as Record<string, unknown>, {
|
|
57
19
|
get: (_target, prop) => (prop === "child" ? () => noopLogger : () => {}),
|
|
58
20
|
});
|
|
@@ -119,9 +81,6 @@ describe("skill_load feature flag enforcement", () => {
|
|
|
119
81
|
|
|
120
82
|
afterEach(() => {
|
|
121
83
|
_setOverridesForTesting({});
|
|
122
|
-
if (existsSync(TEST_DIR)) {
|
|
123
|
-
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
124
|
-
}
|
|
125
84
|
});
|
|
126
85
|
|
|
127
86
|
test("returns deterministic error for flag OFF skill", async () => {
|