@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.
Files changed (121) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/package.json +1 -1
  3. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -7
  4. package/src/__tests__/anthropic-provider.test.ts +86 -1
  5. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
  6. package/src/__tests__/checker.test.ts +37 -98
  7. package/src/__tests__/commit-message-enrichment-service.test.ts +15 -0
  8. package/src/__tests__/config-schema.test.ts +6 -5
  9. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  10. package/src/__tests__/daemon-server-session-init.test.ts +1 -19
  11. package/src/__tests__/followup-tools.test.ts +0 -30
  12. package/src/__tests__/gemini-provider.test.ts +79 -1
  13. package/src/__tests__/ipc-snapshot.test.ts +0 -4
  14. package/src/__tests__/managed-proxy-context.test.ts +163 -0
  15. package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
  16. package/src/__tests__/memory-regressions.test.ts +6 -6
  17. package/src/__tests__/openai-provider.test.ts +82 -0
  18. package/src/__tests__/provider-fail-open-selection.test.ts +134 -1
  19. package/src/__tests__/provider-managed-proxy-integration.test.ts +269 -0
  20. package/src/__tests__/recurrence-types.test.ts +0 -15
  21. package/src/__tests__/schedule-tools.test.ts +28 -44
  22. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  23. package/src/__tests__/task-management-tools.test.ts +111 -0
  24. package/src/__tests__/twilio-config.test.ts +0 -3
  25. package/src/amazon/session.ts +30 -91
  26. package/src/calls/call-controller.ts +423 -571
  27. package/src/calls/finalize-call.ts +20 -0
  28. package/src/calls/relay-access-wait.ts +340 -0
  29. package/src/calls/relay-server.ts +267 -902
  30. package/src/calls/relay-setup-router.ts +307 -0
  31. package/src/calls/relay-verification.ts +280 -0
  32. package/src/calls/twilio-config.ts +1 -8
  33. package/src/calls/voice-control-protocol.ts +184 -0
  34. package/src/calls/voice-session-bridge.ts +1 -8
  35. package/src/config/agent-schema.ts +1 -1
  36. package/src/config/bundled-skills/followups/TOOLS.json +0 -4
  37. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  38. package/src/config/bundled-skills/schedule/TOOLS.json +2 -10
  39. package/src/config/core-schema.ts +1 -1
  40. package/src/config/env.ts +0 -10
  41. package/src/config/feature-flag-registry.json +1 -1
  42. package/src/config/loader.ts +19 -0
  43. package/src/config/schema.ts +2 -2
  44. package/src/daemon/handlers/session-history.ts +398 -0
  45. package/src/daemon/handlers/session-user-message.ts +982 -0
  46. package/src/daemon/handlers/sessions.ts +9 -1338
  47. package/src/daemon/ipc-contract/sessions.ts +0 -6
  48. package/src/daemon/ipc-contract-inventory.json +0 -1
  49. package/src/daemon/lifecycle.ts +0 -29
  50. package/src/home-base/app-link-store.ts +0 -7
  51. package/src/memory/conversation-attention-store.ts +1 -1
  52. package/src/memory/conversation-store.ts +0 -51
  53. package/src/memory/db-init.ts +5 -1
  54. package/src/memory/job-handlers/conflict.ts +24 -0
  55. package/src/memory/migrations/105-contacts-and-triage.ts +4 -7
  56. package/src/memory/migrations/134-contacts-notes-column.ts +50 -33
  57. package/src/memory/migrations/registry.ts +6 -0
  58. package/src/memory/recall-cache.ts +0 -5
  59. package/src/memory/schema/calls.ts +274 -0
  60. package/src/memory/schema/contacts.ts +125 -0
  61. package/src/memory/schema/conversations.ts +129 -0
  62. package/src/memory/schema/guardian.ts +172 -0
  63. package/src/memory/schema/index.ts +8 -0
  64. package/src/memory/schema/infrastructure.ts +205 -0
  65. package/src/memory/schema/memory-core.ts +196 -0
  66. package/src/memory/schema/notifications.ts +191 -0
  67. package/src/memory/schema/tasks.ts +78 -0
  68. package/src/memory/schema.ts +1 -1385
  69. package/src/memory/slack-thread-store.ts +0 -69
  70. package/src/notifications/decisions-store.ts +2 -105
  71. package/src/notifications/deliveries-store.ts +0 -11
  72. package/src/notifications/preferences-store.ts +1 -58
  73. package/src/permissions/checker.ts +6 -17
  74. package/src/providers/anthropic/client.ts +6 -2
  75. package/src/providers/gemini/client.ts +13 -2
  76. package/src/providers/managed-proxy/constants.ts +55 -0
  77. package/src/providers/managed-proxy/context.ts +77 -0
  78. package/src/providers/registry.ts +112 -0
  79. package/src/runtime/auth/__tests__/guard-tests.test.ts +51 -23
  80. package/src/runtime/http-server.ts +83 -722
  81. package/src/runtime/http-types.ts +0 -16
  82. package/src/runtime/middleware/auth.ts +0 -12
  83. package/src/runtime/routes/app-routes.ts +33 -0
  84. package/src/runtime/routes/approval-routes.ts +32 -0
  85. package/src/runtime/routes/attachment-routes.ts +32 -0
  86. package/src/runtime/routes/brain-graph-routes.ts +27 -0
  87. package/src/runtime/routes/call-routes.ts +41 -0
  88. package/src/runtime/routes/channel-readiness-routes.ts +20 -0
  89. package/src/runtime/routes/channel-routes.ts +70 -0
  90. package/src/runtime/routes/contact-routes.ts +63 -0
  91. package/src/runtime/routes/conversation-attention-routes.ts +15 -0
  92. package/src/runtime/routes/conversation-routes.ts +190 -193
  93. package/src/runtime/routes/debug-routes.ts +15 -0
  94. package/src/runtime/routes/events-routes.ts +16 -0
  95. package/src/runtime/routes/global-search-routes.ts +15 -0
  96. package/src/runtime/routes/guardian-action-routes.ts +22 -0
  97. package/src/runtime/routes/guardian-bootstrap-routes.ts +20 -0
  98. package/src/runtime/routes/guardian-refresh-routes.ts +20 -0
  99. package/src/runtime/routes/identity-routes.ts +20 -0
  100. package/src/runtime/routes/inbound-message-handler.ts +8 -0
  101. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +6 -6
  102. package/src/runtime/routes/integration-routes.ts +83 -0
  103. package/src/runtime/routes/invite-routes.ts +31 -0
  104. package/src/runtime/routes/migration-routes.ts +30 -0
  105. package/src/runtime/routes/pairing-routes.ts +18 -0
  106. package/src/runtime/routes/secret-routes.ts +20 -0
  107. package/src/runtime/routes/surface-action-routes.ts +26 -0
  108. package/src/runtime/routes/trust-rules-routes.ts +31 -0
  109. package/src/runtime/routes/twilio-routes.ts +79 -0
  110. package/src/schedule/recurrence-types.ts +1 -11
  111. package/src/tools/followups/followup_create.ts +9 -3
  112. package/src/tools/mcp/mcp-tool-factory.ts +0 -17
  113. package/src/tools/memory/definitions.ts +0 -6
  114. package/src/tools/network/script-proxy/session-manager.ts +38 -3
  115. package/src/tools/schedule/create.ts +1 -3
  116. package/src/tools/schedule/update.ts +9 -6
  117. package/src/twitter/session.ts +29 -77
  118. package/src/util/cookie-session.ts +114 -0
  119. package/src/__tests__/conversation-routes.test.ts +0 -99
  120. package/src/__tests__/task-tools.test.ts +0 -685
  121. 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<br/>sandbox_set (deprecated no-op)"]
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.4.31",
3
+ "version": "0.4.32",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "vellum": "./src/index.ts"
@@ -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: false
245
- expect(result).not.toContain('id="browser"');
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
- // 'legacy', 'strict', and 'workspace' without re-registering the mock.
58
+ // 'strict' and 'workspace' without re-registering the mock.
59
59
  interface TestConfig {
60
- permissions: { mode: "legacy" | "strict" | "workspace" };
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: "legacy" },
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 legacy so existing tests are not affected
173
- testConfig.permissions = { mode: "legacy" };
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 → prompt", async () => {
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 should auto-allow as before
1358
- const result = await check(
1359
- "file_read",
1360
- { path: "/tmp/test.txt" },
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
- const result = await check("file_write", { path: otherPath }, "/tmp");
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 = "legacy";
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 = "legacy";
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 = "legacy";
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: "legacy" };
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: "legacy" };
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: "legacy" };
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: "legacy" };
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: "legacy" };
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: "legacy" };
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("accepts explicit permissions.mode legacy", () => {
531
- const result = AssistantConfigSchema.parse({
532
- permissions: { mode: "legacy" },
533
- });
534
- expect(result.permissions.mode).toBe("legacy");
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(_opts: Record<string, unknown>) {}
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
  });