@vellumai/assistant 0.4.52 → 0.4.53

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 (205) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/docs/architecture/keychain-broker.md +6 -20
  3. package/docs/architecture/memory.md +3 -3
  4. package/package.json +1 -1
  5. package/src/__tests__/approval-cascade.test.ts +3 -1
  6. package/src/__tests__/approval-routes-http.test.ts +0 -1
  7. package/src/__tests__/asset-materialize-tool.test.ts +0 -1
  8. package/src/__tests__/asset-search-tool.test.ts +0 -1
  9. package/src/__tests__/assistant-events-sse-hardening.test.ts +0 -1
  10. package/src/__tests__/attachments-store.test.ts +0 -1
  11. package/src/__tests__/avatar-e2e.test.ts +6 -1
  12. package/src/__tests__/browser-fill-credential.test.ts +3 -0
  13. package/src/__tests__/btw-routes.test.ts +39 -0
  14. package/src/__tests__/call-controller.test.ts +0 -1
  15. package/src/__tests__/call-domain.test.ts +1 -0
  16. package/src/__tests__/call-routes-http.test.ts +1 -2
  17. package/src/__tests__/canonical-guardian-store.test.ts +33 -2
  18. package/src/__tests__/channel-readiness-service.test.ts +1 -0
  19. package/src/__tests__/claude-code-skill-regression.test.ts +6 -2
  20. package/src/__tests__/claude-code-tool-profiles.test.ts +7 -2
  21. package/src/__tests__/config-loader-backfill.test.ts +1 -2
  22. package/src/__tests__/config-schema.test.ts +6 -37
  23. package/src/__tests__/conversation-routes-slash-commands.test.ts +0 -1
  24. package/src/__tests__/credential-broker-server-use.test.ts +16 -16
  25. package/src/__tests__/credential-security-invariants.test.ts +14 -0
  26. package/src/__tests__/credential-vault-unit.test.ts +4 -4
  27. package/src/__tests__/error-handler-friendly-messages.test.ts +4 -5
  28. package/src/__tests__/gateway-only-enforcement.test.ts +0 -2
  29. package/src/__tests__/host-shell-tool.test.ts +0 -1
  30. package/src/__tests__/http-user-message-parity.test.ts +19 -0
  31. package/src/__tests__/list-messages-attachments.test.ts +0 -1
  32. package/src/__tests__/log-export-workspace.test.ts +233 -0
  33. package/src/__tests__/managed-proxy-context.test.ts +1 -1
  34. package/src/__tests__/managed-skill-lifecycle.test.ts +0 -1
  35. package/src/__tests__/media-generate-image.test.ts +7 -2
  36. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -1
  37. package/src/__tests__/memory-regressions.test.ts +0 -1
  38. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1
  39. package/src/__tests__/migration-export-http.test.ts +0 -1
  40. package/src/__tests__/migration-import-commit-http.test.ts +0 -1
  41. package/src/__tests__/migration-import-preflight-http.test.ts +0 -1
  42. package/src/__tests__/migration-validate-http.test.ts +0 -1
  43. package/src/__tests__/notification-schedule-dedup.test.ts +237 -0
  44. package/src/__tests__/oauth-cli.test.ts +1 -10
  45. package/src/__tests__/oauth-store.test.ts +3 -5
  46. package/src/__tests__/oauth2-gateway-transport.test.ts +5 -4
  47. package/src/__tests__/onboarding-starter-tasks.test.ts +1 -1
  48. package/src/__tests__/onboarding-template-contract.test.ts +1 -2
  49. package/src/__tests__/pricing.test.ts +0 -11
  50. package/src/__tests__/provider-commit-message-generator.test.ts +21 -14
  51. package/src/__tests__/provider-fail-open-selection.test.ts +9 -8
  52. package/src/__tests__/provider-managed-proxy-integration.test.ts +27 -24
  53. package/src/__tests__/provider-registry-ollama.test.ts +8 -2
  54. package/src/__tests__/recording-handler.test.ts +0 -1
  55. package/src/__tests__/relay-server.test.ts +0 -1
  56. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  57. package/src/__tests__/runtime-events-sse-parity.test.ts +0 -1
  58. package/src/__tests__/runtime-events-sse.test.ts +0 -1
  59. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -1
  60. package/src/__tests__/secret-scanner-executor.test.ts +0 -1
  61. package/src/__tests__/send-endpoint-busy.test.ts +0 -1
  62. package/src/__tests__/session-abort-tool-results.test.ts +3 -1
  63. package/src/__tests__/session-agent-loop-overflow.test.ts +1012 -838
  64. package/src/__tests__/session-agent-loop.test.ts +2 -2
  65. package/src/__tests__/session-confirmation-signals.test.ts +3 -1
  66. package/src/__tests__/session-error.test.ts +5 -4
  67. package/src/__tests__/session-history-web-search.test.ts +34 -9
  68. package/src/__tests__/session-pre-run-repair.test.ts +3 -1
  69. package/src/__tests__/session-provider-retry-repair.test.ts +31 -26
  70. package/src/__tests__/session-queue.test.ts +3 -1
  71. package/src/__tests__/session-runtime-assembly.test.ts +118 -0
  72. package/src/__tests__/session-slash-known.test.ts +31 -13
  73. package/src/__tests__/session-slash-queue.test.ts +3 -1
  74. package/src/__tests__/session-slash-unknown.test.ts +3 -1
  75. package/src/__tests__/session-workspace-cache-state.test.ts +3 -1
  76. package/src/__tests__/session-workspace-injection.test.ts +3 -1
  77. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -1
  78. package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -1
  79. package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -1
  80. package/src/__tests__/skillssh-registry.test.ts +21 -0
  81. package/src/__tests__/slack-share-routes.test.ts +1 -1
  82. package/src/__tests__/swarm-recursion.test.ts +5 -1
  83. package/src/__tests__/swarm-session-integration.test.ts +25 -14
  84. package/src/__tests__/swarm-tool.test.ts +5 -2
  85. package/src/__tests__/telegram-bot-username-resolution.test.ts +2 -4
  86. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1521 -0
  87. package/src/__tests__/tool-execution-abort-cleanup.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__/trust-store.test.ts +5 -1
  92. package/src/__tests__/twilio-routes.test.ts +2 -2
  93. package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
  94. package/src/__tests__/voice-quality.test.ts +2 -1
  95. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  96. package/src/__tests__/web-search.test.ts +1 -1
  97. package/src/agent/loop.ts +17 -1
  98. package/src/bundler/app-bundler.ts +40 -24
  99. package/src/calls/call-controller.ts +16 -0
  100. package/src/calls/relay-server.ts +29 -13
  101. package/src/calls/voice-control-protocol.ts +1 -0
  102. package/src/calls/voice-quality.ts +1 -1
  103. package/src/calls/voice-session-bridge.ts +9 -3
  104. package/src/channels/types.ts +16 -0
  105. package/src/cli/commands/bash.ts +173 -0
  106. package/src/cli/commands/doctor.ts +5 -23
  107. package/src/cli/commands/oauth/connections.ts +4 -2
  108. package/src/cli/commands/oauth/providers.ts +1 -13
  109. package/src/cli/program.ts +2 -0
  110. package/src/cli/reference.ts +1 -0
  111. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -1
  112. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +3 -5
  113. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -3
  114. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +1 -1
  115. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -6
  116. package/src/config/feature-flag-registry.json +8 -0
  117. package/src/config/loader.ts +7 -135
  118. package/src/config/schema.ts +0 -6
  119. package/src/config/schemas/channels.ts +1 -0
  120. package/src/config/schemas/elevenlabs.ts +2 -2
  121. package/src/contacts/contact-store.ts +21 -25
  122. package/src/contacts/contacts-write.ts +6 -6
  123. package/src/contacts/types.ts +2 -0
  124. package/src/context/token-estimator.ts +35 -2
  125. package/src/context/window-manager.ts +16 -2
  126. package/src/daemon/config-watcher.ts +24 -6
  127. package/src/daemon/context-overflow-reducer.ts +13 -2
  128. package/src/daemon/handlers/config-ingress.ts +25 -8
  129. package/src/daemon/handlers/config-model.ts +21 -15
  130. package/src/daemon/handlers/config-telegram.ts +18 -6
  131. package/src/daemon/handlers/dictation.ts +0 -429
  132. package/src/daemon/handlers/skills.ts +1 -200
  133. package/src/daemon/lifecycle.ts +8 -5
  134. package/src/daemon/message-types/contacts.ts +2 -0
  135. package/src/daemon/message-types/integrations.ts +1 -0
  136. package/src/daemon/message-types/sessions.ts +2 -0
  137. package/src/daemon/parse-actual-tokens-from-error.test.ts +75 -0
  138. package/src/daemon/server.ts +23 -2
  139. package/src/daemon/session-agent-loop-handlers.ts +1 -1
  140. package/src/daemon/session-agent-loop.ts +27 -79
  141. package/src/daemon/session-error.ts +5 -4
  142. package/src/daemon/session-process.ts +17 -10
  143. package/src/daemon/session-runtime-assembly.ts +50 -0
  144. package/src/daemon/session-slash.ts +32 -20
  145. package/src/daemon/session.ts +1 -0
  146. package/src/events/domain-events.ts +1 -0
  147. package/src/media/app-icon-generator.ts +2 -1
  148. package/src/media/avatar-router.ts +3 -2
  149. package/src/memory/canonical-guardian-store.ts +25 -3
  150. package/src/memory/db-init.ts +12 -0
  151. package/src/memory/embedding-backend.ts +25 -16
  152. package/src/memory/migrations/158-channel-interaction-columns.ts +18 -0
  153. package/src/memory/migrations/159-drop-contact-interaction-columns.ts +16 -0
  154. package/src/memory/migrations/160-drop-loopback-port-column.ts +13 -0
  155. package/src/memory/migrations/index.ts +3 -0
  156. package/src/memory/retriever.test.ts +19 -12
  157. package/src/memory/schema/contacts.ts +2 -2
  158. package/src/memory/schema/oauth.ts +0 -1
  159. package/src/oauth/connect-orchestrator.ts +5 -3
  160. package/src/oauth/connect-types.ts +9 -2
  161. package/src/oauth/manual-token-connection.ts +9 -7
  162. package/src/oauth/oauth-store.ts +2 -8
  163. package/src/oauth/provider-behaviors.ts +10 -0
  164. package/src/oauth/seed-providers.ts +13 -5
  165. package/src/permissions/checker.ts +20 -1
  166. package/src/prompts/__tests__/build-cli-reference-section.test.ts +1 -1
  167. package/src/prompts/system-prompt.ts +2 -11
  168. package/src/prompts/templates/BOOTSTRAP.md +1 -3
  169. package/src/providers/anthropic/client.ts +16 -8
  170. package/src/providers/managed-proxy/constants.ts +1 -1
  171. package/src/providers/registry.ts +21 -15
  172. package/src/providers/types.ts +1 -1
  173. package/src/runtime/auth/route-policy.ts +4 -0
  174. package/src/runtime/channel-invite-transports/telegram.ts +12 -6
  175. package/src/runtime/channel-retry-sweep.ts +6 -0
  176. package/src/runtime/http-types.ts +1 -0
  177. package/src/runtime/middleware/error-handler.ts +1 -2
  178. package/src/runtime/routes/app-management-routes.ts +1 -0
  179. package/src/runtime/routes/btw-routes.ts +20 -1
  180. package/src/runtime/routes/conversation-routes.ts +32 -13
  181. package/src/runtime/routes/inbound-message-handler.ts +10 -2
  182. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
  183. package/src/runtime/routes/inbound-stages/edit-intercept.ts +5 -5
  184. package/src/runtime/routes/integrations/slack/share.ts +5 -5
  185. package/src/runtime/routes/log-export-routes.ts +122 -10
  186. package/src/runtime/routes/session-query-routes.ts +3 -3
  187. package/src/runtime/routes/settings-routes.ts +53 -0
  188. package/src/runtime/routes/workspace-routes.ts +3 -0
  189. package/src/runtime/verification-templates.ts +1 -1
  190. package/src/security/oauth2.ts +4 -4
  191. package/src/security/secure-keys.ts +4 -4
  192. package/src/signals/bash.ts +157 -0
  193. package/src/skills/skillssh-registry.ts +6 -1
  194. package/src/swarm/backend-claude-code.ts +6 -6
  195. package/src/swarm/worker-backend.ts +1 -1
  196. package/src/swarm/worker-runner.ts +1 -1
  197. package/src/telegram/bot-username.ts +11 -0
  198. package/src/tools/claude-code/claude-code.ts +4 -4
  199. package/src/tools/credentials/broker.ts +7 -5
  200. package/src/tools/credentials/vault.ts +3 -2
  201. package/src/tools/network/__tests__/web-search.test.ts +18 -86
  202. package/src/tools/network/web-search.ts +9 -15
  203. package/src/util/platform.ts +7 -1
  204. package/src/util/pricing.ts +0 -1
  205. package/src/workspace/provider-commit-message-generator.ts +10 -6
@@ -11,6 +11,7 @@ import {
11
11
  import {
12
12
  CHANNEL_IDS,
13
13
  INTERFACE_IDS,
14
+ isInteractiveInterface,
14
15
  parseChannelId,
15
16
  parseInterfaceId,
16
17
  } from "../../channels/types.js";
@@ -656,14 +657,7 @@ export async function handleSendMessage(
656
657
  }
657
658
 
658
659
  const onEvent = makeHubPublisher(smDeps, mapping.conversationId, session);
659
- // Desktop, CLI, and web interfaces have an SSE client that can display
660
- // permission prompts. Channel interfaces (telegram, slack, etc.) route
661
- // approvals through the guardian system and have no interactive prompter UI.
662
- const isInteractiveInterface =
663
- sourceInterface === "macos" ||
664
- sourceInterface === "ios" ||
665
- sourceInterface === "cli" ||
666
- sourceInterface === "vellum";
660
+ const isInteractive = isInteractiveInterface(sourceInterface);
667
661
  // Only create the host bash proxy for desktop client interfaces that can
668
662
  // execute commands on the user's machine. Non-desktop sessions (CLI,
669
663
  // channels, headless) fall back to local execution.
@@ -709,7 +703,7 @@ export async function handleSendMessage(
709
703
  session.isProcessing() &&
710
704
  sourceInterface !== "macos" &&
711
705
  sourceInterface !== "ios";
712
- session.updateClient(onEvent, !isInteractiveInterface, {
706
+ session.updateClient(onEvent, !isInteractive, {
713
707
  skipProxySenderUpdate: preservingProxies,
714
708
  });
715
709
 
@@ -781,7 +775,7 @@ export async function handleSendMessage(
781
775
  userMessageInterface: sourceInterface,
782
776
  assistantMessageInterface: sourceInterface,
783
777
  },
784
- { isInteractive: isInteractiveInterface },
778
+ { isInteractive },
785
779
  );
786
780
  if (enqueueResult.rejected) {
787
781
  return Response.json(
@@ -821,6 +815,31 @@ export async function handleSendMessage(
821
815
  );
822
816
  }
823
817
 
818
+ // Auto-deny pending confirmations for idle sessions. The legacy
819
+ // handleUserMessage called autoDenyPendingConfirmations unconditionally
820
+ // before dispatching, so an idle session with lingering confirmations
821
+ // (e.g. the user never responded to a tool-approval prompt) must deny
822
+ // them before starting the new turn.
823
+ if (session.hasAnyPendingConfirmation()) {
824
+ for (const interaction of pendingInteractions.getByConversation(
825
+ mapping.conversationId,
826
+ )) {
827
+ if (
828
+ interaction.session === session &&
829
+ interaction.kind === "confirmation"
830
+ ) {
831
+ session.emitConfirmationStateChanged({
832
+ sessionId: mapping.conversationId,
833
+ requestId: interaction.requestId,
834
+ state: "denied" as const,
835
+ source: "auto_deny" as const,
836
+ });
837
+ }
838
+ }
839
+ session.denyAllPendingConfirmations();
840
+ pendingInteractions.removeBySession(session);
841
+ }
842
+
824
843
  // Session is idle — persist and fire agent loop immediately
825
844
  session.setTurnChannelContext({
826
845
  userMessageChannel: sourceChannel,
@@ -845,7 +864,7 @@ export async function handleSendMessage(
845
864
  provider: config.provider,
846
865
  estimatedCost: session.usageStats.estimatedCost,
847
866
  };
848
- const slashResult = resolveSlash(rawContent, slashContext);
867
+ const slashResult = await resolveSlash(rawContent, slashContext);
849
868
 
850
869
  if (slashResult.kind === "unknown") {
851
870
  session.processing = true;
@@ -890,7 +909,7 @@ export async function handleSendMessage(
890
909
  // a config change from a concurrent request.
891
910
  const modelInfoEvent =
892
911
  isModelSlashCommand(rawContent) || isProviderShortcut(rawContent)
893
- ? buildModelInfoEvent()
912
+ ? await buildModelInfoEvent()
894
913
  : null;
895
914
 
896
915
  const response = Response.json(
@@ -960,7 +979,7 @@ export async function handleSendMessage(
960
979
  // Fire-and-forget the agent loop; events flow to the hub via onEvent.
961
980
  session
962
981
  .runAgentLoop(resolvedContent, messageId, onEvent, {
963
- isInteractive: isInteractiveInterface,
982
+ isInteractive,
964
983
  isUserMessage: true,
965
984
  })
966
985
  .catch((err) => {
@@ -250,7 +250,7 @@ export async function handleChannelInbound(
250
250
  canonicalAssistantId,
251
251
  assistantId,
252
252
  content,
253
- contactId: resolvedMember?.contact.id,
253
+ channelId: resolvedMember?.channel.id,
254
254
  });
255
255
  }
256
256
 
@@ -306,7 +306,7 @@ export async function handleChannelInbound(
306
306
  // retries). This was previously in ACL enforcement which runs before dedup,
307
307
  // causing retries to inflate interaction counts.
308
308
  if (!result.duplicate && resolvedMember) {
309
- touchContactInteraction(resolvedMember.contact.id);
309
+ touchContactInteraction(resolvedMember.channel.id);
310
310
  }
311
311
 
312
312
  // external_conversation_bindings is assistant-agnostic. Restrict writes to
@@ -390,6 +390,13 @@ export async function handleChannelInbound(
390
390
  ? (rawCommandIntent as Record<string, unknown>)
391
391
  : undefined;
392
392
 
393
+ // Extract chat type (e.g. "private", "group", "supergroup") for group chat gating
394
+ const sourceChatType =
395
+ typeof sourceMetadata?.chatType === "string" &&
396
+ sourceMetadata.chatType.trim().length > 0
397
+ ? sourceMetadata.chatType.trim()
398
+ : undefined;
399
+
393
400
  // Preserve locale from sourceMetadata so the model can greet in the user's language
394
401
  const sourceLanguageCode =
395
402
  typeof sourceMetadata?.languageCode === "string" &&
@@ -620,6 +627,7 @@ export async function handleChannelInbound(
620
627
  assistantId: canonicalAssistantId,
621
628
  approvalCopyGenerator,
622
629
  externalMessageId: sourceMessageId ?? externalMessageId,
630
+ chatType: sourceChatType,
623
631
  });
624
632
  }
625
633
 
@@ -77,6 +77,8 @@ export interface BackgroundProcessingParams {
77
77
  sourceLanguageCode?: string;
78
78
  /** External message ID (e.g. Slack message ts) used for reaction indicators. */
79
79
  externalMessageId?: string;
80
+ /** Chat type from the gateway (e.g. "private", "group", "supergroup"). */
81
+ chatType?: string;
80
82
  }
81
83
 
82
84
  /**
@@ -105,6 +107,7 @@ export function processChannelMessageInBackground(
105
107
  commandIntent,
106
108
  sourceLanguageCode,
107
109
  externalMessageId,
110
+ chatType,
108
111
  } = params;
109
112
 
110
113
  (async () => {
@@ -193,6 +196,7 @@ export function processChannelMessageInBackground(
193
196
  channelId: sourceChannel,
194
197
  hints: metadataHints.length > 0 ? metadataHints : undefined,
195
198
  uxBrief: metadataUxBrief,
199
+ chatType,
196
200
  },
197
201
  assistantId,
198
202
  trustContext: trustCtx,
@@ -30,8 +30,8 @@ export interface EditInterceptParams {
30
30
  canonicalAssistantId: string;
31
31
  assistantId: string;
32
32
  content: string | undefined;
33
- /** Contact ID for interaction tracking; omitted when the sender has no resolved member. */
34
- contactId?: string;
33
+ /** Channel ID for channel-level interaction tracking. */
34
+ channelId?: string;
35
35
  }
36
36
 
37
37
  /**
@@ -52,7 +52,7 @@ export async function handleEditIntercept(
52
52
  canonicalAssistantId,
53
53
  assistantId,
54
54
  content,
55
- contactId,
55
+ channelId,
56
56
  } = params;
57
57
 
58
58
  // Dedup the edit event itself (retried edited_message webhooks)
@@ -73,8 +73,8 @@ export async function handleEditIntercept(
73
73
 
74
74
  // Track contact interaction only for genuinely new edit events (not webhook
75
75
  // retries), matching the pattern used for the normal message path.
76
- if (contactId) {
77
- touchContactInteraction(contactId);
76
+ if (channelId) {
77
+ touchContactInteraction(channelId);
78
78
  }
79
79
 
80
80
  // Retry lookup a few times -- the original message may still be processing
@@ -13,7 +13,7 @@ import {
13
13
  } from "../../../../messaging/providers/slack/client.js";
14
14
  import type { SlackConversation } from "../../../../messaging/providers/slack/types.js";
15
15
  import { getConnectionByProvider } from "../../../../oauth/oauth-store.js";
16
- import { getSecureKey } from "../../../../security/secure-keys.js";
16
+ import { getSecureKeyAsync } from "../../../../security/secure-keys.js";
17
17
  import { getLogger } from "../../../../util/logger.js";
18
18
  import { httpError } from "../../../http-errors.js";
19
19
  import type { RouteDefinition } from "../../../http-router.js";
@@ -27,10 +27,10 @@ const log = getLogger("slack-share");
27
27
  /**
28
28
  * Resolve the Slack bot token from the OAuth connection store.
29
29
  */
30
- function resolveSlackToken(): string | undefined {
30
+ async function resolveSlackToken(): Promise<string | undefined> {
31
31
  const conn = getConnectionByProvider("integration:slack");
32
32
  return conn
33
- ? getSecureKey(`oauth_connection/${conn.id}/access_token`)
33
+ ? await getSecureKeyAsync(`oauth_connection/${conn.id}/access_token`)
34
34
  : undefined;
35
35
  }
36
36
 
@@ -61,7 +61,7 @@ const TYPE_SORT_ORDER: Record<string, number> = {
61
61
  };
62
62
 
63
63
  export async function handleListSlackChannels(): Promise<Response> {
64
- const token = resolveSlackToken();
64
+ const token = await resolveSlackToken();
65
65
  if (!token) {
66
66
  return httpError("SERVICE_UNAVAILABLE", "No Slack token configured", 503);
67
67
  }
@@ -137,7 +137,7 @@ export async function handleListSlackChannels(): Promise<Response> {
137
137
  export async function handleShareToSlackChannel(
138
138
  req: Request,
139
139
  ): Promise<Response> {
140
- const token = resolveSlackToken();
140
+ const token = await resolveSlackToken();
141
141
  if (!token) {
142
142
  return httpError("SERVICE_UNAVAILABLE", "No Slack token configured", 503);
143
143
  }
@@ -6,8 +6,15 @@
6
6
  * of requiring direct filesystem access.
7
7
  */
8
8
 
9
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
10
- import { join } from "node:path";
9
+ import { spawnSync } from "node:child_process";
10
+ import {
11
+ existsSync,
12
+ lstatSync,
13
+ readdirSync,
14
+ readFileSync,
15
+ statSync,
16
+ } from "node:fs";
17
+ import { join, relative } from "node:path";
11
18
 
12
19
  import { desc } from "drizzle-orm";
13
20
 
@@ -18,6 +25,7 @@ import {
18
25
  getDataDir,
19
26
  getRootDir,
20
27
  getWorkspaceConfigPath,
28
+ getWorkspaceDir,
21
29
  } from "../../util/platform.js";
22
30
  import { httpError } from "../http-errors.js";
23
31
  import type { RouteDefinition } from "../http-router.js";
@@ -36,6 +44,7 @@ interface ExportResponse {
36
44
  auditRows: Array<Record<string, unknown>>;
37
45
  logFiles: Record<string, string>;
38
46
  configSnapshot?: Record<string, unknown>;
47
+ workspaceFiles: Record<string, string>;
39
48
  }
40
49
 
41
50
  /**
@@ -91,12 +100,16 @@ async function handleExport(body: ExportRequestBody): Promise<Response> {
91
100
  // --- Sanitized config snapshot ---
92
101
  const configSnapshot = readSanitizedConfig();
93
102
 
103
+ // --- Workspace files ---
104
+ const workspaceFiles = collectWorkspaceFiles();
105
+
94
106
  log.info(
95
107
  {
96
108
  auditCount: auditRows.length,
97
109
  logFileCount: Object.keys(logFiles).length,
98
110
  totalBytes,
99
111
  hasConfig: configSnapshot !== undefined,
112
+ workspaceFileCount: Object.keys(workspaceFiles).length,
100
113
  },
101
114
  "Export completed",
102
115
  );
@@ -106,6 +119,7 @@ async function handleExport(body: ExportRequestBody): Promise<Response> {
106
119
  auditRows,
107
120
  logFiles,
108
121
  configSnapshot,
122
+ workspaceFiles,
109
123
  };
110
124
  return Response.json(payload);
111
125
  } catch (err) {
@@ -115,6 +129,112 @@ async function handleExport(body: ExportRequestBody): Promise<Response> {
115
129
  }
116
130
  }
117
131
 
132
+ /** Directory prefixes to skip when collecting workspace files. */
133
+ const WORKSPACE_SKIP_DIRS = new Set(["embedding-models", "data/qdrant"]);
134
+
135
+ /** Files at the workspace root to skip (already covered by sanitized fields). */
136
+ const WORKSPACE_SKIP_ROOT_FILES = new Set(["config.json"]);
137
+
138
+ /** Maximum cumulative size for workspace file contents (10 MB). */
139
+ const MAX_WORKSPACE_PAYLOAD_BYTES = 10 * 1024 * 1024;
140
+
141
+ /**
142
+ * Recursively collects files from the workspace directory into a
143
+ * `Record<string, string>` map of relative path to content.
144
+ *
145
+ * - Skips `config.json` at the workspace root (already exported as a
146
+ * sanitized `configSnapshot`; the raw file contains secrets).
147
+ * - Skips symlinks to prevent reading files outside the workspace.
148
+ * - Skips directories in `WORKSPACE_SKIP_DIRS`.
149
+ * - For `.db` files, shells out to `sqlite3 <path> .dump` and stores the
150
+ * SQL text output with a `.sql` suffix appended to the key.
151
+ * - Skips binary files (detected via null-byte heuristic).
152
+ * - Stops collecting once `MAX_WORKSPACE_PAYLOAD_BYTES` is reached.
153
+ */
154
+ function collectWorkspaceFiles(): Record<string, string> {
155
+ const wsDir = getWorkspaceDir();
156
+ if (!existsSync(wsDir)) return {};
157
+
158
+ const result: Record<string, string> = {};
159
+ let totalBytes = 0;
160
+
161
+ function walk(dir: string): void {
162
+ let entries: string[];
163
+ try {
164
+ entries = readdirSync(dir);
165
+ } catch {
166
+ return;
167
+ }
168
+
169
+ for (const entry of entries) {
170
+ const fullPath = join(dir, entry);
171
+ const relPath = relative(wsDir, fullPath);
172
+
173
+ // Check if this path falls under a skipped directory prefix
174
+ if (
175
+ [...WORKSPACE_SKIP_DIRS].some(
176
+ (prefix) => relPath === prefix || relPath.startsWith(prefix + "/"),
177
+ )
178
+ ) {
179
+ continue;
180
+ }
181
+
182
+ // Skip root-level files that are already exported separately
183
+ if (dir === wsDir && WORKSPACE_SKIP_ROOT_FILES.has(entry)) {
184
+ continue;
185
+ }
186
+
187
+ try {
188
+ // Use lstatSync to avoid following symlinks
189
+ const stat = lstatSync(fullPath);
190
+
191
+ // Skip symlinks — they could point outside the workspace
192
+ if (stat.isSymbolicLink()) continue;
193
+
194
+ if (stat.isDirectory()) {
195
+ walk(fullPath);
196
+ continue;
197
+ }
198
+ if (!stat.isFile()) continue;
199
+
200
+ // Enforce cumulative size cap
201
+ if (totalBytes + stat.size > MAX_WORKSPACE_PAYLOAD_BYTES) continue;
202
+
203
+ // SQLite DB handling: dump as SQL text
204
+ if (entry.endsWith(".db")) {
205
+ try {
206
+ const proc = spawnSync("sqlite3", [fullPath, ".dump"], {
207
+ timeout: 10_000,
208
+ });
209
+ if (proc.status === 0 && proc.stdout) {
210
+ const output =
211
+ proc.stdout instanceof Buffer
212
+ ? proc.stdout.toString("utf-8")
213
+ : String(proc.stdout);
214
+ result[relPath + ".sql"] = output;
215
+ totalBytes += Buffer.byteLength(output, "utf-8");
216
+ }
217
+ } catch {
218
+ // Skip if dump fails
219
+ }
220
+ continue;
221
+ }
222
+
223
+ // Read as UTF-8 and skip binary files (null-byte heuristic)
224
+ const content = readFileSync(fullPath, "utf-8");
225
+ if (content.includes("\0")) continue;
226
+ result[relPath] = content;
227
+ totalBytes += stat.size;
228
+ } catch {
229
+ // Skip unreadable files
230
+ }
231
+ }
232
+ }
233
+
234
+ walk(wsDir);
235
+ return result;
236
+ }
237
+
118
238
  /**
119
239
  * Replaces a string value with a presence flag: "(set)" if truthy, "(empty)" otherwise.
120
240
  */
@@ -134,14 +254,6 @@ function readSanitizedConfig(): Record<string, unknown> | undefined {
134
254
  const raw = readFileSync(configPath, "utf-8");
135
255
  const config = JSON.parse(raw) as Record<string, unknown>;
136
256
 
137
- // Strip API key values — preserve which providers have keys configured
138
- if (config.apiKeys && typeof config.apiKeys === "object") {
139
- const keys = config.apiKeys as Record<string, unknown>;
140
- config.apiKeys = Object.fromEntries(
141
- Object.keys(keys).map((k) => [k, redactStringValue(keys[k])]),
142
- );
143
- }
144
-
145
257
  // Strip ingress webhook secret
146
258
  if (config.ingress && typeof config.ingress === "object") {
147
259
  const ingress = config.ingress as Record<string, unknown>;
@@ -52,8 +52,8 @@ export function sessionQueryRouteDefinitions(
52
52
  endpoint: "model",
53
53
  method: "GET",
54
54
  policyKey: "model",
55
- handler: () => {
56
- const info = getModelInfo();
55
+ handler: async () => {
56
+ const info = await getModelInfo();
57
57
  return Response.json(info);
58
58
  },
59
59
  },
@@ -74,7 +74,7 @@ export function sessionQueryRouteDefinitions(
74
74
  );
75
75
  }
76
76
  try {
77
- const info = setModel(body.modelId, deps.getModelSetContext());
77
+ const info = await setModel(body.modelId, deps.getModelSetContext());
78
78
  return Response.json(info);
79
79
  } catch (err) {
80
80
  const message = err instanceof Error ? err.message : String(err);
@@ -10,7 +10,13 @@
10
10
  import { readFileSync } from "node:fs";
11
11
  import { join } from "node:path";
12
12
 
13
+ import { setIngressPublicBaseUrl } from "../../config/env.js";
14
+ import { loadRawConfig, saveRawConfig } from "../../config/loader.js";
13
15
  import { loadSkillCatalog } from "../../config/skills.js";
16
+ import {
17
+ computeGatewayTarget,
18
+ getIngressConfigResult,
19
+ } from "../../daemon/handlers/config-ingress.js";
14
20
  import { normalizeActivationKey } from "../../daemon/handlers/config-voice.js";
15
21
  import { orchestrateOAuthConnect } from "../../oauth/connect-orchestrator.js";
16
22
  import {
@@ -694,5 +700,52 @@ export function settingsRouteDefinitions(): RouteDefinition[] {
694
700
  policyKey: "diagnostics/env-vars",
695
701
  handler: () => handleEnvVars(),
696
702
  },
703
+
704
+ // Ingress config (GET / PUT)
705
+ {
706
+ endpoint: "integrations/ingress/config",
707
+ method: "GET",
708
+ policyKey: "integrations/ingress/config:GET",
709
+ handler: () => Response.json(getIngressConfigResult()),
710
+ },
711
+ {
712
+ endpoint: "integrations/ingress/config",
713
+ method: "PUT",
714
+ policyKey: "integrations/ingress/config",
715
+ handler: async ({ req }) => {
716
+ try {
717
+ const body = (await req.json()) as {
718
+ publicBaseUrl?: string;
719
+ enabled?: boolean;
720
+ };
721
+ const value = (body.publicBaseUrl ?? "").trim().replace(/\/+$/, "");
722
+ const raw = loadRawConfig();
723
+ const ingress = (raw?.ingress ?? {}) as Record<string, unknown>;
724
+ ingress.publicBaseUrl = value || undefined;
725
+ if (body.enabled !== undefined) {
726
+ ingress.enabled = body.enabled;
727
+ }
728
+ saveRawConfig({ ...raw, ingress });
729
+
730
+ const isEnabled = (ingress.enabled as boolean | undefined) ?? false;
731
+ if (value && isEnabled) {
732
+ setIngressPublicBaseUrl(value);
733
+ } else {
734
+ setIngressPublicBaseUrl(undefined);
735
+ }
736
+
737
+ return Response.json({
738
+ enabled: isEnabled,
739
+ publicBaseUrl: value,
740
+ localGatewayTarget: computeGatewayTarget(),
741
+ success: true,
742
+ });
743
+ } catch (err) {
744
+ const message = err instanceof Error ? err.message : String(err);
745
+ log.error({ err }, "Failed to update ingress config via HTTP");
746
+ return httpError("INTERNAL_ERROR", message, 500);
747
+ }
748
+ },
749
+ },
697
750
  ];
698
751
  }
@@ -1,5 +1,8 @@
1
1
  /**
2
2
  * Route handlers for workspace file browsing and content serving.
3
+ *
4
+ * WARNING: Workspace contents are included in diagnostic log exports.
5
+ * Do not store secrets here — use the credential store or protected/ directory.
3
6
  */
4
7
  import {
5
8
  existsSync,
@@ -165,7 +165,7 @@ const voiceTemplates: Record<
165
165
  "That code was incorrect. Please try again.",
166
166
 
167
167
  [GUARDIAN_VERIFY_TEMPLATE_KEYS.VOICE_SUCCESS]: (_vars) =>
168
- "Verification successful. Thank you. Goodbye.",
168
+ "Verification successful.",
169
169
 
170
170
  [GUARDIAN_VERIFY_TEMPLATE_KEYS.VOICE_FAILURE]: (_vars) =>
171
171
  "Too many incorrect attempts. Goodbye.",
@@ -359,9 +359,9 @@ function startLoopbackServerAndWaitForCode(
359
359
  server.close();
360
360
  }
361
361
 
362
- server.listen(loopbackPort ?? 0, "127.0.0.1", () => {
362
+ server.listen(loopbackPort ?? 0, "localhost", () => {
363
363
  const addr = server.address() as { port: number };
364
- boundRedirectUri = `http://127.0.0.1:${addr.port}${LOOPBACK_CALLBACK_PATH}`;
364
+ boundRedirectUri = `http://localhost:${addr.port}${LOOPBACK_CALLBACK_PATH}`;
365
365
 
366
366
  const authParams = new URLSearchParams({
367
367
  ...config.extraParams,
@@ -617,9 +617,9 @@ function startLoopbackServerForPreparedFlow(
617
617
  server.close();
618
618
  }
619
619
 
620
- server.listen(loopbackPort ?? 0, "127.0.0.1", () => {
620
+ server.listen(loopbackPort ?? 0, "localhost", () => {
621
621
  const addr = server.address() as { port: number };
622
- const redirectUri = `http://127.0.0.1:${addr.port}${LOOPBACK_CALLBACK_PATH}`;
622
+ const redirectUri = `http://localhost:${addr.port}${LOOPBACK_CALLBACK_PATH}`;
623
623
  listening = true;
624
624
  resolveSetup({ redirectUri, codePromise });
625
625
  });
@@ -37,8 +37,8 @@ function getBroker(): KeychainBrokerClient {
37
37
  *
38
38
  * @deprecated Use `getSecureKeyAsync` instead. This sync variant only reads
39
39
  * from the encrypted file store, bypassing the keychain broker. Retained only
40
- * for startup code paths in `config/loader.ts` and `providers/managed-proxy/context.ts`
41
- * that cannot do async I/O.
40
+ * for sync code paths in `providers/registry.ts`, `providers/managed-proxy/context.ts`,
41
+ * and `memory/embedding-backend.ts` that cannot do async I/O.
42
42
  */
43
43
  export function getSecureKey(account: string): string | undefined {
44
44
  return encryptedStore.getKey(account);
@@ -50,8 +50,8 @@ export function getSecureKey(account: string): string | undefined {
50
50
  *
51
51
  * @deprecated Use `setSecureKeyAsync` instead. This sync variant only writes
52
52
  * to the encrypted file store, bypassing the keychain broker. Retained only
53
- * for startup code paths in `config/loader.ts` and `providers/managed-proxy/context.ts`
54
- * that cannot do async I/O.
53
+ * for sync code paths in `providers/registry.ts`, `providers/managed-proxy/context.ts`,
54
+ * and `memory/embedding-backend.ts` that cannot do async I/O.
55
55
  */
56
56
  export function setSecureKey(account: string, value: string): boolean {
57
57
  return encryptedStore.setKey(account, value);