@vellumai/assistant 0.4.31 → 0.4.33

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 (193) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/docs/architecture/memory.md +1 -1
  3. package/package.json +1 -1
  4. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -7
  5. package/src/__tests__/access-request-decision.test.ts +83 -1
  6. package/src/__tests__/actor-token-service.test.ts +0 -1
  7. package/src/__tests__/anthropic-provider.test.ts +86 -1
  8. package/src/__tests__/approval-routes-http.test.ts +0 -1
  9. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
  10. package/src/__tests__/call-controller.test.ts +0 -1
  11. package/src/__tests__/call-routes-http.test.ts +0 -1
  12. package/src/__tests__/channel-guardian.test.ts +0 -1
  13. package/src/__tests__/channel-invite-transport.test.ts +52 -40
  14. package/src/__tests__/checker.test.ts +37 -98
  15. package/src/__tests__/commit-message-enrichment-service.test.ts +4 -23
  16. package/src/__tests__/computer-use-session-working-dir.test.ts +0 -1
  17. package/src/__tests__/config-schema.test.ts +6 -5
  18. package/src/__tests__/credential-security-invariants.test.ts +2 -0
  19. package/src/__tests__/daemon-server-session-init.test.ts +1 -19
  20. package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
  21. package/src/__tests__/followup-tools.test.ts +0 -30
  22. package/src/__tests__/gemini-provider.test.ts +79 -1
  23. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -1
  24. package/src/__tests__/guardian-dispatch.test.ts +0 -1
  25. package/src/__tests__/guardian-outbound-http.test.ts +0 -1
  26. package/src/__tests__/handlers-telegram-config.test.ts +0 -1
  27. package/src/__tests__/inbound-invite-redemption.test.ts +1 -4
  28. package/src/__tests__/ingress-reconcile.test.ts +3 -36
  29. package/src/__tests__/ipc-snapshot.test.ts +0 -4
  30. package/src/__tests__/managed-proxy-context.test.ts +163 -0
  31. package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
  32. package/src/__tests__/memory-regressions.test.ts +6 -6
  33. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1
  34. package/src/__tests__/migration-export-http.test.ts +0 -1
  35. package/src/__tests__/migration-import-commit-http.test.ts +0 -1
  36. package/src/__tests__/migration-import-preflight-http.test.ts +0 -1
  37. package/src/__tests__/migration-validate-http.test.ts +0 -1
  38. package/src/__tests__/non-member-access-request.test.ts +0 -1
  39. package/src/__tests__/notification-guardian-path.test.ts +0 -1
  40. package/src/__tests__/notification-telegram-adapter.test.ts +0 -4
  41. package/src/__tests__/openai-provider.test.ts +82 -0
  42. package/src/__tests__/provider-fail-open-selection.test.ts +134 -1
  43. package/src/__tests__/provider-managed-proxy-integration.test.ts +269 -0
  44. package/src/__tests__/recurrence-types.test.ts +0 -15
  45. package/src/__tests__/relay-server.test.ts +145 -2
  46. package/src/__tests__/sandbox-host-parity.test.ts +5 -2
  47. package/src/__tests__/schedule-tools.test.ts +28 -44
  48. package/src/__tests__/session-init.benchmark.test.ts +0 -2
  49. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  50. package/src/__tests__/slack-channel-config.test.ts +0 -1
  51. package/src/__tests__/slack-inbound-verification.test.ts +0 -1
  52. package/src/__tests__/sms-messaging-provider.test.ts +0 -4
  53. package/src/__tests__/task-management-tools.test.ts +111 -0
  54. package/src/__tests__/terminal-tools.test.ts +5 -2
  55. package/src/__tests__/trusted-contact-approval-notifier.test.ts +66 -74
  56. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  57. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -1
  58. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  59. package/src/__tests__/trusted-contact-verification.test.ts +0 -1
  60. package/src/__tests__/twilio-config.test.ts +0 -3
  61. package/src/__tests__/twilio-routes.test.ts +0 -1
  62. package/src/__tests__/update-bulletin.test.ts +0 -2
  63. package/src/__tests__/user-reference.test.ts +47 -1
  64. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  65. package/src/__tests__/workspace-git-service.test.ts +2 -2
  66. package/src/amazon/session.ts +30 -91
  67. package/src/calls/call-controller.ts +423 -571
  68. package/src/calls/finalize-call.ts +20 -0
  69. package/src/calls/relay-access-wait.ts +340 -0
  70. package/src/calls/relay-server.ts +271 -956
  71. package/src/calls/relay-setup-router.ts +307 -0
  72. package/src/calls/relay-verification.ts +280 -0
  73. package/src/calls/twilio-config.ts +1 -8
  74. package/src/calls/voice-control-protocol.ts +184 -0
  75. package/src/calls/voice-session-bridge.ts +1 -8
  76. package/src/channels/config.ts +41 -2
  77. package/src/config/agent-schema.ts +1 -1
  78. package/src/config/bundled-skills/followups/TOOLS.json +0 -4
  79. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  80. package/src/config/bundled-skills/schedule/TOOLS.json +2 -10
  81. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  82. package/src/config/bundled-skills/slack-digest-setup/SKILL.md +164 -0
  83. package/src/config/core-schema.ts +1 -1
  84. package/src/config/env.ts +0 -14
  85. package/src/config/feature-flag-registry.json +5 -5
  86. package/src/config/loader.ts +19 -0
  87. package/src/config/schema.ts +2 -2
  88. package/src/config/user-reference.ts +47 -9
  89. package/src/daemon/handlers/config-channels.ts +11 -10
  90. package/src/daemon/handlers/contacts.ts +5 -1
  91. package/src/daemon/handlers/session-history.ts +398 -0
  92. package/src/daemon/handlers/session-user-message.ts +982 -0
  93. package/src/daemon/handlers/sessions.ts +9 -1338
  94. package/src/daemon/ipc-contract/sessions.ts +0 -6
  95. package/src/daemon/ipc-contract-inventory.json +0 -1
  96. package/src/daemon/lifecycle.ts +18 -55
  97. package/src/home-base/app-link-store.ts +0 -7
  98. package/src/memory/channel-delivery-store.ts +1 -0
  99. package/src/memory/conversation-attention-store.ts +1 -1
  100. package/src/memory/conversation-store.ts +0 -51
  101. package/src/memory/db-init.ts +9 -1
  102. package/src/memory/delivery-crud.ts +13 -0
  103. package/src/memory/invite-store.ts +71 -1
  104. package/src/memory/job-handlers/conflict.ts +24 -0
  105. package/src/memory/migrations/040-invite-code-hash-column.ts +16 -0
  106. package/src/memory/migrations/105-contacts-and-triage.ts +4 -7
  107. package/src/memory/migrations/134-contacts-notes-column.ts +50 -33
  108. package/src/memory/migrations/index.ts +1 -0
  109. package/src/memory/migrations/registry.ts +6 -0
  110. package/src/memory/recall-cache.ts +0 -5
  111. package/src/memory/schema/calls.ts +274 -0
  112. package/src/memory/schema/contacts.ts +127 -0
  113. package/src/memory/schema/conversations.ts +129 -0
  114. package/src/memory/schema/guardian.ts +172 -0
  115. package/src/memory/schema/index.ts +8 -0
  116. package/src/memory/schema/infrastructure.ts +205 -0
  117. package/src/memory/schema/memory-core.ts +196 -0
  118. package/src/memory/schema/notifications.ts +191 -0
  119. package/src/memory/schema/tasks.ts +78 -0
  120. package/src/memory/schema.ts +1 -1385
  121. package/src/memory/slack-thread-store.ts +0 -69
  122. package/src/notifications/decisions-store.ts +2 -105
  123. package/src/notifications/deliveries-store.ts +0 -11
  124. package/src/notifications/preferences-store.ts +1 -58
  125. package/src/permissions/checker.ts +6 -17
  126. package/src/providers/anthropic/client.ts +6 -2
  127. package/src/providers/gemini/client.ts +13 -2
  128. package/src/providers/managed-proxy/constants.ts +55 -0
  129. package/src/providers/managed-proxy/context.ts +77 -0
  130. package/src/providers/registry.ts +112 -0
  131. package/src/runtime/auth/__tests__/guard-tests.test.ts +52 -26
  132. package/src/runtime/auth/token-service.ts +50 -0
  133. package/src/runtime/channel-guardian-service.ts +1 -3
  134. package/src/runtime/channel-invite-transport.ts +121 -34
  135. package/src/runtime/channel-invite-transports/email.ts +50 -0
  136. package/src/runtime/channel-invite-transports/slack.ts +81 -0
  137. package/src/runtime/channel-invite-transports/sms.ts +70 -0
  138. package/src/runtime/channel-invite-transports/telegram.ts +29 -11
  139. package/src/runtime/channel-invite-transports/voice.ts +12 -12
  140. package/src/runtime/http-server.ts +83 -722
  141. package/src/runtime/http-types.ts +0 -16
  142. package/src/runtime/invite-redemption-service.ts +193 -0
  143. package/src/runtime/invite-redemption-templates.ts +6 -6
  144. package/src/runtime/invite-service.ts +81 -11
  145. package/src/runtime/middleware/auth.ts +0 -12
  146. package/src/runtime/routes/access-request-decision.ts +52 -6
  147. package/src/runtime/routes/app-routes.ts +33 -0
  148. package/src/runtime/routes/approval-routes.ts +32 -0
  149. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -0
  150. package/src/runtime/routes/attachment-routes.ts +32 -0
  151. package/src/runtime/routes/brain-graph-routes.ts +27 -0
  152. package/src/runtime/routes/call-routes.ts +41 -0
  153. package/src/runtime/routes/channel-readiness-routes.ts +20 -0
  154. package/src/runtime/routes/channel-routes.ts +70 -0
  155. package/src/runtime/routes/contact-routes.ts +96 -6
  156. package/src/runtime/routes/conversation-attention-routes.ts +15 -0
  157. package/src/runtime/routes/conversation-routes.ts +190 -193
  158. package/src/runtime/routes/debug-routes.ts +15 -0
  159. package/src/runtime/routes/events-routes.ts +16 -0
  160. package/src/runtime/routes/global-search-routes.ts +15 -0
  161. package/src/runtime/routes/guardian-action-routes.ts +22 -0
  162. package/src/runtime/routes/guardian-bootstrap-routes.ts +21 -6
  163. package/src/runtime/routes/guardian-refresh-routes.ts +20 -0
  164. package/src/runtime/routes/identity-routes.ts +20 -0
  165. package/src/runtime/routes/inbound-message-handler.ts +9 -3
  166. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +295 -10
  167. package/src/runtime/routes/inbound-stages/background-dispatch.ts +9 -42
  168. package/src/runtime/routes/inbound-stages/edit-intercept.ts +10 -0
  169. package/src/runtime/routes/integration-routes.ts +83 -0
  170. package/src/runtime/routes/invite-routes.ts +32 -0
  171. package/src/runtime/routes/migration-routes.ts +30 -0
  172. package/src/runtime/routes/pairing-routes.ts +18 -0
  173. package/src/runtime/routes/secret-routes.ts +20 -0
  174. package/src/runtime/routes/surface-action-routes.ts +26 -0
  175. package/src/runtime/routes/trust-rules-routes.ts +31 -0
  176. package/src/runtime/routes/twilio-routes.ts +79 -0
  177. package/src/schedule/recurrence-types.ts +1 -11
  178. package/src/tools/browser/browser-manager.ts +10 -1
  179. package/src/tools/browser/runtime-check.ts +3 -1
  180. package/src/tools/followups/followup_create.ts +9 -3
  181. package/src/tools/mcp/mcp-tool-factory.ts +0 -17
  182. package/src/tools/memory/definitions.ts +0 -6
  183. package/src/tools/network/script-proxy/session-manager.ts +38 -3
  184. package/src/tools/schedule/create.ts +1 -3
  185. package/src/tools/schedule/update.ts +9 -6
  186. package/src/tools/shared/shell-output.ts +7 -2
  187. package/src/twitter/session.ts +29 -77
  188. package/src/util/cookie-session.ts +114 -0
  189. package/src/util/platform.ts +0 -4
  190. package/src/workspace/git-service.ts +10 -4
  191. package/src/__tests__/conversation-routes.test.ts +0 -99
  192. package/src/__tests__/task-tools.test.ts +0 -685
  193. 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)"]
@@ -487,7 +487,7 @@ graph TB
487
487
 
488
488
  ### How it works
489
489
 
490
- 1. **Lazy initialization**: The git repository is created on first use, not at workspace creation. When `ensureInitialized()` is called, it checks for a `.git` directory. If absent, it runs `git init`, creates a `.gitignore` (excluding `data/`, `logs/`, `*.log`, `*.sock`, `*.pid`, `session-token`, `http-token`), sets the git identity to "Vellum Assistant", and creates an initial baseline commit capturing any pre-existing files. The baseline commit is intentional — it makes `git log`, `git diff`, and `git revert` work cleanly from the start. Both new and existing workspaces get the same treatment. For existing repos (e.g. created by older versions or external tools), `.gitignore` rules and git identity are set idempotently on each init, ensuring proper configuration regardless of how the repo was originally created.
490
+ 1. **Lazy initialization**: The git repository is created on first use, not at workspace creation. When `ensureInitialized()` is called, it checks for a `.git` directory. If absent, it runs `git init`, creates a `.gitignore` (excluding `data/`, `logs/`, `*.log`, `*.sock`, `*.pid`, `session-token`), sets the git identity to "Vellum Assistant", and creates an initial baseline commit capturing any pre-existing files. The baseline commit is intentional — it makes `git log`, `git diff`, and `git revert` work cleanly from the start. Both new and existing workspaces get the same treatment. For existing repos (e.g. created by older versions or external tools), `.gitignore` rules and git identity are set idempotently on each init, ensuring proper configuration regardless of how the repo was originally created.
491
491
 
492
492
  2. **Turn-boundary commits**: After each conversation turn (user message + assistant response cycle), `session.ts` commits workspace changes via `commitTurnChanges(workspaceDir, sessionId, turnNumber)`. The commit runs in the `finally` block of `runAgentLoop`, guarded by a `turnStarted` flag that is set once the agent loop begins executing. This guarantees a commit attempt even when post-processing (e.g. `resolveAssistantAttachments`) throws, or when the user cancels mid-turn. The commit is raced against a configurable timeout (`workspaceGit.turnCommitMaxWaitMs`, default 4s) via `Promise.race`. If the commit exceeds the timeout, the turn proceeds immediately while the commit continues in the background. Note: the background commit is NOT awaited before the next turn starts, so brief cross-turn file attribution windows are possible but accepted as a tradeoff for responsiveness. Commit outcomes are logged with structured fields (`sessionId`, `turnNumber`, `filesChanged`, `durationMs`) for observability.
493
493
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.4.31",
3
+ "version": "0.4.33",
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,
@@ -30,7 +30,6 @@ mock.module("../util/platform.js", () => ({
30
30
  getDbPath: () => join(testDir, "test.db"),
31
31
  getLogPath: () => join(testDir, "test.log"),
32
32
  ensureDataDir: () => {},
33
- readHttpToken: () => "test-bearer-token",
34
33
  }));
35
34
 
36
35
  mock.module("../util/logger.js", () => ({
@@ -331,4 +330,87 @@ describe("access request notification delivery", () => {
331
330
  expect(text).toContain("unable to deliver");
332
331
  expect(text).toContain("try again");
333
332
  });
333
+
334
+ test("slack approval notification is sent as DM using requesterExternalUserId", async () => {
335
+ await notifyRequesterOfApproval({
336
+ replyCallbackUrl:
337
+ "http://localhost:7830/deliver/slack?threadTs=1234.5678",
338
+ requesterChatId: "C12345-channel",
339
+ requesterExternalUserId: "U98765-user",
340
+ channel: "slack",
341
+ assistantId: "self",
342
+ bearerToken: "test-token",
343
+ });
344
+
345
+ expect(deliverReplyCalls.length).toBe(1);
346
+ const call = deliverReplyCalls[0];
347
+ // Should target the user ID (DM) not the channel
348
+ expect(call.payload.chatId).toBe("U98765-user");
349
+ // threadTs should be stripped — it belongs to the guardian's channel thread
350
+ expect(call.url).not.toContain("threadTs");
351
+ });
352
+
353
+ test("slack denial notification is sent as DM using requesterExternalUserId", async () => {
354
+ await notifyRequesterOfDenial({
355
+ replyCallbackUrl:
356
+ "http://localhost:7830/deliver/slack?threadTs=1234.5678",
357
+ requesterChatId: "C12345-channel",
358
+ requesterExternalUserId: "U98765-user",
359
+ channel: "slack",
360
+ assistantId: "self",
361
+ bearerToken: "test-token",
362
+ });
363
+
364
+ expect(deliverReplyCalls.length).toBe(1);
365
+ const call = deliverReplyCalls[0];
366
+ expect(call.payload.chatId).toBe("U98765-user");
367
+ expect(call.url).not.toContain("threadTs");
368
+ });
369
+
370
+ test("slack delivery failure notification is sent as DM using requesterExternalUserId", async () => {
371
+ await notifyRequesterOfDeliveryFailure({
372
+ replyCallbackUrl:
373
+ "http://localhost:7830/deliver/slack?threadTs=1234.5678",
374
+ requesterChatId: "C12345-channel",
375
+ requesterExternalUserId: "U98765-user",
376
+ channel: "slack",
377
+ assistantId: "self",
378
+ bearerToken: "test-token",
379
+ });
380
+
381
+ expect(deliverReplyCalls.length).toBe(1);
382
+ const call = deliverReplyCalls[0];
383
+ expect(call.payload.chatId).toBe("U98765-user");
384
+ expect(call.url).not.toContain("threadTs");
385
+ });
386
+
387
+ test("non-slack channels still use requesterChatId and preserve threadTs", async () => {
388
+ await notifyRequesterOfApproval({
389
+ replyCallbackUrl:
390
+ "http://localhost:7830/deliver/telegram?threadTs=1234.5678",
391
+ requesterChatId: "chat-123",
392
+ requesterExternalUserId: "user-456",
393
+ channel: "telegram",
394
+ assistantId: "self",
395
+ bearerToken: "test-token",
396
+ });
397
+
398
+ expect(deliverReplyCalls.length).toBe(1);
399
+ expect(deliverReplyCalls[0].payload.chatId).toBe("chat-123");
400
+ // threadTs should be preserved for non-slack channels
401
+ expect(deliverReplyCalls[0].url).toContain("threadTs=1234.5678");
402
+ });
403
+
404
+ test("slack without requesterExternalUserId falls back to requesterChatId", async () => {
405
+ await notifyRequesterOfApproval({
406
+ replyCallbackUrl: "http://localhost:7830/deliver/slack",
407
+ requesterChatId: "C12345-channel",
408
+ channel: "slack",
409
+ assistantId: "self",
410
+ bearerToken: "test-token",
411
+ });
412
+
413
+ expect(deliverReplyCalls.length).toBe(1);
414
+ expect(deliverReplyCalls[0].payload.chatId).toBe("C12345-channel");
415
+ });
334
416
  });
@@ -39,7 +39,6 @@ mock.module("../config/env.js", () => ({
39
39
  isHttpAuthDisabled: () => false,
40
40
  getInternalGatewayTarget: () => "http://localhost:7822",
41
41
  getGatewayBaseUrl: () => "http://localhost:7822",
42
- getRuntimeProxyBearerToken: () => undefined,
43
42
  getRuntimeGatewayOriginSecret: () => undefined,
44
43
  isHttpAuthDisabledWithoutSafetyGate: () => false,
45
44
  getEnableMonitoring: () => false,
@@ -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
+ });
@@ -57,7 +57,6 @@ mock.module("../config/env.js", () => ({
57
57
  getGatewayPort: () => 7830,
58
58
  getRuntimeHttpPort: () => 7821,
59
59
  getRuntimeHttpHost: () => "127.0.0.1",
60
- getRuntimeProxyBearerToken: () => undefined,
61
60
  getRuntimeGatewayOriginSecret: () => undefined,
62
61
  getIngressPublicBaseUrl: () => undefined,
63
62
  setIngressPublicBaseUrl: () => {},
@@ -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
 
@@ -27,7 +27,6 @@ mock.module("../util/platform.js", () => ({
27
27
  getDbPath: () => join(testDir, "test.db"),
28
28
  getLogPath: () => join(testDir, "test.log"),
29
29
  ensureDataDir: () => {},
30
- readHttpToken: () => null,
31
30
  }));
32
31
 
33
32
  mock.module("../util/logger.js", () => ({
@@ -27,7 +27,6 @@ mock.module("../util/platform.js", () => ({
27
27
  getDbPath: () => join(testDir, "test.db"),
28
28
  getLogPath: () => join(testDir, "test.log"),
29
29
  ensureDataDir: () => {},
30
- readHttpToken: () => null,
31
30
  }));
32
31
 
33
32
  mock.module("../util/logger.js", () => ({
@@ -21,7 +21,6 @@ mock.module("../util/platform.js", () => ({
21
21
  getDbPath: () => join(testDir, "test.db"),
22
22
  getLogPath: () => join(testDir, "test.log"),
23
23
  ensureDataDir: () => {},
24
- readHttpToken: () => "test-bearer-token",
25
24
  }));
26
25
 
27
26
  mock.module("../util/logger.js", () => ({
@@ -17,19 +17,19 @@ mock.module("../tools/credentials/metadata-store.js", () => ({
17
17
 
18
18
  import {
19
19
  _resetRegistry,
20
- type ChannelInviteTransport,
20
+ type ChannelInviteAdapter,
21
+ getInviteAdapterRegistry,
21
22
  getTransport,
22
23
  registerTransport,
23
24
  } from "../runtime/channel-invite-transport.js";
24
- // Importing the Telegram module auto-registers the transport
25
- import { telegramInviteTransport } from "../runtime/channel-invite-transports/telegram.js";
25
+ import { telegramInviteAdapter } from "../runtime/channel-invite-transports/telegram.js";
26
26
 
27
27
  describe("channel-invite-transport", () => {
28
28
  beforeEach(() => {
29
29
  _resetRegistry();
30
30
  mockBotUsername = "test_invite_bot";
31
31
  // Re-register after reset so Telegram tests work
32
- registerTransport(telegramInviteTransport);
32
+ registerTransport(telegramInviteAdapter);
33
33
  });
34
34
 
35
35
  // =========================================================================
@@ -37,46 +37,58 @@ describe("channel-invite-transport", () => {
37
37
  // =========================================================================
38
38
 
39
39
  describe("registry", () => {
40
- test("returns the Telegram transport for telegram channel", () => {
41
- const transport = getTransport("telegram");
42
- expect(transport).toBeDefined();
43
- expect(transport!.channel).toBe("telegram");
40
+ test("returns the Telegram adapter for telegram channel", () => {
41
+ const adapter = getTransport("telegram");
42
+ expect(adapter).toBeDefined();
43
+ expect(adapter!.channel).toBe("telegram");
44
44
  });
45
45
 
46
46
  test("returns undefined for an unregistered channel", () => {
47
- const transport = getTransport("sms");
48
- expect(transport).toBeUndefined();
47
+ const adapter = getTransport("sms");
48
+ expect(adapter).toBeUndefined();
49
49
  });
50
50
 
51
- test("overwrites a previously registered transport for the same channel", () => {
52
- const custom: ChannelInviteTransport = {
51
+ test("overwrites a previously registered adapter for the same channel", () => {
52
+ const custom: ChannelInviteAdapter = {
53
53
  channel: "telegram",
54
- buildShareableInvite: () => ({ url: "custom", displayText: "custom" }),
54
+ buildShareLink: () => ({ url: "custom", displayText: "custom" }),
55
55
  extractInboundToken: () => undefined,
56
56
  };
57
57
  registerTransport(custom);
58
- const transport = getTransport("telegram");
58
+ const adapter = getTransport("telegram");
59
59
  expect(
60
- transport!.buildShareableInvite({
60
+ adapter!.buildShareLink!({
61
61
  rawToken: "x",
62
62
  sourceChannel: "telegram",
63
63
  }).url,
64
64
  ).toBe("custom");
65
65
  });
66
66
 
67
- test("_resetRegistry clears all transports", () => {
67
+ test("_resetRegistry clears all adapters", () => {
68
68
  _resetRegistry();
69
69
  expect(getTransport("telegram")).toBeUndefined();
70
70
  });
71
+
72
+ test("getInviteAdapterRegistry returns the singleton registry", () => {
73
+ const registry = getInviteAdapterRegistry();
74
+ expect(registry.get("telegram")).toBeDefined();
75
+ });
76
+
77
+ test("registry.getAll returns all registered adapters", () => {
78
+ const registry = getInviteAdapterRegistry();
79
+ const all = registry.getAll();
80
+ expect(all.length).toBeGreaterThanOrEqual(1);
81
+ expect(all.some((a) => a.channel === "telegram")).toBe(true);
82
+ });
71
83
  });
72
84
 
73
85
  // =========================================================================
74
- // Telegram adapter — buildShareableInvite
86
+ // Telegram adapter — buildShareLink
75
87
  // =========================================================================
76
88
 
77
- describe("telegram buildShareableInvite", () => {
89
+ describe("telegram buildShareLink", () => {
78
90
  test("produces a valid Telegram deep link", () => {
79
- const result = telegramInviteTransport.buildShareableInvite!({
91
+ const result = telegramInviteAdapter.buildShareLink!({
80
92
  rawToken: "abc123_test-token",
81
93
  sourceChannel: "telegram",
82
94
  });
@@ -90,11 +102,11 @@ describe("channel-invite-transport", () => {
90
102
  });
91
103
 
92
104
  test("deep link is deterministic for the same token", () => {
93
- const a = telegramInviteTransport.buildShareableInvite!({
105
+ const a = telegramInviteAdapter.buildShareLink!({
94
106
  rawToken: "tok1",
95
107
  sourceChannel: "telegram",
96
108
  });
97
- const b = telegramInviteTransport.buildShareableInvite!({
109
+ const b = telegramInviteAdapter.buildShareLink!({
98
110
  rawToken: "tok1",
99
111
  sourceChannel: "telegram",
100
112
  });
@@ -104,7 +116,7 @@ describe("channel-invite-transport", () => {
104
116
 
105
117
  test("uses the configured bot username", () => {
106
118
  mockBotUsername = "my_custom_bot";
107
- const result = telegramInviteTransport.buildShareableInvite!({
119
+ const result = telegramInviteAdapter.buildShareLink!({
108
120
  rawToken: "token",
109
121
  sourceChannel: "telegram",
110
122
  });
@@ -118,7 +130,7 @@ describe("channel-invite-transport", () => {
118
130
  delete process.env.TELEGRAM_BOT_USERNAME;
119
131
  try {
120
132
  expect(() =>
121
- telegramInviteTransport.buildShareableInvite!({
133
+ telegramInviteAdapter.buildShareLink!({
122
134
  rawToken: "token",
123
135
  sourceChannel: "telegram",
124
136
  }),
@@ -133,7 +145,7 @@ describe("channel-invite-transport", () => {
133
145
  const prev = process.env.TELEGRAM_BOT_USERNAME;
134
146
  process.env.TELEGRAM_BOT_USERNAME = "env_bot";
135
147
  try {
136
- const result = telegramInviteTransport.buildShareableInvite!({
148
+ const result = telegramInviteAdapter.buildShareLink!({
137
149
  rawToken: "token",
138
150
  sourceChannel: "telegram",
139
151
  });
@@ -154,7 +166,7 @@ describe("channel-invite-transport", () => {
154
166
 
155
167
  describe("telegram extractInboundToken", () => {
156
168
  test("extracts token from structured commandIntent", () => {
157
- const token = telegramInviteTransport.extractInboundToken({
169
+ const token = telegramInviteAdapter.extractInboundToken!({
158
170
  commandIntent: { type: "start", payload: "iv_abc123" },
159
171
  content: "/start iv_abc123",
160
172
  });
@@ -162,7 +174,7 @@ describe("channel-invite-transport", () => {
162
174
  });
163
175
 
164
176
  test("extracts base64url token from commandIntent", () => {
165
- const token = telegramInviteTransport.extractInboundToken({
177
+ const token = telegramInviteAdapter.extractInboundToken!({
166
178
  commandIntent: { type: "start", payload: "iv_YWJjMTIz-_test" },
167
179
  content: "/start iv_YWJjMTIz-_test",
168
180
  });
@@ -170,7 +182,7 @@ describe("channel-invite-transport", () => {
170
182
  });
171
183
 
172
184
  test("returns undefined when commandIntent has no payload", () => {
173
- const token = telegramInviteTransport.extractInboundToken({
185
+ const token = telegramInviteAdapter.extractInboundToken!({
174
186
  commandIntent: { type: "start" },
175
187
  content: "/start",
176
188
  });
@@ -178,7 +190,7 @@ describe("channel-invite-transport", () => {
178
190
  });
179
191
 
180
192
  test("returns undefined when commandIntent payload has wrong prefix (gv_)", () => {
181
- const token = telegramInviteTransport.extractInboundToken({
193
+ const token = telegramInviteAdapter.extractInboundToken!({
182
194
  commandIntent: { type: "start", payload: "gv_abc123" },
183
195
  content: "/start gv_abc123",
184
196
  });
@@ -186,7 +198,7 @@ describe("channel-invite-transport", () => {
186
198
  });
187
199
 
188
200
  test("returns undefined when commandIntent payload has no prefix", () => {
189
- const token = telegramInviteTransport.extractInboundToken({
201
+ const token = telegramInviteAdapter.extractInboundToken!({
190
202
  commandIntent: { type: "start", payload: "abc123" },
191
203
  content: "/start abc123",
192
204
  });
@@ -194,7 +206,7 @@ describe("channel-invite-transport", () => {
194
206
  });
195
207
 
196
208
  test("returns undefined when commandIntent type is not start", () => {
197
- const token = telegramInviteTransport.extractInboundToken({
209
+ const token = telegramInviteAdapter.extractInboundToken!({
198
210
  commandIntent: { type: "help", payload: "iv_abc123" },
199
211
  content: "/help iv_abc123",
200
212
  });
@@ -202,7 +214,7 @@ describe("channel-invite-transport", () => {
202
214
  });
203
215
 
204
216
  test("returns undefined when commandIntent payload is iv_ with empty token", () => {
205
- const token = telegramInviteTransport.extractInboundToken({
217
+ const token = telegramInviteAdapter.extractInboundToken!({
206
218
  commandIntent: { type: "start", payload: "iv_" },
207
219
  content: "/start iv_",
208
220
  });
@@ -210,7 +222,7 @@ describe("channel-invite-transport", () => {
210
222
  });
211
223
 
212
224
  test("returns undefined when commandIntent payload is iv_ with whitespace-only token", () => {
213
- const token = telegramInviteTransport.extractInboundToken({
225
+ const token = telegramInviteAdapter.extractInboundToken!({
214
226
  commandIntent: { type: "start", payload: "iv_ " },
215
227
  content: "/start iv_ ",
216
228
  });
@@ -218,49 +230,49 @@ describe("channel-invite-transport", () => {
218
230
  });
219
231
 
220
232
  test("extracts token from raw content fallback", () => {
221
- const token = telegramInviteTransport.extractInboundToken({
233
+ const token = telegramInviteAdapter.extractInboundToken!({
222
234
  content: "/start iv_abc123",
223
235
  });
224
236
  expect(token).toBe("abc123");
225
237
  });
226
238
 
227
239
  test("extracts token from raw content with extra whitespace", () => {
228
- const token = telegramInviteTransport.extractInboundToken({
240
+ const token = telegramInviteAdapter.extractInboundToken!({
229
241
  content: "/start iv_token123",
230
242
  });
231
243
  expect(token).toBe("token123");
232
244
  });
233
245
 
234
246
  test("returns undefined for empty content", () => {
235
- const token = telegramInviteTransport.extractInboundToken({
247
+ const token = telegramInviteAdapter.extractInboundToken!({
236
248
  content: "",
237
249
  });
238
250
  expect(token).toBeUndefined();
239
251
  });
240
252
 
241
253
  test("returns undefined for content without /start", () => {
242
- const token = telegramInviteTransport.extractInboundToken({
254
+ const token = telegramInviteAdapter.extractInboundToken!({
243
255
  content: "hello world",
244
256
  });
245
257
  expect(token).toBeUndefined();
246
258
  });
247
259
 
248
260
  test("returns undefined for /start without iv_ prefix in content", () => {
249
- const token = telegramInviteTransport.extractInboundToken({
261
+ const token = telegramInviteAdapter.extractInboundToken!({
250
262
  content: "/start gv_abc123",
251
263
  });
252
264
  expect(token).toBeUndefined();
253
265
  });
254
266
 
255
267
  test("returns undefined for malformed /start with only iv_ in content", () => {
256
- const token = telegramInviteTransport.extractInboundToken({
268
+ const token = telegramInviteAdapter.extractInboundToken!({
257
269
  content: "/start iv_",
258
270
  });
259
271
  expect(token).toBeUndefined();
260
272
  });
261
273
 
262
274
  test("prefers commandIntent over raw content", () => {
263
- const token = telegramInviteTransport.extractInboundToken({
275
+ const token = telegramInviteAdapter.extractInboundToken!({
264
276
  commandIntent: { type: "start", payload: "iv_from_intent" },
265
277
  content: "/start iv_from_content",
266
278
  });
@@ -269,7 +281,7 @@ describe("channel-invite-transport", () => {
269
281
 
270
282
  test("returns undefined when commandIntent rejects, even if content has token", () => {
271
283
  // commandIntent present but payload has wrong prefix
272
- const token = telegramInviteTransport.extractInboundToken({
284
+ const token = telegramInviteAdapter.extractInboundToken!({
273
285
  commandIntent: { type: "start", payload: "gv_abc123" },
274
286
  content: "/start iv_valid_token",
275
287
  });