@vellumai/assistant 0.4.48 → 0.4.49

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 (252) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/README.md +2 -23
  3. package/docs/architecture/integrations.md +45 -41
  4. package/docs/architecture/keychain-broker.md +3 -3
  5. package/docs/runbook-trusted-contacts.md +3 -8
  6. package/hook-templates/debug-prompt-logger/hook.json +1 -1
  7. package/hook-templates/debug-prompt-logger/run.sh +1 -3
  8. package/package.json +1 -1
  9. package/src/__tests__/actor-token-service.test.ts +0 -1
  10. package/src/__tests__/anthropic-provider.test.ts +156 -0
  11. package/src/__tests__/approval-cascade.test.ts +810 -0
  12. package/src/__tests__/approval-primitive.test.ts +0 -1
  13. package/src/__tests__/approval-routes-http.test.ts +2 -0
  14. package/src/__tests__/assistant-attachments.test.ts +12 -34
  15. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
  16. package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
  17. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  18. package/src/__tests__/channel-guardian.test.ts +0 -2
  19. package/src/__tests__/channel-readiness-routes.test.ts +15 -6
  20. package/src/__tests__/channel-readiness-service.test.ts +10 -9
  21. package/src/__tests__/checker.test.ts +9 -29
  22. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  23. package/src/__tests__/computer-use-tools.test.ts +2 -19
  24. package/src/__tests__/config-watcher.test.ts +0 -1
  25. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  26. package/src/__tests__/context-image-dimensions.test.ts +332 -0
  27. package/src/__tests__/context-token-estimator.test.ts +196 -13
  28. package/src/__tests__/conversation-attention-store.test.ts +0 -1
  29. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  30. package/src/__tests__/conversation-routes-guardian-reply.test.ts +144 -0
  31. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  32. package/src/__tests__/credential-metadata-store.test.ts +64 -73
  33. package/src/__tests__/credential-security-invariants.test.ts +13 -7
  34. package/src/__tests__/credential-vault-unit.test.ts +280 -49
  35. package/src/__tests__/credential-vault.test.ts +138 -16
  36. package/src/__tests__/credentials-cli.test.ts +71 -0
  37. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  38. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  39. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  40. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
  41. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
  42. package/src/__tests__/guardian-routing-invariants.test.ts +0 -1
  43. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
  44. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
  45. package/src/__tests__/heartbeat-service.test.ts +0 -1
  46. package/src/__tests__/host-cu-proxy.test.ts +629 -0
  47. package/src/__tests__/host-shell-tool.test.ts +27 -15
  48. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  49. package/src/__tests__/ingress-url-consistency.test.ts +14 -21
  50. package/src/__tests__/integration-status.test.ts +32 -51
  51. package/src/__tests__/intent-routing.test.ts +0 -1
  52. package/src/__tests__/invite-routes-http.test.ts +10 -9
  53. package/src/__tests__/keychain-broker-client.test.ts +11 -43
  54. package/src/__tests__/notification-routing-intent.test.ts +0 -1
  55. package/src/__tests__/oauth-cli.test.ts +373 -14
  56. package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
  57. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  58. package/src/__tests__/oauth-store.test.ts +756 -0
  59. package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
  60. package/src/__tests__/provider-error-scenarios.test.ts +0 -1
  61. package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
  62. package/src/__tests__/public-ingress-urls.test.ts +15 -21
  63. package/src/__tests__/recording-handler.test.ts +3 -4
  64. package/src/__tests__/registry.test.ts +2 -2
  65. package/src/__tests__/runtime-events-sse.test.ts +55 -7
  66. package/src/__tests__/schedule-store.test.ts +0 -1
  67. package/src/__tests__/scheduler-recurrence.test.ts +0 -1
  68. package/src/__tests__/scoped-approval-grants.test.ts +0 -1
  69. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
  70. package/src/__tests__/secret-ingress-handler.test.ts +0 -1
  71. package/src/__tests__/send-endpoint-busy.test.ts +21 -6
  72. package/src/__tests__/sequence-store.test.ts +0 -1
  73. package/src/__tests__/session-init.benchmark.test.ts +4 -5
  74. package/src/__tests__/skill-include-graph.test.ts +66 -0
  75. package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
  76. package/src/__tests__/skill-load-tool.test.ts +149 -1
  77. package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
  78. package/src/__tests__/skills-uninstall.test.ts +1 -1
  79. package/src/__tests__/skills.test.ts +3 -3
  80. package/src/__tests__/slack-channel-config.test.ts +67 -3
  81. package/src/__tests__/slack-share-routes.test.ts +17 -19
  82. package/src/__tests__/system-prompt.test.ts +0 -1
  83. package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
  84. package/src/__tests__/terminal-tools.test.ts +4 -3
  85. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  86. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  87. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
  88. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  89. package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
  90. package/src/__tests__/tool-executor.test.ts +0 -1
  91. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
  92. package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
  93. package/src/__tests__/trust-store.test.ts +1 -22
  94. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  95. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  96. package/src/__tests__/twilio-routes.test.ts +0 -16
  97. package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
  98. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  99. package/src/agent/ax-tree-compaction.test.ts +235 -0
  100. package/src/agent/loop.ts +76 -130
  101. package/src/calls/call-domain.ts +1 -6
  102. package/src/calls/relay-server.ts +9 -13
  103. package/src/calls/twilio-config.ts +2 -7
  104. package/src/calls/twilio-routes.ts +1 -2
  105. package/src/calls/voice-ingress-preflight.ts +1 -1
  106. package/src/cli/commands/browser-relay.ts +18 -12
  107. package/src/cli/commands/completions.ts +0 -3
  108. package/src/cli/commands/credentials.ts +101 -15
  109. package/src/cli/commands/oauth/apps.ts +255 -0
  110. package/src/cli/commands/oauth/connections.ts +299 -0
  111. package/src/cli/commands/oauth/index.ts +52 -0
  112. package/src/cli/commands/oauth/providers.ts +242 -0
  113. package/src/cli/commands/skills.ts +4 -338
  114. package/src/cli/program.ts +1 -5
  115. package/src/cli/reference.ts +1 -3
  116. package/src/config/assistant-feature-flags.ts +0 -3
  117. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  118. package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
  119. package/src/config/bundled-skills/computer-use/TOOLS.json +22 -4
  120. package/src/config/bundled-skills/google-calendar/calendar-client.ts +21 -16
  121. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -4
  122. package/src/config/bundled-skills/settings/SKILL.md +1 -1
  123. package/src/config/bundled-skills/settings/TOOLS.json +2 -8
  124. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
  125. package/src/config/env-registry.ts +14 -83
  126. package/src/config/env.ts +11 -50
  127. package/src/config/feature-flag-registry.json +16 -16
  128. package/src/config/loader.ts +0 -6
  129. package/src/config/schema.ts +3 -1
  130. package/src/config/skills.ts +21 -2
  131. package/src/context/image-dimensions.ts +229 -0
  132. package/src/context/token-estimator.ts +75 -12
  133. package/src/context/window-manager.ts +49 -10
  134. package/src/daemon/assistant-attachments.ts +1 -13
  135. package/src/daemon/handlers/config-ingress.ts +8 -33
  136. package/src/daemon/handlers/config-slack-channel.ts +49 -46
  137. package/src/daemon/handlers/config-telegram.ts +32 -16
  138. package/src/daemon/handlers/sessions.ts +10 -24
  139. package/src/daemon/handlers/shared.ts +0 -130
  140. package/src/daemon/host-cu-proxy.ts +401 -0
  141. package/src/daemon/lifecycle.ts +36 -68
  142. package/src/daemon/message-protocol.ts +3 -0
  143. package/src/daemon/message-types/computer-use.ts +2 -119
  144. package/src/daemon/message-types/host-cu.ts +19 -0
  145. package/src/daemon/message-types/messages.ts +3 -0
  146. package/src/daemon/server.ts +14 -21
  147. package/src/daemon/session-agent-loop-handlers.ts +2 -0
  148. package/src/daemon/session-attachments.ts +1 -2
  149. package/src/daemon/session-slash.ts +1 -1
  150. package/src/daemon/session-surfaces.ts +40 -28
  151. package/src/daemon/session-tool-setup.ts +2 -9
  152. package/src/daemon/session.ts +138 -15
  153. package/src/daemon/tool-side-effects.ts +2 -8
  154. package/src/daemon/watch-handler.ts +2 -2
  155. package/src/events/tool-metrics-listener.ts +2 -2
  156. package/src/hooks/manager.ts +1 -4
  157. package/src/inbound/public-ingress-urls.ts +7 -7
  158. package/src/logfire.ts +16 -5
  159. package/src/memory/conversation-key-store.ts +21 -0
  160. package/src/memory/db-init.ts +4 -0
  161. package/src/memory/migrations/149-oauth-tables.ts +60 -0
  162. package/src/memory/migrations/index.ts +1 -0
  163. package/src/memory/schema/index.ts +1 -0
  164. package/src/memory/schema/oauth.ts +65 -0
  165. package/src/messaging/provider.ts +4 -4
  166. package/src/messaging/providers/gmail/client.ts +82 -2
  167. package/src/messaging/providers/gmail/people-client.ts +10 -10
  168. package/src/messaging/providers/telegram-bot/adapter.ts +17 -17
  169. package/src/messaging/providers/whatsapp/adapter.ts +11 -8
  170. package/src/messaging/registry.ts +2 -32
  171. package/src/notifications/copy-composer.ts +0 -5
  172. package/src/notifications/signal.ts +4 -5
  173. package/src/oauth/byo-connection.test.ts +126 -25
  174. package/src/oauth/byo-connection.ts +22 -6
  175. package/src/oauth/connect-orchestrator.ts +113 -57
  176. package/src/oauth/connect-types.ts +17 -23
  177. package/src/oauth/connection-resolver.ts +35 -11
  178. package/src/oauth/connection.ts +1 -1
  179. package/src/oauth/manual-token-connection.ts +104 -0
  180. package/src/oauth/oauth-store.ts +496 -0
  181. package/src/oauth/platform-connection.test.ts +29 -0
  182. package/src/oauth/platform-connection.ts +6 -5
  183. package/src/oauth/provider-behaviors.ts +124 -0
  184. package/src/oauth/scope-policy.ts +9 -2
  185. package/src/oauth/seed-providers.ts +161 -0
  186. package/src/oauth/token-persistence.ts +74 -78
  187. package/src/permissions/checker.ts +3 -3
  188. package/src/permissions/defaults.ts +0 -1
  189. package/src/permissions/prompter.ts +10 -1
  190. package/src/permissions/trust-store.ts +13 -0
  191. package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
  192. package/src/prompts/system-prompt.ts +28 -40
  193. package/src/providers/anthropic/client.ts +133 -24
  194. package/src/providers/retry.ts +1 -27
  195. package/src/runtime/auth/route-policy.ts +0 -3
  196. package/src/runtime/channel-reply-delivery.ts +0 -40
  197. package/src/runtime/gateway-client.ts +0 -7
  198. package/src/runtime/http-server.ts +8 -6
  199. package/src/runtime/http-types.ts +2 -2
  200. package/src/runtime/middleware/twilio-validation.ts +1 -11
  201. package/src/runtime/pending-interactions.ts +14 -12
  202. package/src/runtime/routes/channel-delivery-routes.ts +0 -1
  203. package/src/runtime/routes/conversation-routes.ts +73 -19
  204. package/src/runtime/routes/events-routes.ts +21 -11
  205. package/src/runtime/routes/host-cu-routes.ts +97 -0
  206. package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
  207. package/src/runtime/routes/integrations/slack/share.ts +6 -7
  208. package/src/runtime/routes/log-export-routes.ts +126 -8
  209. package/src/runtime/routes/settings-routes.ts +55 -48
  210. package/src/runtime/routes/surface-action-routes.ts +1 -1
  211. package/src/runtime/routes/watch-routes.ts +128 -0
  212. package/src/schedule/integration-status.ts +10 -9
  213. package/src/security/credential-key.ts +0 -156
  214. package/src/security/keychain-broker-client.ts +5 -6
  215. package/src/security/oauth2.ts +1 -1
  216. package/src/security/token-manager.ts +119 -46
  217. package/src/skills/catalog-install.ts +358 -0
  218. package/src/skills/include-graph.ts +32 -0
  219. package/src/telegram/bot-username.ts +2 -3
  220. package/src/tools/browser/network-recorder.ts +1 -1
  221. package/src/tools/browser/network-recording-types.ts +1 -1
  222. package/src/tools/computer-use/definitions.ts +46 -11
  223. package/src/tools/computer-use/registry.ts +4 -5
  224. package/src/tools/credentials/broker.ts +1 -2
  225. package/src/tools/credentials/metadata-store.ts +17 -121
  226. package/src/tools/credentials/vault.ts +94 -167
  227. package/src/tools/registry.ts +2 -7
  228. package/src/tools/skills/load.ts +62 -3
  229. package/src/tools/watch/watch-state.ts +0 -12
  230. package/src/util/logger.ts +7 -41
  231. package/src/util/platform.ts +9 -28
  232. package/src/watcher/providers/google-calendar.ts +2 -1
  233. package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
  234. package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
  235. package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
  236. package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
  237. package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
  238. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
  239. package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
  240. package/src/cli/commands/dev.ts +0 -129
  241. package/src/cli/commands/map.ts +0 -391
  242. package/src/cli/commands/oauth.ts +0 -77
  243. package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +0 -16
  244. package/src/daemon/computer-use-session.ts +0 -1026
  245. package/src/daemon/ride-shotgun-handler.ts +0 -569
  246. package/src/oauth/provider-base-urls.ts +0 -21
  247. package/src/oauth/provider-profiles.ts +0 -192
  248. package/src/prompts/computer-use-prompt.ts +0 -98
  249. package/src/runtime/routes/computer-use-routes.ts +0 -641
  250. package/src/runtime/telegram-streaming-delivery.test.ts +0 -729
  251. package/src/runtime/telegram-streaming-delivery.ts +0 -393
  252. package/src/tools/computer-use/request-computer-control.ts +0 -56
@@ -501,8 +501,8 @@ describe("host_bash — environment setup", () => {
501
501
  });
502
502
 
503
503
  test("injects INTERNAL_GATEWAY_BASE_URL for host_bash commands", async () => {
504
- const originalGatewayBase = process.env.GATEWAY_INTERNAL_BASE_URL;
505
- process.env.GATEWAY_INTERNAL_BASE_URL = "http://gateway.internal:9000/";
504
+ const originalGatewayPort = process.env.GATEWAY_PORT;
505
+ process.env.GATEWAY_PORT = "9000";
506
506
  try {
507
507
  const result = await hostShellTool.execute(
508
508
  {
@@ -511,12 +511,12 @@ describe("host_bash — environment setup", () => {
511
511
  makeContext(),
512
512
  );
513
513
  expect(result.isError).toBe(false);
514
- expect(result.content.trim()).toBe("http://gateway.internal:9000");
514
+ expect(result.content.trim()).toBe("http://127.0.0.1:9000");
515
515
  } finally {
516
- if (originalGatewayBase === undefined) {
517
- delete process.env.GATEWAY_INTERNAL_BASE_URL;
516
+ if (originalGatewayPort === undefined) {
517
+ delete process.env.GATEWAY_PORT;
518
518
  } else {
519
- process.env.GATEWAY_INTERNAL_BASE_URL = originalGatewayBase;
519
+ process.env.GATEWAY_PORT = originalGatewayPort;
520
520
  }
521
521
  }
522
522
  });
@@ -695,7 +695,11 @@ describe("host_bash — spawn error handling", () => {
695
695
  describe("host_bash — proxy delegation", () => {
696
696
  function makeMockProxy(result: ToolExecutionResult) {
697
697
  const calls: Array<{
698
- input: { command: string; working_dir?: string; timeout_seconds?: number };
698
+ input: {
699
+ command: string;
700
+ working_dir?: string;
701
+ timeout_seconds?: number;
702
+ };
699
703
  sessionId: string;
700
704
  }> = [];
701
705
 
@@ -703,7 +707,11 @@ describe("host_bash — proxy delegation", () => {
703
707
  proxy: {
704
708
  isAvailable: () => true,
705
709
  request: async (
706
- input: { command: string; working_dir?: string; timeout_seconds?: number },
710
+ input: {
711
+ command: string;
712
+ working_dir?: string;
713
+ timeout_seconds?: number;
714
+ },
707
715
  sessionId: string,
708
716
  _signal?: AbortSignal,
709
717
  ) => {
@@ -748,7 +756,10 @@ describe("host_bash — proxy delegation", () => {
748
756
  });
749
757
 
750
758
  test("still validates input before proxying (null bytes in command)", async () => {
751
- const proxyResult: ToolExecutionResult = { content: "proxied", isError: false };
759
+ const proxyResult: ToolExecutionResult = {
760
+ content: "proxied",
761
+ isError: false,
762
+ };
752
763
  const { proxy, calls } = makeMockProxy(proxyResult);
753
764
 
754
765
  const ctx: ToolContext = {
@@ -756,10 +767,7 @@ describe("host_bash — proxy delegation", () => {
756
767
  hostBashProxy: proxy as unknown as ToolContext["hostBashProxy"],
757
768
  };
758
769
 
759
- const result = await hostShellTool.execute(
760
- { command: "echo \0evil" },
761
- ctx,
762
- );
770
+ const result = await hostShellTool.execute({ command: "echo \0evil" }, ctx);
763
771
 
764
772
  expect(result.isError).toBe(true);
765
773
  expect(result.content).toContain("null bytes");
@@ -768,7 +776,10 @@ describe("host_bash — proxy delegation", () => {
768
776
  });
769
777
 
770
778
  test("still validates input before proxying (relative working_dir)", async () => {
771
- const proxyResult: ToolExecutionResult = { content: "proxied", isError: false };
779
+ const proxyResult: ToolExecutionResult = {
780
+ content: "proxied",
781
+ isError: false,
782
+ };
772
783
  const { proxy, calls } = makeMockProxy(proxyResult);
773
784
 
774
785
  const ctx: ToolContext = {
@@ -803,7 +814,8 @@ describe("host_bash — proxy delegation", () => {
803
814
 
804
815
  const ctx: ToolContext = {
805
816
  ...makeContext(),
806
- hostBashProxy: unavailableProxy as unknown as ToolContext["hostBashProxy"],
817
+ hostBashProxy:
818
+ unavailableProxy as unknown as ToolContext["hostBashProxy"],
807
819
  };
808
820
 
809
821
  spawnCalls.length = 0;
@@ -169,6 +169,7 @@ function makeSession(overrides: Record<string, unknown> = {}) {
169
169
  updateClient: () => {},
170
170
  setHostBashProxy: () => {},
171
171
  setHostFileProxy: () => {},
172
+ setHostCuProxy: () => {},
172
173
  emitConfirmationStateChanged: () => {},
173
174
  emitActivityState: () => {},
174
175
  setTurnChannelContext: () => {},
@@ -26,6 +26,7 @@ mock.module("../util/logger.js", () => ({
26
26
  getLogger: () => makeLoggerStub(),
27
27
  }));
28
28
 
29
+ import { setIngressPublicBaseUrl } from "../config/env.js";
29
30
  import {
30
31
  getPublicBaseUrl,
31
32
  getTwilioStatusCallbackUrl,
@@ -78,19 +79,12 @@ function computeTwilioSignature(
78
79
  // ---------------------------------------------------------------------------
79
80
 
80
81
  describe("Ingress URL consistency between assistant and gateway", () => {
81
- let savedIngressEnv: string | undefined;
82
-
83
82
  beforeEach(() => {
84
- savedIngressEnv = process.env.INGRESS_PUBLIC_BASE_URL;
85
- delete process.env.INGRESS_PUBLIC_BASE_URL;
83
+ setIngressPublicBaseUrl(undefined);
86
84
  });
87
85
 
88
86
  afterEach(() => {
89
- if (savedIngressEnv !== undefined) {
90
- process.env.INGRESS_PUBLIC_BASE_URL = savedIngressEnv;
91
- } else {
92
- delete process.env.INGRESS_PUBLIC_BASE_URL;
93
- }
87
+ setIngressPublicBaseUrl(undefined);
94
88
  });
95
89
 
96
90
  test("assistant callback URL and gateway signature reconstruction use same base when config is set", () => {
@@ -104,9 +98,8 @@ describe("Ingress URL consistency between assistant and gateway", () => {
104
98
  "session-abc",
105
99
  );
106
100
 
107
- // Simulate: when hatch.ts spawns the gateway, it reads config.ingress.publicBaseUrl
108
- // and passes it as INGRESS_PUBLIC_BASE_URL. The gateway stores this as
109
- // config.ingressPublicBaseUrl.
101
+ // The gateway reads config.ingress.publicBaseUrl from the workspace config
102
+ // file via ConfigFileCache.
110
103
  const gatewayIngressPublicBaseUrl = getPublicBaseUrl(config);
111
104
 
112
105
  // When Twilio calls the gateway, the gateway reconstructs the canonical URL
@@ -147,7 +140,7 @@ describe("Ingress URL consistency between assistant and gateway", () => {
147
140
  const localRequestUrl = "http://127.0.0.1:7830/webhooks/twilio/status";
148
141
 
149
142
  // Gateway reconstructs the canonical URL using its configured base
150
- // (which was passed from the assistant's config via INGRESS_PUBLIC_BASE_URL)
143
+ // (which was read from the workspace config file via ConfigFileCache)
151
144
  const gatewayIngressPublicBaseUrl = getPublicBaseUrl(config);
152
145
  const canonicalUrl = reconstructGatewayCanonicalUrl(
153
146
  gatewayIngressPublicBaseUrl,
@@ -208,20 +201,20 @@ describe("Ingress URL consistency between assistant and gateway", () => {
208
201
  expect(recomputedWith).toBe(twilioSignature);
209
202
  });
210
203
 
211
- test("env var fallback produces consistent URLs across assistant and gateway", () => {
212
- // When no config.ingress.publicBaseUrl is set, both assistant and gateway
213
- // fall back to the INGRESS_PUBLIC_BASE_URL env var.
214
- process.env.INGRESS_PUBLIC_BASE_URL = "https://env-tunnel.example.com";
204
+ test("module-level state fallback produces consistent URLs across assistant and gateway", () => {
205
+ // When no config.ingress.publicBaseUrl is set, the assistant falls back
206
+ // to the module-level ingress state.
207
+ setIngressPublicBaseUrl("https://env-tunnel.example.com");
215
208
 
216
209
  const config: IngressConfig = {};
217
210
 
218
- // Assistant resolves the base URL from env
211
+ // Assistant resolves the base URL from module state
219
212
  const assistantBase = getPublicBaseUrl(config);
220
213
  expect(assistantBase).toBe("https://env-tunnel.example.com");
221
214
 
222
- // Gateway would also read the same env var (process.env.INGRESS_PUBLIC_BASE_URL)
223
- // and store it as config.ingressPublicBaseUrl.
224
- const gatewayIngressPublicBaseUrl = process.env.INGRESS_PUBLIC_BASE_URL;
215
+ // Gateway would read the same value from the workspace config file
216
+ // via ConfigFileCache.getString("ingress", "publicBaseUrl").
217
+ const gatewayIngressPublicBaseUrl = "https://env-tunnel.example.com";
225
218
 
226
219
  // Callback URL generated by assistant
227
220
  const callbackUrl = getTwilioVoiceWebhookUrl(config, "session-xyz");
@@ -5,6 +5,9 @@ import { credentialKey } from "../security/credential-key.js";
5
5
  const secureKeyValues = new Map<string, string>();
6
6
  let mockTwilioAccountSid: string | undefined;
7
7
 
8
+ /** Set of providers that should report as connected via isProviderConnected(). */
9
+ const connectedProviders = new Set<string>();
10
+
8
11
  mock.module("../security/secure-keys.js", () => ({
9
12
  getSecureKey: (account: string) => secureKeyValues.get(account),
10
13
  }));
@@ -17,12 +20,27 @@ mock.module("../config/loader.js", () => ({
17
20
  }),
18
21
  }));
19
22
 
23
+ mock.module("../oauth/oauth-store.js", () => ({
24
+ isProviderConnected: (providerKey: string) =>
25
+ connectedProviders.has(providerKey),
26
+ getConnectionByProvider: (providerKey: string) =>
27
+ connectedProviders.has(providerKey)
28
+ ? { id: `conn-${providerKey}`, status: "active" }
29
+ : undefined,
30
+ }));
31
+
32
+ /** Mark a provider as fully connected (active row + access token). */
33
+ function setOAuthConnected(providerKey: string): void {
34
+ connectedProviders.add(providerKey);
35
+ }
36
+
20
37
  const { getIntegrationSummary, formatIntegrationSummary, hasCapability } =
21
38
  await import("../schedule/integration-status.js");
22
39
 
23
40
  describe("integration-status", () => {
24
41
  beforeEach(() => {
25
42
  secureKeyValues.clear();
43
+ connectedProviders.clear();
26
44
  mockTwilioAccountSid = undefined;
27
45
  });
28
46
 
@@ -38,21 +56,11 @@ describe("integration-status", () => {
38
56
  });
39
57
 
40
58
  test("returns all connected when all keys are set", () => {
41
- secureKeyValues.set(
42
- credentialKey("integration:gmail", "access_token"),
43
- "tok",
44
- );
45
- secureKeyValues.set(
46
- credentialKey("integration:slack", "access_token"),
47
- "tok",
48
- );
59
+ setOAuthConnected("integration:gmail");
60
+ setOAuthConnected("integration:slack");
49
61
  mockTwilioAccountSid = "sid";
50
62
  secureKeyValues.set(credentialKey("twilio", "auth_token"), "auth");
51
- secureKeyValues.set(credentialKey("telegram", "bot_token"), "tok");
52
- secureKeyValues.set(
53
- credentialKey("telegram", "webhook_secret"),
54
- "secret",
55
- );
63
+ setOAuthConnected("telegram");
56
64
 
57
65
  const summary = getIntegrationSummary();
58
66
  expect(summary.every((s: { connected: boolean }) => s.connected)).toBe(
@@ -63,11 +71,7 @@ describe("integration-status", () => {
63
71
  test("returns mixed status", () => {
64
72
  mockTwilioAccountSid = "sid";
65
73
  secureKeyValues.set(credentialKey("twilio", "auth_token"), "auth");
66
- secureKeyValues.set(credentialKey("telegram", "bot_token"), "tok");
67
- secureKeyValues.set(
68
- credentialKey("telegram", "webhook_secret"),
69
- "secret",
70
- );
74
+ setOAuthConnected("telegram");
71
75
 
72
76
  const summary = getIntegrationSummary();
73
77
  const connected = summary.filter(
@@ -95,9 +99,8 @@ describe("integration-status", () => {
95
99
  expect(twilio?.connected).toBe(false);
96
100
  });
97
101
 
98
- test("Telegram disconnected when only bot_token is set (missing webhook_secret)", () => {
99
- secureKeyValues.set(credentialKey("telegram", "bot_token"), "tok");
100
-
102
+ test("Telegram disconnected when no connection record exists", () => {
103
+ // No oauth_connection record for telegram should be disconnected
101
104
  const summary = getIntegrationSummary();
102
105
  const telegram = summary.find(
103
106
  (s: { name: string }) => s.name === "Telegram",
@@ -110,11 +113,7 @@ describe("integration-status", () => {
110
113
  test("shows checkmarks and crosses", () => {
111
114
  mockTwilioAccountSid = "sid";
112
115
  secureKeyValues.set(credentialKey("twilio", "auth_token"), "auth");
113
- secureKeyValues.set(credentialKey("telegram", "bot_token"), "tok");
114
- secureKeyValues.set(
115
- credentialKey("telegram", "webhook_secret"),
116
- "secret",
117
- );
116
+ setOAuthConnected("telegram");
118
117
 
119
118
  const result = formatIntegrationSummary();
120
119
  expect(result).toBe(
@@ -130,21 +129,11 @@ describe("integration-status", () => {
130
129
  });
131
130
 
132
131
  test("all connected", () => {
133
- secureKeyValues.set(
134
- credentialKey("integration:gmail", "access_token"),
135
- "tok",
136
- );
137
- secureKeyValues.set(
138
- credentialKey("integration:slack", "access_token"),
139
- "tok",
140
- );
132
+ setOAuthConnected("integration:gmail");
133
+ setOAuthConnected("integration:slack");
141
134
  mockTwilioAccountSid = "sid";
142
135
  secureKeyValues.set(credentialKey("twilio", "auth_token"), "auth");
143
- secureKeyValues.set(credentialKey("telegram", "bot_token"), "tok");
144
- secureKeyValues.set(
145
- credentialKey("telegram", "webhook_secret"),
146
- "secret",
147
- );
136
+ setOAuthConnected("telegram");
148
137
 
149
138
  const result = formatIntegrationSummary();
150
139
  expect(result).toBe(
@@ -160,17 +149,12 @@ describe("integration-status", () => {
160
149
  });
161
150
 
162
151
  test("returns true when any integration in category is connected", () => {
163
- secureKeyValues.set(credentialKey("telegram", "bot_token"), "tok");
164
- secureKeyValues.set(
165
- credentialKey("telegram", "webhook_secret"),
166
- "secret",
167
- );
152
+ setOAuthConnected("telegram");
168
153
  expect(hasCapability("messaging")).toBe(true);
169
154
  });
170
155
 
171
- test("returns false when only partial credentials exist for category integrations", () => {
172
- secureKeyValues.set(credentialKey("telegram", "bot_token"), "tok");
173
- // Missing webhook_secret — Telegram should not count as connected
156
+ test("returns false when no connection record exists for category integrations", () => {
157
+ // No oauth_connection record for telegram should not count as connected
174
158
  expect(hasCapability("messaging")).toBe(false);
175
159
  });
176
160
 
@@ -179,10 +163,7 @@ describe("integration-status", () => {
179
163
  });
180
164
 
181
165
  test("email category checks Gmail", () => {
182
- secureKeyValues.set(
183
- credentialKey("integration:gmail", "access_token"),
184
- "tok",
185
- );
166
+ setOAuthConnected("integration:gmail");
186
167
  expect(hasCapability("email")).toBe(true);
187
168
  });
188
169
  });
@@ -45,7 +45,6 @@ mock.module("../util/logger.js", () => ({
45
45
  ...realLogger,
46
46
  getLogger: () => noopLogger,
47
47
  getCliLogger: () => noopLogger,
48
- isDebug: () => false,
49
48
  truncateForLog: (v: string) => v,
50
49
  initLogger: () => {},
51
50
  pruneOldLogFiles: () => 0,
@@ -24,8 +24,7 @@ mock.module("../util/logger.js", () => ({
24
24
  }));
25
25
 
26
26
  // Prevent ensureTelegramBotUsernameResolved() from reading real credentials
27
- // and calling the Telegram API, which would populate credential metadata
28
- // with the real bot username and shadow the env var override in tests.
27
+ // and calling the Telegram API.
29
28
  mock.module("../security/secure-keys.js", () => ({
30
29
  getSecureKey: () => undefined,
31
30
  setSecureKey: () => {},
@@ -35,6 +34,13 @@ mock.module("../security/secure-keys.js", () => ({
35
34
  deleteSecureKeyAsync: async () => {},
36
35
  }));
37
36
 
37
+ // Mock getTelegramBotUsername — the env var fallback was removed so we
38
+ // control the return value directly via a mutable variable.
39
+ let mockTelegramBotUsername: string | undefined;
40
+ mock.module("../telegram/bot-username.js", () => ({
41
+ getTelegramBotUsername: () => mockTelegramBotUsername,
42
+ }));
43
+
38
44
  import { getSqlite, initializeDb, resetDb } from "../memory/db.js";
39
45
  import {
40
46
  handleCreateInvite,
@@ -94,8 +100,7 @@ describe("ingress invite HTTP routes", () => {
94
100
  });
95
101
 
96
102
  test("POST /v1/contacts/invites — includes canonical share URL when bot username is configured", async () => {
97
- const prevBotUsername = process.env.TELEGRAM_BOT_USERNAME;
98
- process.env.TELEGRAM_BOT_USERNAME = "test_invite_bot";
103
+ mockTelegramBotUsername = "test_invite_bot";
99
104
 
100
105
  try {
101
106
  const req = new Request("http://localhost/v1/contacts/invites", {
@@ -121,11 +126,7 @@ describe("ingress invite HTTP routes", () => {
121
126
  expect(share.url).toBe(`https://t.me/test_invite_bot?start=iv_${token}`);
122
127
  expect(typeof share.displayText).toBe("string");
123
128
  } finally {
124
- if (prevBotUsername === undefined) {
125
- delete process.env.TELEGRAM_BOT_USERNAME;
126
- } else {
127
- process.env.TELEGRAM_BOT_USERNAME = prevBotUsername;
128
- }
129
+ mockTelegramBotUsername = undefined;
129
130
  }
130
131
  });
131
132
 
@@ -36,7 +36,7 @@ const TEST_DIR = join(
36
36
  );
37
37
  const TOKEN_DIR = join(TEST_DIR, ".vellum", "protected");
38
38
  const TOKEN_PATH = join(TOKEN_DIR, "keychain-broker.token");
39
- const SOCKET_PATH = join(TEST_DIR, "broker.sock");
39
+ const SOCKET_PATH = join(TEST_DIR, ".vellum", "keychain-broker.sock");
40
40
  const TEST_TOKEN = "test-auth-token-abc123";
41
41
 
42
42
  // ---------------------------------------------------------------------------
@@ -106,14 +106,11 @@ function createMockBroker(): {
106
106
  // Setup / teardown
107
107
  // ---------------------------------------------------------------------------
108
108
 
109
- let originalEnv: string | undefined;
110
-
111
109
  beforeAll(() => {
112
110
  mkdirSync(TOKEN_DIR, { recursive: true });
113
111
  });
114
112
 
115
113
  beforeEach(() => {
116
- originalEnv = process.env.VELLUM_KEYCHAIN_BROKER_SOCKET;
117
114
  // Clean up socket file from prior test
118
115
  try {
119
116
  rmSync(SOCKET_PATH, { force: true });
@@ -122,14 +119,6 @@ beforeEach(() => {
122
119
  }
123
120
  });
124
121
 
125
- afterEach(() => {
126
- if (originalEnv === undefined) {
127
- delete process.env.VELLUM_KEYCHAIN_BROKER_SOCKET;
128
- } else {
129
- process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = originalEnv;
130
- }
131
- });
132
-
133
122
  afterAll(() => {
134
123
  rmSync(TEST_DIR, { recursive: true, force: true });
135
124
  });
@@ -157,15 +146,15 @@ describe("keychain-broker-client", () => {
157
146
  // isAvailable()
158
147
  // -----------------------------------------------------------------------
159
148
  describe("isAvailable", () => {
160
- test("returns false when env var is unset", () => {
161
- delete process.env.VELLUM_KEYCHAIN_BROKER_SOCKET;
149
+ test("returns false when socket file does not exist", () => {
162
150
  writeFileSync(TOKEN_PATH, TEST_TOKEN);
163
151
  const client = createBrokerClient();
164
152
  expect(client.isAvailable()).toBe(false);
165
153
  });
166
154
 
167
155
  test("returns false when token file does not exist", () => {
168
- process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
156
+ // Create the socket file so that check passes
157
+ writeFileSync(SOCKET_PATH, "");
169
158
  try {
170
159
  rmSync(TOKEN_PATH, { force: true });
171
160
  } catch {
@@ -175,8 +164,8 @@ describe("keychain-broker-client", () => {
175
164
  expect(client.isAvailable()).toBe(false);
176
165
  });
177
166
 
178
- test("returns true when both env var and token file exist", () => {
179
- process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
167
+ test("returns true when both socket file and token file exist", () => {
168
+ writeFileSync(SOCKET_PATH, "");
180
169
  writeFileSync(TOKEN_PATH, TEST_TOKEN);
181
170
  const client = createBrokerClient();
182
171
  expect(client.isAvailable()).toBe(true);
@@ -190,7 +179,6 @@ describe("keychain-broker-client", () => {
190
179
  let broker: ReturnType<typeof createMockBroker>;
191
180
 
192
181
  beforeEach(async () => {
193
- process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
194
182
  writeFileSync(TOKEN_PATH, TEST_TOKEN);
195
183
  broker = createMockBroker();
196
184
  });
@@ -343,7 +331,6 @@ describe("keychain-broker-client", () => {
343
331
  let broker: ReturnType<typeof createMockBroker>;
344
332
 
345
333
  beforeEach(async () => {
346
- process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
347
334
  writeFileSync(TOKEN_PATH, TEST_TOKEN);
348
335
  broker = createMockBroker();
349
336
  });
@@ -381,7 +368,6 @@ describe("keychain-broker-client", () => {
381
368
  let broker: ReturnType<typeof createMockBroker>;
382
369
 
383
370
  beforeEach(async () => {
384
- process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
385
371
  writeFileSync(TOKEN_PATH, "old-token");
386
372
  broker = createMockBroker();
387
373
  });
@@ -441,59 +427,42 @@ describe("keychain-broker-client", () => {
441
427
  // Graceful degradation
442
428
  // -----------------------------------------------------------------------
443
429
  describe("graceful degradation", () => {
444
- test("get returns null when broker is not running", async () => {
445
- process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
430
+ test("get returns null when socket file does not exist", async () => {
446
431
  writeFileSync(TOKEN_PATH, TEST_TOKEN);
447
432
  const client = createBrokerClient();
448
433
  const result = await client.get("test-key");
449
434
  expect(result).toBeNull();
450
435
  });
451
436
 
452
- test("set returns false when broker is not running", async () => {
453
- process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
437
+ test("set returns false when socket file does not exist", async () => {
454
438
  writeFileSync(TOKEN_PATH, TEST_TOKEN);
455
439
  const client = createBrokerClient();
456
440
  const result = await client.set("test-key", "value");
457
441
  expect(result).toBe(false);
458
442
  });
459
443
 
460
- test("del returns false when broker is not running", async () => {
461
- process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
444
+ test("del returns false when socket file does not exist", async () => {
462
445
  writeFileSync(TOKEN_PATH, TEST_TOKEN);
463
446
  const client = createBrokerClient();
464
447
  const result = await client.del("test-key");
465
448
  expect(result).toBe(false);
466
449
  });
467
450
 
468
- test("list returns empty array when broker is not running", async () => {
469
- process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
451
+ test("list returns empty array when socket file does not exist", async () => {
470
452
  writeFileSync(TOKEN_PATH, TEST_TOKEN);
471
453
  const client = createBrokerClient();
472
454
  const result = await client.list();
473
455
  expect(result).toEqual([]);
474
456
  });
475
457
 
476
- test("ping returns null when broker is not running", async () => {
477
- process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
458
+ test("ping returns null when socket file does not exist", async () => {
478
459
  writeFileSync(TOKEN_PATH, TEST_TOKEN);
479
460
  const client = createBrokerClient();
480
461
  const result = await client.ping();
481
462
  expect(result).toBeNull();
482
463
  });
483
464
 
484
- test("returns fallbacks when socket path env var is unset", async () => {
485
- delete process.env.VELLUM_KEYCHAIN_BROKER_SOCKET;
486
- writeFileSync(TOKEN_PATH, TEST_TOKEN);
487
- const client = createBrokerClient();
488
- expect(await client.get("key")).toBeNull();
489
- expect(await client.set("key", "val")).toBe(false);
490
- expect(await client.del("key")).toBe(false);
491
- expect(await client.list()).toEqual([]);
492
- expect(await client.ping()).toBeNull();
493
- });
494
-
495
465
  test("returns fallbacks when token file is missing", async () => {
496
- process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
497
466
  try {
498
467
  rmSync(TOKEN_PATH, { force: true });
499
468
  } catch {
@@ -515,7 +484,6 @@ describe("keychain-broker-client", () => {
515
484
  let broker: ReturnType<typeof createMockBroker>;
516
485
 
517
486
  beforeEach(async () => {
518
- process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
519
487
  writeFileSync(TOKEN_PATH, TEST_TOKEN);
520
488
  broker = createMockBroker();
521
489
  });
@@ -13,7 +13,6 @@ mock.module("../util/logger.js", () => ({
13
13
  new Proxy({} as Record<string, unknown>, {
14
14
  get: () => () => {},
15
15
  }),
16
- isDebug: () => false,
17
16
  truncateForLog: (v: string) => v,
18
17
  }));
19
18