@vellumai/assistant 0.4.31 → 0.4.32
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 +1 -1
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -7
- package/src/__tests__/anthropic-provider.test.ts +86 -1
- package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
- package/src/__tests__/checker.test.ts +37 -98
- package/src/__tests__/commit-message-enrichment-service.test.ts +15 -0
- package/src/__tests__/config-schema.test.ts +6 -5
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-server-session-init.test.ts +1 -19
- package/src/__tests__/followup-tools.test.ts +0 -30
- package/src/__tests__/gemini-provider.test.ts +79 -1
- package/src/__tests__/ipc-snapshot.test.ts +0 -4
- package/src/__tests__/managed-proxy-context.test.ts +163 -0
- package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
- package/src/__tests__/memory-regressions.test.ts +6 -6
- package/src/__tests__/openai-provider.test.ts +82 -0
- package/src/__tests__/provider-fail-open-selection.test.ts +134 -1
- package/src/__tests__/provider-managed-proxy-integration.test.ts +269 -0
- package/src/__tests__/recurrence-types.test.ts +0 -15
- package/src/__tests__/schedule-tools.test.ts +28 -44
- package/src/__tests__/skill-feature-flags.test.ts +2 -2
- package/src/__tests__/task-management-tools.test.ts +111 -0
- package/src/__tests__/twilio-config.test.ts +0 -3
- package/src/amazon/session.ts +30 -91
- package/src/calls/call-controller.ts +423 -571
- package/src/calls/finalize-call.ts +20 -0
- package/src/calls/relay-access-wait.ts +340 -0
- package/src/calls/relay-server.ts +267 -902
- package/src/calls/relay-setup-router.ts +307 -0
- package/src/calls/relay-verification.ts +280 -0
- package/src/calls/twilio-config.ts +1 -8
- package/src/calls/voice-control-protocol.ts +184 -0
- package/src/calls/voice-session-bridge.ts +1 -8
- package/src/config/agent-schema.ts +1 -1
- package/src/config/bundled-skills/followups/TOOLS.json +0 -4
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/bundled-skills/schedule/TOOLS.json +2 -10
- package/src/config/core-schema.ts +1 -1
- package/src/config/env.ts +0 -10
- package/src/config/feature-flag-registry.json +1 -1
- package/src/config/loader.ts +19 -0
- package/src/config/schema.ts +2 -2
- package/src/daemon/handlers/session-history.ts +398 -0
- package/src/daemon/handlers/session-user-message.ts +982 -0
- package/src/daemon/handlers/sessions.ts +9 -1338
- package/src/daemon/ipc-contract/sessions.ts +0 -6
- package/src/daemon/ipc-contract-inventory.json +0 -1
- package/src/daemon/lifecycle.ts +0 -29
- package/src/home-base/app-link-store.ts +0 -7
- package/src/memory/conversation-attention-store.ts +1 -1
- package/src/memory/conversation-store.ts +0 -51
- package/src/memory/db-init.ts +5 -1
- package/src/memory/job-handlers/conflict.ts +24 -0
- package/src/memory/migrations/105-contacts-and-triage.ts +4 -7
- package/src/memory/migrations/134-contacts-notes-column.ts +50 -33
- package/src/memory/migrations/registry.ts +6 -0
- package/src/memory/recall-cache.ts +0 -5
- package/src/memory/schema/calls.ts +274 -0
- package/src/memory/schema/contacts.ts +125 -0
- package/src/memory/schema/conversations.ts +129 -0
- package/src/memory/schema/guardian.ts +172 -0
- package/src/memory/schema/index.ts +8 -0
- package/src/memory/schema/infrastructure.ts +205 -0
- package/src/memory/schema/memory-core.ts +196 -0
- package/src/memory/schema/notifications.ts +191 -0
- package/src/memory/schema/tasks.ts +78 -0
- package/src/memory/schema.ts +1 -1385
- package/src/memory/slack-thread-store.ts +0 -69
- package/src/notifications/decisions-store.ts +2 -105
- package/src/notifications/deliveries-store.ts +0 -11
- package/src/notifications/preferences-store.ts +1 -58
- package/src/permissions/checker.ts +6 -17
- package/src/providers/anthropic/client.ts +6 -2
- package/src/providers/gemini/client.ts +13 -2
- package/src/providers/managed-proxy/constants.ts +55 -0
- package/src/providers/managed-proxy/context.ts +77 -0
- package/src/providers/registry.ts +112 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +51 -23
- package/src/runtime/http-server.ts +83 -722
- package/src/runtime/http-types.ts +0 -16
- package/src/runtime/middleware/auth.ts +0 -12
- package/src/runtime/routes/app-routes.ts +33 -0
- package/src/runtime/routes/approval-routes.ts +32 -0
- package/src/runtime/routes/attachment-routes.ts +32 -0
- package/src/runtime/routes/brain-graph-routes.ts +27 -0
- package/src/runtime/routes/call-routes.ts +41 -0
- package/src/runtime/routes/channel-readiness-routes.ts +20 -0
- package/src/runtime/routes/channel-routes.ts +70 -0
- package/src/runtime/routes/contact-routes.ts +63 -0
- package/src/runtime/routes/conversation-attention-routes.ts +15 -0
- package/src/runtime/routes/conversation-routes.ts +190 -193
- package/src/runtime/routes/debug-routes.ts +15 -0
- package/src/runtime/routes/events-routes.ts +16 -0
- package/src/runtime/routes/global-search-routes.ts +15 -0
- package/src/runtime/routes/guardian-action-routes.ts +22 -0
- package/src/runtime/routes/guardian-bootstrap-routes.ts +20 -0
- package/src/runtime/routes/guardian-refresh-routes.ts +20 -0
- package/src/runtime/routes/identity-routes.ts +20 -0
- package/src/runtime/routes/inbound-message-handler.ts +8 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +6 -6
- package/src/runtime/routes/integration-routes.ts +83 -0
- package/src/runtime/routes/invite-routes.ts +31 -0
- package/src/runtime/routes/migration-routes.ts +30 -0
- package/src/runtime/routes/pairing-routes.ts +18 -0
- package/src/runtime/routes/secret-routes.ts +20 -0
- package/src/runtime/routes/surface-action-routes.ts +26 -0
- package/src/runtime/routes/trust-rules-routes.ts +31 -0
- package/src/runtime/routes/twilio-routes.ts +79 -0
- package/src/schedule/recurrence-types.ts +1 -11
- package/src/tools/followups/followup_create.ts +9 -3
- package/src/tools/mcp/mcp-tool-factory.ts +0 -17
- package/src/tools/memory/definitions.ts +0 -6
- package/src/tools/network/script-proxy/session-manager.ts +38 -3
- package/src/tools/schedule/create.ts +1 -3
- package/src/tools/schedule/update.ts +9 -6
- package/src/twitter/session.ts +29 -77
- package/src/util/cookie-session.ts +114 -0
- package/src/__tests__/conversation-routes.test.ts +0 -99
- package/src/__tests__/task-tools.test.ts +0 -685
- package/src/contacts/startup-migration.ts +0 -21
package/ARCHITECTURE.md
CHANGED
|
@@ -883,7 +883,7 @@ graph LR
|
|
|
883
883
|
C5["user_message<br/>text, attachments"]
|
|
884
884
|
C6["confirmation_response<br/>decision"]
|
|
885
885
|
C7["cancel / undo"]
|
|
886
|
-
C8["model_get / model_set
|
|
886
|
+
C8["model_get / model_set"]
|
|
887
887
|
C9["ping"]
|
|
888
888
|
C10["ipc_blob_probe<br/>probeId, nonceSha256"]
|
|
889
889
|
C11["work_items_list / work_item_get<br/>work_item_create / work_item_update<br/>work_item_complete / work_item_run_task<br/>(planned)"]
|
package/package.json
CHANGED
|
@@ -145,13 +145,6 @@ exports[`IPC message snapshots ClientMessage types usage_request serializes to e
|
|
|
145
145
|
}
|
|
146
146
|
`;
|
|
147
147
|
|
|
148
|
-
exports[`IPC message snapshots ClientMessage types sandbox_set serializes to expected JSON 1`] = `
|
|
149
|
-
{
|
|
150
|
-
"enabled": true,
|
|
151
|
-
"type": "sandbox_set",
|
|
152
|
-
}
|
|
153
|
-
`;
|
|
154
|
-
|
|
155
148
|
exports[`IPC message snapshots ClientMessage types cu_session_create serializes to expected JSON 1`] = `
|
|
156
149
|
{
|
|
157
150
|
"screenHeight": 1080,
|
|
@@ -8,6 +8,7 @@ import type { Message, ToolDefinition } from "../providers/types.js";
|
|
|
8
8
|
|
|
9
9
|
let lastStreamParams: Record<string, unknown> | null = null;
|
|
10
10
|
let _lastStreamOptions: Record<string, unknown> | null = null;
|
|
11
|
+
let lastConstructorArgs: Record<string, unknown> | null = null;
|
|
11
12
|
|
|
12
13
|
const fakeResponse = {
|
|
13
14
|
content: [{ type: "text", text: "Hello" }],
|
|
@@ -33,7 +34,9 @@ class FakeAPIError extends Error {
|
|
|
33
34
|
mock.module("@anthropic-ai/sdk", () => ({
|
|
34
35
|
default: class MockAnthropic {
|
|
35
36
|
static APIError = FakeAPIError;
|
|
36
|
-
constructor() {
|
|
37
|
+
constructor(args: Record<string, unknown>) {
|
|
38
|
+
lastConstructorArgs = { ...args };
|
|
39
|
+
}
|
|
37
40
|
messages = {
|
|
38
41
|
stream: (
|
|
39
42
|
params: Record<string, unknown>,
|
|
@@ -127,6 +130,7 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
|
127
130
|
beforeEach(() => {
|
|
128
131
|
lastStreamParams = null;
|
|
129
132
|
_lastStreamOptions = null;
|
|
133
|
+
lastConstructorArgs = null;
|
|
130
134
|
provider = new AnthropicProvider("sk-ant-test", "claude-sonnet-4-6");
|
|
131
135
|
});
|
|
132
136
|
|
|
@@ -935,3 +939,84 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
|
935
939
|
expect(userMsgs[2].content[1].cache_control).toEqual({ type: "ephemeral" });
|
|
936
940
|
});
|
|
937
941
|
});
|
|
942
|
+
|
|
943
|
+
// ---------------------------------------------------------------------------
|
|
944
|
+
// Tests — Managed Proxy Fallback
|
|
945
|
+
// ---------------------------------------------------------------------------
|
|
946
|
+
|
|
947
|
+
describe("AnthropicProvider — Managed Proxy Fallback", () => {
|
|
948
|
+
beforeEach(() => {
|
|
949
|
+
lastStreamParams = null;
|
|
950
|
+
_lastStreamOptions = null;
|
|
951
|
+
lastConstructorArgs = null;
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
test("constructor passes baseURL to Anthropic SDK when provided", () => {
|
|
955
|
+
new AnthropicProvider("managed-key", "claude-sonnet-4-6", {
|
|
956
|
+
baseURL: "https://platform.example.com/v1/runtime-proxy/anthropic",
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
expect(lastConstructorArgs).not.toBeNull();
|
|
960
|
+
expect(lastConstructorArgs!.apiKey).toBe("managed-key");
|
|
961
|
+
expect(lastConstructorArgs!.baseURL).toBe(
|
|
962
|
+
"https://platform.example.com/v1/runtime-proxy/anthropic",
|
|
963
|
+
);
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
test("constructor does not set baseURL when option is omitted", () => {
|
|
967
|
+
new AnthropicProvider("sk-ant-user-key", "claude-sonnet-4-6");
|
|
968
|
+
|
|
969
|
+
expect(lastConstructorArgs).not.toBeNull();
|
|
970
|
+
expect(lastConstructorArgs!.apiKey).toBe("sk-ant-user-key");
|
|
971
|
+
expect(lastConstructorArgs!.baseURL).toBeUndefined();
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
test("managed mode provider preserves tool-pairing behavior", async () => {
|
|
975
|
+
const provider = new AnthropicProvider("managed-key", "claude-sonnet-4-6", {
|
|
976
|
+
baseURL: "https://platform.example.com/v1/runtime-proxy/anthropic",
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
const messages: Message[] = [
|
|
980
|
+
userMsg("Read file"),
|
|
981
|
+
toolUseMsg("tu_1", "file_read"),
|
|
982
|
+
toolResultMsg("tu_1", "file contents"),
|
|
983
|
+
];
|
|
984
|
+
await provider.sendMessage(messages);
|
|
985
|
+
|
|
986
|
+
const sent = lastStreamParams!.messages as Array<{
|
|
987
|
+
role: string;
|
|
988
|
+
content: Array<{ type: string; tool_use_id?: string }>;
|
|
989
|
+
}>;
|
|
990
|
+
|
|
991
|
+
expect(sent).toHaveLength(3);
|
|
992
|
+
const toolResults = sent[2].content.filter((b) => b.type === "tool_result");
|
|
993
|
+
expect(toolResults).toHaveLength(1);
|
|
994
|
+
expect(toolResults[0].tool_use_id).toBe("tu_1");
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
test("managed mode provider preserves cache-control behavior", async () => {
|
|
998
|
+
const provider = new AnthropicProvider("managed-key", "claude-sonnet-4-6", {
|
|
999
|
+
baseURL: "https://platform.example.com/v1/runtime-proxy/anthropic",
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
await provider.sendMessage(
|
|
1003
|
+
[userMsg("Hi")],
|
|
1004
|
+
sampleTools,
|
|
1005
|
+
"You are helpful.",
|
|
1006
|
+
);
|
|
1007
|
+
|
|
1008
|
+
// System prompt cache control
|
|
1009
|
+
const system = lastStreamParams!.system as Array<{
|
|
1010
|
+
cache_control?: { type: string };
|
|
1011
|
+
}>;
|
|
1012
|
+
expect(system[0].cache_control).toEqual({ type: "ephemeral" });
|
|
1013
|
+
|
|
1014
|
+
// Last tool cache control
|
|
1015
|
+
const tools = lastStreamParams!.tools as Array<{
|
|
1016
|
+
cache_control?: { type: string };
|
|
1017
|
+
}>;
|
|
1018
|
+
expect(tools[tools.length - 1].cache_control).toEqual({
|
|
1019
|
+
type: "ephemeral",
|
|
1020
|
+
});
|
|
1021
|
+
});
|
|
1022
|
+
});
|
|
@@ -241,8 +241,8 @@ describe("buildSystemPrompt assistant feature flag filtering", () => {
|
|
|
241
241
|
|
|
242
242
|
const result = buildSystemPrompt();
|
|
243
243
|
|
|
244
|
-
// browser is declared in the registry with defaultEnabled:
|
|
245
|
-
expect(result).
|
|
244
|
+
// browser is declared in the registry with defaultEnabled: true
|
|
245
|
+
expect(result).toContain('id="browser"');
|
|
246
246
|
});
|
|
247
247
|
});
|
|
248
248
|
|
|
@@ -55,16 +55,16 @@ mock.module("../util/logger.js", () => ({
|
|
|
55
55
|
}));
|
|
56
56
|
|
|
57
57
|
// Mutable config object so tests can switch permissions.mode between
|
|
58
|
-
// '
|
|
58
|
+
// 'strict' and 'workspace' without re-registering the mock.
|
|
59
59
|
interface TestConfig {
|
|
60
|
-
permissions: { mode: "
|
|
60
|
+
permissions: { mode: "strict" | "workspace" };
|
|
61
61
|
skills: { load: { extraDirs: string[] } };
|
|
62
62
|
sandbox: { enabled: boolean };
|
|
63
63
|
[key: string]: unknown;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
const testConfig: TestConfig = {
|
|
67
|
-
permissions: { mode: "
|
|
67
|
+
permissions: { mode: "workspace" },
|
|
68
68
|
skills: { load: { extraDirs: [] } },
|
|
69
69
|
sandbox: { enabled: true },
|
|
70
70
|
};
|
|
@@ -81,7 +81,6 @@ mock.module("../config/loader.js", () => ({
|
|
|
81
81
|
}));
|
|
82
82
|
|
|
83
83
|
import {
|
|
84
|
-
_resetLegacyDeprecationWarning,
|
|
85
84
|
check,
|
|
86
85
|
classifyRisk,
|
|
87
86
|
generateAllowlistOptions,
|
|
@@ -169,11 +168,9 @@ describe("Permission Checker", () => {
|
|
|
169
168
|
beforeEach(() => {
|
|
170
169
|
// Reset trust-store state between tests
|
|
171
170
|
clearCache();
|
|
172
|
-
// Reset permissions mode to
|
|
173
|
-
testConfig.permissions = { mode: "
|
|
171
|
+
// Reset permissions mode to workspace (default) so existing tests are not affected
|
|
172
|
+
testConfig.permissions = { mode: "workspace" };
|
|
174
173
|
testConfig.skills = { load: { extraDirs: [] } };
|
|
175
|
-
// Reset the one-time legacy deprecation warning flag and captured log calls
|
|
176
|
-
_resetLegacyDeprecationWarning();
|
|
177
174
|
loggerWarnCalls.length = 0;
|
|
178
175
|
try {
|
|
179
176
|
rmSync(join(checkerTestDir, "protected", "trust.json"));
|
|
@@ -684,12 +681,22 @@ describe("Permission Checker", () => {
|
|
|
684
681
|
expect(result.decision).toBe("allow");
|
|
685
682
|
});
|
|
686
683
|
|
|
687
|
-
test("file_write with no rule →
|
|
684
|
+
test("file_write within workspace with no rule → auto-allowed in workspace mode", async () => {
|
|
688
685
|
const result = await check(
|
|
689
686
|
"file_write",
|
|
690
687
|
{ path: "/tmp/file.txt" },
|
|
691
688
|
"/tmp",
|
|
692
689
|
);
|
|
690
|
+
expect(result.decision).toBe("allow");
|
|
691
|
+
expect(result.reason).toContain("workspace-scoped");
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
test("file_write outside workspace with no rule → prompt", async () => {
|
|
695
|
+
const result = await check(
|
|
696
|
+
"file_write",
|
|
697
|
+
{ path: "/etc/some-file.txt" },
|
|
698
|
+
"/tmp",
|
|
699
|
+
);
|
|
693
700
|
expect(result.decision).toBe("prompt");
|
|
694
701
|
});
|
|
695
702
|
|
|
@@ -1354,12 +1361,10 @@ describe("Permission Checker", () => {
|
|
|
1354
1361
|
});
|
|
1355
1362
|
|
|
1356
1363
|
test("core tool (no origin) still follows risk-based fallback", async () => {
|
|
1357
|
-
// file_read is a core tool with Low risk
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
"/tmp",
|
|
1362
|
-
);
|
|
1364
|
+
// file_read is a core tool with Low risk — in workspace mode,
|
|
1365
|
+
// workspace-scoped invocations are auto-allowed before risk fallback.
|
|
1366
|
+
// Use a path outside the workspace to test the risk-based fallback.
|
|
1367
|
+
const result = await check("file_read", { path: "/etc/hosts" }, "/tmp");
|
|
1363
1368
|
expect(result.decision).toBe("allow");
|
|
1364
1369
|
expect(result.reason).toContain("Low risk");
|
|
1365
1370
|
});
|
|
@@ -1488,7 +1493,8 @@ describe("Permission Checker", () => {
|
|
|
1488
1493
|
|
|
1489
1494
|
test("file_write of non-workspace file is not auto-allowed", async () => {
|
|
1490
1495
|
const otherPath = join(checkerTestDir, "workspace", "OTHER.md");
|
|
1491
|
-
|
|
1496
|
+
// Use a workingDir that doesn't contain the path so it's not workspace-scoped
|
|
1497
|
+
const result = await check("file_write", { path: otherPath }, "/home");
|
|
1492
1498
|
// Medium risk with no matching allow rule → prompt
|
|
1493
1499
|
expect(result.decision).toBe("prompt");
|
|
1494
1500
|
});
|
|
@@ -2565,7 +2571,7 @@ describe("Permission Checker", () => {
|
|
|
2565
2571
|
});
|
|
2566
2572
|
|
|
2567
2573
|
test("legacy mode: file_write to skill source still prompts as High risk", async () => {
|
|
2568
|
-
testConfig.permissions.mode = "
|
|
2574
|
+
testConfig.permissions.mode = "workspace";
|
|
2569
2575
|
ensureSkillsDir();
|
|
2570
2576
|
const skillPath = join(
|
|
2571
2577
|
checkerTestDir,
|
|
@@ -3327,7 +3333,7 @@ describe("Permission Checker", () => {
|
|
|
3327
3333
|
});
|
|
3328
3334
|
|
|
3329
3335
|
test("skill_load auto-allows in legacy mode (backward compat)", async () => {
|
|
3330
|
-
testConfig.permissions.mode = "
|
|
3336
|
+
testConfig.permissions.mode = "workspace";
|
|
3331
3337
|
const result = await check("skill_load", { skill: "any-skill" }, "/tmp");
|
|
3332
3338
|
expect(result.decision).toBe("allow");
|
|
3333
3339
|
// The default allow rule matches before the Low risk fallback
|
|
@@ -3850,7 +3856,7 @@ describe("Permission Checker", () => {
|
|
|
3850
3856
|
|
|
3851
3857
|
describe("Invariant 6: user can set broad rules if they choose", () => {
|
|
3852
3858
|
test("wildcard allow rule matches any command in legacy mode", async () => {
|
|
3853
|
-
testConfig.permissions.mode = "
|
|
3859
|
+
testConfig.permissions.mode = "workspace";
|
|
3854
3860
|
addRule("bash", "*", "everywhere");
|
|
3855
3861
|
const result = await check(
|
|
3856
3862
|
"bash",
|
|
@@ -4203,7 +4209,7 @@ describe("Permission Checker", () => {
|
|
|
4203
4209
|
}
|
|
4204
4210
|
|
|
4205
4211
|
test("browser tools are auto-allowed in legacy mode", async () => {
|
|
4206
|
-
testConfig.permissions = { mode: "
|
|
4212
|
+
testConfig.permissions = { mode: "workspace" };
|
|
4207
4213
|
for (const toolName of browserToolNames) {
|
|
4208
4214
|
const result = await check(toolName, {}, "/tmp");
|
|
4209
4215
|
expect(result.decision).toBe("allow");
|
|
@@ -4218,7 +4224,7 @@ describe("Permission Checker", () => {
|
|
|
4218
4224
|
expect(result.decision).toBe("allow");
|
|
4219
4225
|
}
|
|
4220
4226
|
} finally {
|
|
4221
|
-
testConfig.permissions = { mode: "
|
|
4227
|
+
testConfig.permissions = { mode: "workspace" };
|
|
4222
4228
|
}
|
|
4223
4229
|
});
|
|
4224
4230
|
});
|
|
@@ -4295,7 +4301,7 @@ describe("Permission Checker", () => {
|
|
|
4295
4301
|
describe("bash network_mode=proxied — no special-casing", () => {
|
|
4296
4302
|
beforeEach(() => {
|
|
4297
4303
|
clearCache();
|
|
4298
|
-
testConfig.permissions = { mode: "
|
|
4304
|
+
testConfig.permissions = { mode: "workspace" };
|
|
4299
4305
|
testConfig.skills = { load: { extraDirs: [] } };
|
|
4300
4306
|
});
|
|
4301
4307
|
|
|
@@ -4416,7 +4422,7 @@ describe("computer-use tool permission defaults", () => {
|
|
|
4416
4422
|
describe("scope matching behavior", () => {
|
|
4417
4423
|
beforeEach(() => {
|
|
4418
4424
|
clearCache();
|
|
4419
|
-
testConfig.permissions = { mode: "
|
|
4425
|
+
testConfig.permissions = { mode: "workspace" };
|
|
4420
4426
|
try {
|
|
4421
4427
|
rmSync(join(checkerTestDir, "protected", "trust.json"));
|
|
4422
4428
|
} catch {
|
|
@@ -4456,6 +4462,8 @@ describe("scope matching behavior", () => {
|
|
|
4456
4462
|
});
|
|
4457
4463
|
|
|
4458
4464
|
test("project-scoped rule does NOT match invocations from sibling directory", async () => {
|
|
4465
|
+
// Use strict mode to test rule-matching isolation without workspace auto-allow
|
|
4466
|
+
testConfig.permissions.mode = "strict";
|
|
4459
4467
|
const projectDir = "/home/user/my-project";
|
|
4460
4468
|
// Use a broad pattern that matches any file, scoped to the project
|
|
4461
4469
|
addRule("file_write", "file_write:*", projectDir);
|
|
@@ -4470,6 +4478,8 @@ describe("scope matching behavior", () => {
|
|
|
4470
4478
|
});
|
|
4471
4479
|
|
|
4472
4480
|
test("project-scoped rule does NOT match invocations from parent directory", async () => {
|
|
4481
|
+
// Use strict mode to test rule-matching isolation without workspace auto-allow
|
|
4482
|
+
testConfig.permissions.mode = "strict";
|
|
4473
4483
|
const projectDir = "/home/user/my-project";
|
|
4474
4484
|
addRule("file_write", "file_write:*", projectDir);
|
|
4475
4485
|
|
|
@@ -4483,6 +4493,8 @@ describe("scope matching behavior", () => {
|
|
|
4483
4493
|
});
|
|
4484
4494
|
|
|
4485
4495
|
test("project-scoped rule does NOT match directory with shared prefix", async () => {
|
|
4496
|
+
// Use strict mode to test rule-matching isolation without workspace auto-allow
|
|
4497
|
+
testConfig.permissions.mode = "strict";
|
|
4486
4498
|
// A rule for /home/user/project should NOT match /home/user/project-evil
|
|
4487
4499
|
// (directory-boundary enforcement in matchesScope)
|
|
4488
4500
|
const projectDir = "/home/user/project";
|
|
@@ -4562,7 +4574,7 @@ describe("workspace mode — auto-allow workspace-scoped operations", () => {
|
|
|
4562
4574
|
});
|
|
4563
4575
|
|
|
4564
4576
|
afterEach(() => {
|
|
4565
|
-
testConfig.permissions = { mode: "
|
|
4577
|
+
testConfig.permissions = { mode: "workspace" };
|
|
4566
4578
|
});
|
|
4567
4579
|
|
|
4568
4580
|
// ── workspace-scoped file operations auto-allow ──────────────────
|
|
@@ -4771,79 +4783,6 @@ describe("workspace mode — auto-allow workspace-scoped operations", () => {
|
|
|
4771
4783
|
});
|
|
4772
4784
|
});
|
|
4773
4785
|
|
|
4774
|
-
// ── legacy mode deprecation warning ─────────────────────────────────────
|
|
4775
|
-
|
|
4776
|
-
describe("legacy mode — deprecation warning", () => {
|
|
4777
|
-
beforeEach(() => {
|
|
4778
|
-
clearCache();
|
|
4779
|
-
_resetLegacyDeprecationWarning();
|
|
4780
|
-
loggerWarnCalls.length = 0;
|
|
4781
|
-
testConfig.permissions = { mode: "legacy" };
|
|
4782
|
-
testConfig.skills = { load: { extraDirs: [] } };
|
|
4783
|
-
try {
|
|
4784
|
-
rmSync(join(checkerTestDir, "protected", "trust.json"));
|
|
4785
|
-
} catch {
|
|
4786
|
-
/* may not exist */
|
|
4787
|
-
}
|
|
4788
|
-
});
|
|
4789
|
-
|
|
4790
|
-
afterEach(() => {
|
|
4791
|
-
testConfig.permissions = { mode: "legacy" };
|
|
4792
|
-
});
|
|
4793
|
-
|
|
4794
|
-
test("emits deprecation warning on first check() call in legacy mode", async () => {
|
|
4795
|
-
await check("file_read", { file_path: "/tmp/test.txt" }, "/tmp");
|
|
4796
|
-
expect(loggerWarnCalls.some((m) => m.includes("deprecated"))).toBe(true);
|
|
4797
|
-
expect(loggerWarnCalls.some((m) => m.includes("legacy"))).toBe(true);
|
|
4798
|
-
});
|
|
4799
|
-
|
|
4800
|
-
test("deprecation warning fires only once per process", async () => {
|
|
4801
|
-
await check("file_read", { file_path: "/tmp/a.txt" }, "/tmp");
|
|
4802
|
-
const firstCount = loggerWarnCalls.filter((m) =>
|
|
4803
|
-
m.includes("deprecated"),
|
|
4804
|
-
).length;
|
|
4805
|
-
expect(firstCount).toBe(1);
|
|
4806
|
-
|
|
4807
|
-
await check("file_read", { file_path: "/tmp/b.txt" }, "/tmp");
|
|
4808
|
-
const secondCount = loggerWarnCalls.filter((m) =>
|
|
4809
|
-
m.includes("deprecated"),
|
|
4810
|
-
).length;
|
|
4811
|
-
expect(secondCount).toBe(1);
|
|
4812
|
-
});
|
|
4813
|
-
|
|
4814
|
-
test("no deprecation warning in workspace mode", async () => {
|
|
4815
|
-
testConfig.permissions = { mode: "workspace" };
|
|
4816
|
-
await check("file_read", { file_path: "/tmp/test.txt" }, "/tmp");
|
|
4817
|
-
expect(loggerWarnCalls.some((m) => m.includes("deprecated"))).toBe(false);
|
|
4818
|
-
});
|
|
4819
|
-
|
|
4820
|
-
test("no deprecation warning in strict mode", async () => {
|
|
4821
|
-
testConfig.permissions = { mode: "strict" };
|
|
4822
|
-
await check("file_read", { file_path: "/tmp/test.txt" }, "/tmp");
|
|
4823
|
-
expect(loggerWarnCalls.some((m) => m.includes("deprecated"))).toBe(false);
|
|
4824
|
-
});
|
|
4825
|
-
|
|
4826
|
-
test("legacy mode still produces correct decisions (low risk auto-allowed)", async () => {
|
|
4827
|
-
const result = await check(
|
|
4828
|
-
"file_read",
|
|
4829
|
-
{ file_path: "/tmp/test.txt" },
|
|
4830
|
-
"/tmp",
|
|
4831
|
-
);
|
|
4832
|
-
expect(result.decision).toBe("allow");
|
|
4833
|
-
expect(result.reason).toContain("Low risk");
|
|
4834
|
-
});
|
|
4835
|
-
|
|
4836
|
-
test("legacy mode still prompts for medium risk", async () => {
|
|
4837
|
-
const result = await check(
|
|
4838
|
-
"file_write",
|
|
4839
|
-
{ file_path: "/tmp/test.txt" },
|
|
4840
|
-
"/tmp",
|
|
4841
|
-
);
|
|
4842
|
-
expect(result.decision).toBe("prompt");
|
|
4843
|
-
expect(result.reason).toContain("risk");
|
|
4844
|
-
});
|
|
4845
|
-
});
|
|
4846
|
-
|
|
4847
4786
|
describe("shell command candidates wiring (PR 04)", () => {
|
|
4848
4787
|
test("existing raw shell rule still matches", async () => {
|
|
4849
4788
|
clearCache();
|
|
@@ -4896,7 +4835,7 @@ describe("integration regressions (PR 11)", () => {
|
|
|
4896
4835
|
/* may not exist */
|
|
4897
4836
|
}
|
|
4898
4837
|
clearCache();
|
|
4899
|
-
testConfig.permissions = { mode: "
|
|
4838
|
+
testConfig.permissions = { mode: "workspace" };
|
|
4900
4839
|
testConfig.sandbox = { enabled: true };
|
|
4901
4840
|
});
|
|
4902
4841
|
|
|
@@ -51,6 +51,14 @@ describe("CommitEnrichmentService", () => {
|
|
|
51
51
|
/* ignore */
|
|
52
52
|
}
|
|
53
53
|
_resetEnrichmentService();
|
|
54
|
+
|
|
55
|
+
// Remove stale index.lock left by async enrichment jobs that ran git
|
|
56
|
+
// commands concurrently. Without this, the next test's createCommit()
|
|
57
|
+
// can fail with "Unable to create index.lock: File exists".
|
|
58
|
+
const lockFile = join(testDir, ".git", "index.lock");
|
|
59
|
+
if (existsSync(lockFile)) {
|
|
60
|
+
rmSync(lockFile, { force: true });
|
|
61
|
+
}
|
|
54
62
|
});
|
|
55
63
|
|
|
56
64
|
afterAll(async () => {
|
|
@@ -78,6 +86,13 @@ describe("CommitEnrichmentService", () => {
|
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
async function createCommit(): Promise<string> {
|
|
89
|
+
// Remove stale index.lock left by async enrichment jobs that ran git
|
|
90
|
+
// commands concurrently in a previous test. Without this, git add -A
|
|
91
|
+
// can fail with "Unable to create index.lock: File exists".
|
|
92
|
+
const lockFile = join(testDir, ".git", "index.lock");
|
|
93
|
+
if (existsSync(lockFile)) {
|
|
94
|
+
rmSync(lockFile, { force: true });
|
|
95
|
+
}
|
|
81
96
|
writeFileSync(join(testDir, `file-${Date.now()}.txt`), "content");
|
|
82
97
|
await gitService.commitChanges("test commit");
|
|
83
98
|
return await gitService.getHeadHash();
|
|
@@ -527,11 +527,12 @@ describe("AssistantConfigSchema", () => {
|
|
|
527
527
|
expect(result.permissions.mode).toBe("strict");
|
|
528
528
|
});
|
|
529
529
|
|
|
530
|
-
test("
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
530
|
+
test("rejects permissions.mode legacy", () => {
|
|
531
|
+
expect(() =>
|
|
532
|
+
AssistantConfigSchema.parse({
|
|
533
|
+
permissions: { mode: "legacy" },
|
|
534
|
+
}),
|
|
535
|
+
).toThrow();
|
|
535
536
|
});
|
|
536
537
|
|
|
537
538
|
test("accepts explicit permissions.mode workspace", () => {
|
|
@@ -241,6 +241,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
|
|
|
241
241
|
"schedule/integration-status.ts", // integration status checks for scheduled reports
|
|
242
242
|
"daemon/handlers/oauth-connect.ts", // OAuth connect handler for integration setup
|
|
243
243
|
"daemon/handlers/config-slack-channel.ts", // Slack channel config credential management
|
|
244
|
+
"providers/managed-proxy/context.ts", // managed proxy API key lookup for provider initialization
|
|
244
245
|
]);
|
|
245
246
|
|
|
246
247
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -52,7 +52,6 @@ class MockSession {
|
|
|
52
52
|
| { skipPreMessageRollback?: boolean; isInteractive?: boolean }
|
|
53
53
|
| undefined;
|
|
54
54
|
public updateClientHistory: Array<{ hasNoClient: boolean }> = [];
|
|
55
|
-
public setSandboxOverrideCalls = 0;
|
|
56
55
|
private stale = false;
|
|
57
56
|
private processing = false;
|
|
58
57
|
public trustContext: Record<string, unknown> | null = null;
|
|
@@ -95,9 +94,7 @@ class MockSession {
|
|
|
95
94
|
return this._currentSender;
|
|
96
95
|
}
|
|
97
96
|
|
|
98
|
-
setSandboxOverride(): void {
|
|
99
|
-
this.setSandboxOverrideCalls += 1;
|
|
100
|
-
}
|
|
97
|
+
setSandboxOverride(): void {}
|
|
101
98
|
|
|
102
99
|
isProcessing(): boolean {
|
|
103
100
|
return this.processing;
|
|
@@ -433,21 +430,6 @@ describe("DaemonServer initial session hydration", () => {
|
|
|
433
430
|
expect(lastCreatedWorkingDir).toBe("/tmp/workspace");
|
|
434
431
|
});
|
|
435
432
|
|
|
436
|
-
test("ignores deprecated sandbox_set runtime override messages", async () => {
|
|
437
|
-
const server = new DaemonServer();
|
|
438
|
-
const internal = asDaemonServerTestAccess(server);
|
|
439
|
-
const { socket } = createFakeSocket();
|
|
440
|
-
|
|
441
|
-
await internal.sendInitialSession(socket);
|
|
442
|
-
const session = internal.sessions.get(conversation.id);
|
|
443
|
-
expect(session).toBeDefined();
|
|
444
|
-
expect(session!.setSandboxOverrideCalls).toBe(0);
|
|
445
|
-
|
|
446
|
-
internal.dispatchMessage({ type: "sandbox_set", enabled: false }, socket);
|
|
447
|
-
|
|
448
|
-
expect(session!.setSandboxOverrideCalls).toBe(0);
|
|
449
|
-
});
|
|
450
|
-
|
|
451
433
|
test("sendInitialSession includes threadType in session_info", async () => {
|
|
452
434
|
conversation.threadType = "private";
|
|
453
435
|
const server = new DaemonServer();
|
|
@@ -122,36 +122,6 @@ describe("followup_create tool", () => {
|
|
|
122
122
|
expect(result.content).toContain("Reminder schedule: sched-abc");
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
-
test("creates a follow-up with deprecated reminder_cron_id alias", async () => {
|
|
126
|
-
const result = await executeFollowupCreate(
|
|
127
|
-
{
|
|
128
|
-
channel: "email",
|
|
129
|
-
thread_id: "thread-789",
|
|
130
|
-
reminder_cron_id: "cron-abc",
|
|
131
|
-
},
|
|
132
|
-
ctx,
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
expect(result.isError).toBe(false);
|
|
136
|
-
expect(result.content).toContain("Reminder schedule: cron-abc");
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test("reminder_schedule_id takes precedence over reminder_cron_id", async () => {
|
|
140
|
-
const result = await executeFollowupCreate(
|
|
141
|
-
{
|
|
142
|
-
channel: "email",
|
|
143
|
-
thread_id: "thread-prio",
|
|
144
|
-
reminder_schedule_id: "sched-wins",
|
|
145
|
-
reminder_cron_id: "cron-loses",
|
|
146
|
-
},
|
|
147
|
-
ctx,
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
expect(result.isError).toBe(false);
|
|
151
|
-
expect(result.content).toContain("Reminder schedule: sched-wins");
|
|
152
|
-
expect(result.content).not.toContain("cron-loses");
|
|
153
|
-
});
|
|
154
|
-
|
|
155
125
|
test("rejects missing channel", async () => {
|
|
156
126
|
const result = await executeFollowupCreate(
|
|
157
127
|
{
|
|
@@ -30,6 +30,7 @@ interface FakeChunk {
|
|
|
30
30
|
|
|
31
31
|
let fakeChunks: FakeChunk[] = [];
|
|
32
32
|
let lastStreamParams: Record<string, unknown> | null = null;
|
|
33
|
+
let lastConstructorOpts: Record<string, unknown> | null = null;
|
|
33
34
|
let shouldThrow: Error | null = null;
|
|
34
35
|
|
|
35
36
|
class FakeApiError extends Error {
|
|
@@ -43,7 +44,9 @@ class FakeApiError extends Error {
|
|
|
43
44
|
|
|
44
45
|
mock.module("@google/genai", () => ({
|
|
45
46
|
GoogleGenAI: class MockGoogleGenAI {
|
|
46
|
-
constructor(
|
|
47
|
+
constructor(opts: Record<string, unknown>) {
|
|
48
|
+
lastConstructorOpts = opts;
|
|
49
|
+
}
|
|
47
50
|
models = {
|
|
48
51
|
generateContentStream: async (params: Record<string, unknown>) => {
|
|
49
52
|
lastStreamParams = params;
|
|
@@ -108,6 +111,7 @@ describe("GeminiProvider", () => {
|
|
|
108
111
|
provider = new GeminiProvider("test-api-key", "gemini-3-flash");
|
|
109
112
|
fakeChunks = [];
|
|
110
113
|
lastStreamParams = null;
|
|
114
|
+
lastConstructorOpts = null;
|
|
111
115
|
shouldThrow = null;
|
|
112
116
|
});
|
|
113
117
|
|
|
@@ -726,4 +730,78 @@ describe("GeminiProvider", () => {
|
|
|
726
730
|
|
|
727
731
|
expect(result.usage).toEqual({ inputTokens: 0, outputTokens: 0 });
|
|
728
732
|
});
|
|
733
|
+
|
|
734
|
+
// -----------------------------------------------------------------------
|
|
735
|
+
// Managed transport — constructor configuration
|
|
736
|
+
// -----------------------------------------------------------------------
|
|
737
|
+
test("does not set httpOptions when managedBaseUrl is not provided", () => {
|
|
738
|
+
new GeminiProvider("test-key", "gemini-3-flash");
|
|
739
|
+
expect(lastConstructorOpts).toEqual({ apiKey: "test-key" });
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
test("sets httpOptions.baseUrl when managedBaseUrl is provided", () => {
|
|
743
|
+
new GeminiProvider("managed-key", "gemini-3-flash", {
|
|
744
|
+
managedBaseUrl: "https://platform.example.com/v1/runtime-proxy/gemini",
|
|
745
|
+
});
|
|
746
|
+
expect(lastConstructorOpts).toEqual({
|
|
747
|
+
apiKey: "managed-key",
|
|
748
|
+
httpOptions: {
|
|
749
|
+
baseUrl: "https://platform.example.com/v1/runtime-proxy/gemini",
|
|
750
|
+
},
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
test("managed transport produces same ProviderResponse shape", async () => {
|
|
755
|
+
const managedProvider = new GeminiProvider(
|
|
756
|
+
"managed-key",
|
|
757
|
+
"gemini-3-flash",
|
|
758
|
+
{
|
|
759
|
+
managedBaseUrl: "https://platform.example.com/v1/runtime-proxy/gemini",
|
|
760
|
+
},
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
fakeChunks = [textChunk("Hello from managed"), finishChunk("STOP", 15, 8)];
|
|
764
|
+
|
|
765
|
+
const result = await managedProvider.sendMessage([
|
|
766
|
+
{ role: "user", content: [{ type: "text", text: "Hi" }] },
|
|
767
|
+
]);
|
|
768
|
+
|
|
769
|
+
expect(result.content).toHaveLength(1);
|
|
770
|
+
expect(result.content[0]).toEqual({
|
|
771
|
+
type: "text",
|
|
772
|
+
text: "Hello from managed",
|
|
773
|
+
});
|
|
774
|
+
expect(result.model).toBe("gemini-3-flash-001");
|
|
775
|
+
expect(result.usage).toEqual({ inputTokens: 15, outputTokens: 8 });
|
|
776
|
+
expect(result.stopReason).toBe("STOP");
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
test("managed transport handles tool calls correctly", async () => {
|
|
780
|
+
const managedProvider = new GeminiProvider(
|
|
781
|
+
"managed-key",
|
|
782
|
+
"gemini-3-flash",
|
|
783
|
+
{
|
|
784
|
+
managedBaseUrl: "https://platform.example.com/v1/runtime-proxy/gemini",
|
|
785
|
+
},
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
fakeChunks = [
|
|
789
|
+
functionCallChunk([
|
|
790
|
+
{ id: "call_managed", name: "file_read", args: { path: "/tmp/test" } },
|
|
791
|
+
]),
|
|
792
|
+
finishChunk("STOP", 10, 15),
|
|
793
|
+
];
|
|
794
|
+
|
|
795
|
+
const result = await managedProvider.sendMessage([
|
|
796
|
+
{ role: "user", content: [{ type: "text", text: "Read /tmp/test" }] },
|
|
797
|
+
]);
|
|
798
|
+
|
|
799
|
+
expect(result.content).toHaveLength(1);
|
|
800
|
+
expect(result.content[0]).toEqual({
|
|
801
|
+
type: "tool_use",
|
|
802
|
+
id: "call_managed",
|
|
803
|
+
name: "file_read",
|
|
804
|
+
input: { path: "/tmp/test" },
|
|
805
|
+
});
|
|
806
|
+
});
|
|
729
807
|
});
|