@vellumai/assistant 0.4.51 → 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 (220) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/docs/architecture/keychain-broker.md +19 -6
  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-routes.test.ts +1 -0
  19. package/src/__tests__/channel-readiness-service.test.ts +1 -0
  20. package/src/__tests__/claude-code-skill-regression.test.ts +6 -2
  21. package/src/__tests__/claude-code-tool-profiles.test.ts +7 -2
  22. package/src/__tests__/config-loader-backfill.test.ts +1 -2
  23. package/src/__tests__/config-schema.test.ts +6 -37
  24. package/src/__tests__/conversation-routes-slash-commands.test.ts +0 -1
  25. package/src/__tests__/credential-broker-server-use.test.ts +16 -16
  26. package/src/__tests__/credential-security-invariants.test.ts +14 -0
  27. package/src/__tests__/credential-vault-unit.test.ts +4 -4
  28. package/src/__tests__/error-handler-friendly-messages.test.ts +4 -5
  29. package/src/__tests__/gateway-only-enforcement.test.ts +0 -2
  30. package/src/__tests__/host-shell-tool.test.ts +0 -1
  31. package/src/__tests__/http-user-message-parity.test.ts +19 -0
  32. package/src/__tests__/list-messages-attachments.test.ts +0 -1
  33. package/src/__tests__/log-export-workspace.test.ts +233 -0
  34. package/src/__tests__/managed-proxy-context.test.ts +1 -1
  35. package/src/__tests__/managed-skill-lifecycle.test.ts +0 -1
  36. package/src/__tests__/media-generate-image.test.ts +7 -2
  37. package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
  38. package/src/__tests__/memory-regressions.test.ts +0 -1
  39. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1
  40. package/src/__tests__/migration-export-http.test.ts +0 -1
  41. package/src/__tests__/migration-import-commit-http.test.ts +0 -1
  42. package/src/__tests__/migration-import-preflight-http.test.ts +0 -1
  43. package/src/__tests__/migration-validate-http.test.ts +0 -1
  44. package/src/__tests__/notification-schedule-dedup.test.ts +237 -0
  45. package/src/__tests__/oauth-cli.test.ts +1 -10
  46. package/src/__tests__/oauth-store.test.ts +3 -5
  47. package/src/__tests__/oauth2-gateway-transport.test.ts +5 -4
  48. package/src/__tests__/onboarding-starter-tasks.test.ts +1 -1
  49. package/src/__tests__/onboarding-template-contract.test.ts +1 -2
  50. package/src/__tests__/pricing.test.ts +0 -11
  51. package/src/__tests__/provider-commit-message-generator.test.ts +21 -14
  52. package/src/__tests__/provider-fail-open-selection.test.ts +9 -8
  53. package/src/__tests__/provider-managed-proxy-integration.test.ts +27 -24
  54. package/src/__tests__/provider-registry-ollama.test.ts +8 -2
  55. package/src/__tests__/recording-handler.test.ts +0 -1
  56. package/src/__tests__/relay-server.test.ts +0 -1
  57. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  58. package/src/__tests__/runtime-events-sse-parity.test.ts +0 -1
  59. package/src/__tests__/runtime-events-sse.test.ts +0 -1
  60. package/src/__tests__/script-proxy-injection-runtime.test.ts +4 -0
  61. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -1
  62. package/src/__tests__/secret-scanner-executor.test.ts +0 -1
  63. package/src/__tests__/send-endpoint-busy.test.ts +0 -1
  64. package/src/__tests__/session-abort-tool-results.test.ts +3 -1
  65. package/src/__tests__/session-agent-loop-overflow.test.ts +1012 -838
  66. package/src/__tests__/session-agent-loop.test.ts +2 -2
  67. package/src/__tests__/session-confirmation-signals.test.ts +3 -1
  68. package/src/__tests__/session-error.test.ts +5 -4
  69. package/src/__tests__/session-history-web-search.test.ts +34 -9
  70. package/src/__tests__/session-pre-run-repair.test.ts +3 -1
  71. package/src/__tests__/session-provider-retry-repair.test.ts +31 -26
  72. package/src/__tests__/session-queue.test.ts +3 -1
  73. package/src/__tests__/session-runtime-assembly.test.ts +118 -0
  74. package/src/__tests__/session-slash-known.test.ts +31 -13
  75. package/src/__tests__/session-slash-queue.test.ts +3 -1
  76. package/src/__tests__/session-slash-unknown.test.ts +3 -1
  77. package/src/__tests__/session-workspace-cache-state.test.ts +3 -1
  78. package/src/__tests__/session-workspace-injection.test.ts +3 -1
  79. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -1
  80. package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -1
  81. package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -1
  82. package/src/__tests__/skillssh-registry.test.ts +21 -0
  83. package/src/__tests__/slack-share-routes.test.ts +1 -1
  84. package/src/__tests__/swarm-recursion.test.ts +5 -1
  85. package/src/__tests__/swarm-session-integration.test.ts +25 -14
  86. package/src/__tests__/swarm-tool.test.ts +5 -2
  87. package/src/__tests__/telegram-bot-username-resolution.test.ts +2 -4
  88. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1521 -0
  89. package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -1
  90. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  91. package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
  92. package/src/__tests__/tool-executor.test.ts +0 -1
  93. package/src/__tests__/trust-store.test.ts +5 -1
  94. package/src/__tests__/twilio-routes.test.ts +2 -2
  95. package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
  96. package/src/__tests__/voice-quality.test.ts +2 -1
  97. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  98. package/src/__tests__/web-search.test.ts +1 -1
  99. package/src/agent/loop.ts +17 -1
  100. package/src/bundler/app-bundler.ts +40 -24
  101. package/src/calls/call-controller.ts +16 -0
  102. package/src/calls/relay-server.ts +29 -13
  103. package/src/calls/voice-control-protocol.ts +1 -0
  104. package/src/calls/voice-quality.ts +1 -1
  105. package/src/calls/voice-session-bridge.ts +9 -3
  106. package/src/channels/types.ts +16 -0
  107. package/src/cli/commands/bash.ts +173 -0
  108. package/src/cli/commands/doctor.ts +5 -23
  109. package/src/cli/commands/oauth/connections.ts +4 -2
  110. package/src/cli/commands/oauth/providers.ts +1 -13
  111. package/src/cli/program.ts +2 -0
  112. package/src/cli/reference.ts +1 -0
  113. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -1
  114. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +3 -5
  115. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -3
  116. package/src/config/bundled-skills/messaging/TOOLS.json +41 -1
  117. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
  118. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -1
  119. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +2 -1
  120. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +2 -1
  121. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +2 -1
  122. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +2 -1
  123. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +2 -1
  124. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +2 -1
  125. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -1
  126. package/src/config/bundled-skills/messaging/tools/shared.ts +2 -1
  127. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +1 -1
  128. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -6
  129. package/src/config/feature-flag-registry.json +8 -0
  130. package/src/config/loader.ts +7 -135
  131. package/src/config/schema.ts +0 -6
  132. package/src/config/schemas/channels.ts +1 -0
  133. package/src/config/schemas/elevenlabs.ts +2 -2
  134. package/src/contacts/contact-store.ts +21 -25
  135. package/src/contacts/contacts-write.ts +6 -6
  136. package/src/contacts/types.ts +2 -0
  137. package/src/context/token-estimator.ts +35 -2
  138. package/src/context/window-manager.ts +16 -2
  139. package/src/daemon/config-watcher.ts +24 -6
  140. package/src/daemon/context-overflow-reducer.ts +13 -2
  141. package/src/daemon/handlers/config-ingress.ts +25 -8
  142. package/src/daemon/handlers/config-model.ts +21 -15
  143. package/src/daemon/handlers/config-telegram.ts +18 -6
  144. package/src/daemon/handlers/dictation.ts +0 -429
  145. package/src/daemon/handlers/skills.ts +1 -200
  146. package/src/daemon/lifecycle.ts +8 -5
  147. package/src/daemon/message-types/contacts.ts +2 -0
  148. package/src/daemon/message-types/integrations.ts +1 -0
  149. package/src/daemon/message-types/sessions.ts +2 -0
  150. package/src/daemon/parse-actual-tokens-from-error.test.ts +75 -0
  151. package/src/daemon/server.ts +23 -2
  152. package/src/daemon/session-agent-loop-handlers.ts +1 -1
  153. package/src/daemon/session-agent-loop.ts +27 -79
  154. package/src/daemon/session-error.ts +5 -4
  155. package/src/daemon/session-process.ts +17 -10
  156. package/src/daemon/session-runtime-assembly.ts +50 -0
  157. package/src/daemon/session-slash.ts +32 -20
  158. package/src/daemon/session.ts +1 -0
  159. package/src/events/domain-events.ts +1 -0
  160. package/src/media/app-icon-generator.ts +2 -1
  161. package/src/media/avatar-router.ts +3 -2
  162. package/src/memory/canonical-guardian-store.ts +25 -3
  163. package/src/memory/db-init.ts +12 -0
  164. package/src/memory/embedding-backend.ts +25 -16
  165. package/src/memory/migrations/158-channel-interaction-columns.ts +18 -0
  166. package/src/memory/migrations/159-drop-contact-interaction-columns.ts +16 -0
  167. package/src/memory/migrations/160-drop-loopback-port-column.ts +13 -0
  168. package/src/memory/migrations/index.ts +3 -0
  169. package/src/memory/retriever.test.ts +19 -12
  170. package/src/memory/schema/contacts.ts +2 -2
  171. package/src/memory/schema/oauth.ts +0 -1
  172. package/src/oauth/byo-connection.ts +55 -49
  173. package/src/oauth/connect-orchestrator.ts +5 -3
  174. package/src/oauth/connect-types.ts +9 -2
  175. package/src/oauth/manual-token-connection.ts +9 -7
  176. package/src/oauth/oauth-store.ts +2 -8
  177. package/src/oauth/provider-behaviors.ts +10 -0
  178. package/src/oauth/seed-providers.ts +13 -5
  179. package/src/permissions/checker.ts +20 -1
  180. package/src/prompts/__tests__/build-cli-reference-section.test.ts +1 -1
  181. package/src/prompts/system-prompt.ts +2 -11
  182. package/src/prompts/templates/BOOTSTRAP.md +1 -3
  183. package/src/providers/anthropic/client.ts +16 -8
  184. package/src/providers/managed-proxy/constants.ts +1 -1
  185. package/src/providers/registry.ts +21 -15
  186. package/src/providers/types.ts +1 -1
  187. package/src/runtime/auth/route-policy.ts +4 -0
  188. package/src/runtime/channel-invite-transports/telegram.ts +12 -6
  189. package/src/runtime/channel-retry-sweep.ts +6 -0
  190. package/src/runtime/http-types.ts +1 -0
  191. package/src/runtime/middleware/error-handler.ts +1 -2
  192. package/src/runtime/routes/app-management-routes.ts +1 -0
  193. package/src/runtime/routes/btw-routes.ts +20 -1
  194. package/src/runtime/routes/conversation-routes.ts +32 -13
  195. package/src/runtime/routes/inbound-message-handler.ts +10 -2
  196. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
  197. package/src/runtime/routes/inbound-stages/edit-intercept.ts +5 -5
  198. package/src/runtime/routes/integrations/slack/share.ts +5 -5
  199. package/src/runtime/routes/log-export-routes.ts +122 -10
  200. package/src/runtime/routes/session-query-routes.ts +3 -3
  201. package/src/runtime/routes/settings-routes.ts +53 -0
  202. package/src/runtime/routes/workspace-routes.ts +3 -0
  203. package/src/runtime/verification-templates.ts +1 -1
  204. package/src/security/oauth2.ts +4 -4
  205. package/src/security/secure-keys.ts +24 -3
  206. package/src/security/token-manager.ts +7 -8
  207. package/src/signals/bash.ts +157 -0
  208. package/src/skills/skillssh-registry.ts +6 -1
  209. package/src/swarm/backend-claude-code.ts +6 -6
  210. package/src/swarm/worker-backend.ts +1 -1
  211. package/src/swarm/worker-runner.ts +1 -1
  212. package/src/telegram/bot-username.ts +11 -0
  213. package/src/tools/claude-code/claude-code.ts +4 -4
  214. package/src/tools/credentials/broker.ts +7 -5
  215. package/src/tools/credentials/vault.ts +3 -2
  216. package/src/tools/network/__tests__/web-search.test.ts +18 -86
  217. package/src/tools/network/web-search.ts +9 -15
  218. package/src/util/platform.ts +7 -1
  219. package/src/util/pricing.ts +0 -1
  220. package/src/workspace/provider-commit-message-generator.ts +10 -6
@@ -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
  });
@@ -3,8 +3,14 @@
3
3
  * available (macOS app embedded), with transparent fallback to the
4
4
  * encrypted-at-rest file store.
5
5
  *
6
- * Async variants try the encrypted store first; sync variants always use the
7
- * encrypted store (startup code paths cannot do async I/O).
6
+ * **Async variants are the primary API.** They try the encrypted store first
7
+ * and fall back to the keychain broker. All new call sites should use
8
+ * `getSecureKeyAsync`, `setSecureKeyAsync`, and `deleteSecureKeyAsync`.
9
+ *
10
+ * Sync variants (`getSecureKey`, `setSecureKey`, `deleteSecureKey`) are
11
+ * **deprecated startup-only exceptions** that bypass the keychain broker
12
+ * entirely. They exist solely for code paths that cannot do async I/O
13
+ * (e.g. `config/loader.ts`, `providers/managed-proxy/context.ts`).
8
14
  */
9
15
 
10
16
  import { getLogger } from "../util/logger.js";
@@ -22,12 +28,17 @@ function getBroker(): KeychainBrokerClient {
22
28
  }
23
29
 
24
30
  // ---------------------------------------------------------------------------
25
- // Sync variants — encrypted store only (startup / sync call sites)
31
+ // Sync variants — encrypted store only (DEPRECATED startup-only exceptions)
26
32
  // ---------------------------------------------------------------------------
27
33
 
28
34
  /**
29
35
  * Retrieve a secret from secure storage (sync — encrypted store only).
30
36
  * Returns `undefined` if the key doesn't exist or on error.
37
+ *
38
+ * @deprecated Use `getSecureKeyAsync` instead. This sync variant only reads
39
+ * from the encrypted file store, bypassing the keychain broker. Retained only
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.
31
42
  */
32
43
  export function getSecureKey(account: string): string | undefined {
33
44
  return encryptedStore.getKey(account);
@@ -36,6 +47,11 @@ export function getSecureKey(account: string): string | undefined {
36
47
  /**
37
48
  * Store a secret in secure storage (sync — encrypted store only).
38
49
  * Returns `true` on success, `false` on failure.
50
+ *
51
+ * @deprecated Use `setSecureKeyAsync` instead. This sync variant only writes
52
+ * to the encrypted file store, bypassing the keychain broker. Retained only
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.
39
55
  */
40
56
  export function setSecureKey(account: string, value: string): boolean {
41
57
  return encryptedStore.setKey(account, value);
@@ -48,6 +64,11 @@ export type DeleteResult = "deleted" | "not-found" | "error";
48
64
  * Delete a secret from secure storage (sync — encrypted store only).
49
65
  * Returns `"deleted"` on success, `"not-found"` if key doesn't exist,
50
66
  * or `"error"` on failure.
67
+ *
68
+ * @deprecated Use `deleteSecureKeyAsync` instead. This sync variant only
69
+ * deletes from the encrypted file store, bypassing the keychain broker.
70
+ * Retained only for startup code paths in `config/loader.ts` and
71
+ * `providers/managed-proxy/context.ts` that cannot do async I/O.
51
72
  */
52
73
  export function deleteSecureKey(account: string): DeleteResult {
53
74
  return encryptedStore.deleteKey(account);
@@ -16,11 +16,7 @@ import {
16
16
  } from "../oauth/oauth-store.js";
17
17
  import { getLogger } from "../util/logger.js";
18
18
  import { refreshOAuth2Token, type TokenEndpointAuthMethod } from "./oauth2.js";
19
- import {
20
- getSecureKey,
21
- getSecureKeyAsync,
22
- setSecureKeyAsync,
23
- } from "./secure-keys.js";
19
+ import { getSecureKeyAsync, setSecureKeyAsync } from "./secure-keys.js";
24
20
 
25
21
  const log = getLogger("token-manager");
26
22
 
@@ -374,11 +370,14 @@ async function doRefresh(service: string, connId: string): Promise<string> {
374
370
  export async function withValidToken<T>(
375
371
  service: string,
376
372
  callback: (token: string) => Promise<T>,
377
- clientId?: string,
373
+ opts?: string | { connectionId: string },
378
374
  ): Promise<T> {
379
- const conn = getConnectionByProvider(service, clientId);
375
+ const conn =
376
+ opts && typeof opts === "object"
377
+ ? getConnection(opts.connectionId)
378
+ : getConnectionByProvider(service, opts);
380
379
  let token = conn
381
- ? getSecureKey(`oauth_connection/${conn.id}/access_token`)
380
+ ? await getSecureKeyAsync(`oauth_connection/${conn.id}/access_token`)
382
381
  : undefined;
383
382
  if (!token || !conn) {
384
383
  throw new TokenExpiredError(
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Handle bash debug signals delivered via signal files from the CLI.
3
+ *
4
+ * Each invocation writes JSON to a unique `signals/bash.<requestId>` file.
5
+ * ConfigWatcher detects the new file and invokes {@link handleBashSignal},
6
+ * which reads the payload, spawns the command, and writes the result to
7
+ * `signals/bash.<requestId>.result` for the CLI to pick up.
8
+ *
9
+ * Per-request filenames avoid dropped commands when overlapping invocations
10
+ * race on the same signal file.
11
+ */
12
+
13
+ import { spawn } from "node:child_process";
14
+ import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
15
+ import { join } from "node:path";
16
+
17
+ import { getLogger } from "../util/logger.js";
18
+ import { getWorkspaceDir } from "../util/platform.js";
19
+
20
+ const log = getLogger("signal:bash");
21
+
22
+ const DEFAULT_TIMEOUT_MS = 30_000;
23
+
24
+ interface BashSignalPayload {
25
+ requestId: string;
26
+ command: string;
27
+ timeoutMs?: number;
28
+ }
29
+
30
+ interface BashSignalResult {
31
+ requestId: string;
32
+ stdout: string;
33
+ stderr: string;
34
+ exitCode: number | null;
35
+ timedOut: boolean;
36
+ error?: string;
37
+ }
38
+
39
+ function writeResult(requestId: string, result: BashSignalResult): void {
40
+ try {
41
+ writeFileSync(
42
+ join(getWorkspaceDir(), "signals", `bash.${requestId}.result`),
43
+ JSON.stringify(result),
44
+ );
45
+ } catch (err) {
46
+ log.error({ err, requestId }, "Failed to write bash signal result");
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Read a `signals/bash.<requestId>` file, execute the command, and write
52
+ * the result to `signals/bash.<requestId>.result`. Called by ConfigWatcher
53
+ * when a matching signal file is created or modified.
54
+ */
55
+ export function handleBashSignal(filename: string): void {
56
+ const signalPath = join(getWorkspaceDir(), "signals", filename);
57
+ let raw: string;
58
+ try {
59
+ raw = readFileSync(signalPath, "utf-8");
60
+ } catch {
61
+ // File may already be deleted (e.g. re-trigger from our own unlinkSync).
62
+ return;
63
+ }
64
+
65
+ let payload: BashSignalPayload;
66
+ try {
67
+ payload = JSON.parse(raw) as BashSignalPayload;
68
+ } catch (err) {
69
+ log.error({ err, filename }, "Failed to parse bash signal file");
70
+ return;
71
+ }
72
+
73
+ try {
74
+ unlinkSync(signalPath);
75
+ } catch {
76
+ // Best-effort cleanup; the file may already be gone.
77
+ }
78
+
79
+ const { requestId, command, timeoutMs } = payload;
80
+
81
+ if (!requestId || typeof requestId !== "string") {
82
+ log.warn("Bash signal missing requestId");
83
+ return;
84
+ }
85
+ if (!command || typeof command !== "string") {
86
+ log.warn({ requestId }, "Bash signal missing command");
87
+ return;
88
+ }
89
+
90
+ const effectiveTimeout =
91
+ typeof timeoutMs === "number" && timeoutMs > 0
92
+ ? timeoutMs
93
+ : DEFAULT_TIMEOUT_MS;
94
+
95
+ log.info({ requestId, command }, "Executing bash signal command");
96
+
97
+ const stdoutChunks: Buffer[] = [];
98
+ const stderrChunks: Buffer[] = [];
99
+ let timedOut = false;
100
+ let resultWritten = false;
101
+
102
+ const child = spawn("bash", ["-c", command], {
103
+ cwd: getWorkspaceDir(),
104
+ stdio: ["ignore", "pipe", "pipe"],
105
+ });
106
+
107
+ const timer = setTimeout(() => {
108
+ timedOut = true;
109
+ child.kill("SIGKILL");
110
+ }, effectiveTimeout);
111
+
112
+ child.stdout.on("data", (data: Buffer) => {
113
+ stdoutChunks.push(data);
114
+ });
115
+
116
+ child.stderr.on("data", (data: Buffer) => {
117
+ stderrChunks.push(data);
118
+ });
119
+
120
+ child.on("close", (code) => {
121
+ clearTimeout(timer);
122
+ if (resultWritten) return;
123
+ resultWritten = true;
124
+
125
+ const stdout = Buffer.concat(stdoutChunks).toString();
126
+ const stderr = Buffer.concat(stderrChunks).toString();
127
+
128
+ log.info(
129
+ { requestId, exitCode: code, timedOut },
130
+ "Bash signal command completed",
131
+ );
132
+
133
+ writeResult(requestId, {
134
+ requestId,
135
+ stdout,
136
+ stderr,
137
+ exitCode: code,
138
+ timedOut,
139
+ });
140
+ });
141
+
142
+ child.on("error", (err) => {
143
+ clearTimeout(timer);
144
+ if (resultWritten) return;
145
+ resultWritten = true;
146
+
147
+ log.error({ err, requestId }, "Failed to spawn bash signal command");
148
+ writeResult(requestId, {
149
+ requestId,
150
+ stdout: "",
151
+ stderr: "",
152
+ exitCode: null,
153
+ timedOut: false,
154
+ error: err.message,
155
+ });
156
+ });
157
+ }
@@ -228,7 +228,12 @@ async function findSkillDirInTree(
228
228
  headers,
229
229
  signal: AbortSignal.timeout(15_000),
230
230
  });
231
- if (!response.ok) return null;
231
+ if (response.status === 404) return null;
232
+ if (!response.ok) {
233
+ throw new Error(
234
+ `GitHub API error while searching repo tree: HTTP ${response.status} ${response.statusText}`,
235
+ );
236
+ }
232
237
 
233
238
  const data = (await response.json()) as { tree: GitHubTreeEntry[] };
234
239
  const suffix = `${skillSlug}/SKILL.md`;
@@ -5,9 +5,9 @@
5
5
  * is testable and swappable independently of the tool adapter.
6
6
  */
7
7
 
8
- import { getConfig } from "../config/loader.js";
9
8
  import { resolveModelIntent } from "../providers/model-intents.js";
10
9
  import type { ModelIntent } from "../providers/types.js";
10
+ import { getSecureKeyAsync } from "../security/secure-keys.js";
11
11
  import { getLogger } from "../util/logger.js";
12
12
  import type {
13
13
  SwarmWorkerBackend,
@@ -28,9 +28,9 @@ export function createClaudeCodeBackend(): SwarmWorkerBackend {
28
28
  return {
29
29
  name: "claude_code",
30
30
 
31
- isAvailable(): boolean {
32
- const config = getConfig();
33
- const apiKey = config.apiKeys.anthropic ?? process.env.ANTHROPIC_API_KEY;
31
+ async isAvailable(): Promise<boolean> {
32
+ const apiKey =
33
+ (await getSecureKeyAsync("anthropic")) ?? process.env.ANTHROPIC_API_KEY;
34
34
  return !!apiKey;
35
35
  },
36
36
 
@@ -39,9 +39,9 @@ export function createClaudeCodeBackend(): SwarmWorkerBackend {
39
39
  const stderrLines: string[] = [];
40
40
  try {
41
41
  const { query } = await import("@anthropic-ai/claude-agent-sdk");
42
- const config = getConfig();
43
42
  const apiKey =
44
- config.apiKeys.anthropic ?? process.env.ANTHROPIC_API_KEY;
43
+ (await getSecureKeyAsync("anthropic")) ??
44
+ process.env.ANTHROPIC_API_KEY;
45
45
  if (!apiKey) {
46
46
  return {
47
47
  success: false,
@@ -125,7 +125,7 @@ export interface SwarmWorkerBackendInput {
125
125
  export interface SwarmWorkerBackend {
126
126
  readonly name: string;
127
127
  /** Check whether the backend is available (e.g. API key present). */
128
- isAvailable(): boolean;
128
+ isAvailable(): boolean | Promise<boolean>;
129
129
  /** Run a task with the given input. */
130
130
  runTask(input: SwarmWorkerBackendInput): Promise<SwarmWorkerBackendResult>;
131
131
  }
@@ -63,7 +63,7 @@ export async function runWorkerTask(
63
63
  emitStatus(onStatus, task.id, "queued");
64
64
 
65
65
  // Check backend availability
66
- if (!backend.isAvailable()) {
66
+ if (!(await backend.isAvailable())) {
67
67
  emitStatus(onStatus, task.id, "failed");
68
68
  return {
69
69
  taskId: task.id,
@@ -1,5 +1,16 @@
1
1
  import { getConfig } from "../config/loader.js";
2
2
 
3
+ /**
4
+ * Read the Telegram bot ID from config.
5
+ */
6
+ export function getTelegramBotId(): string | undefined {
7
+ const value = getConfig().telegram.botId;
8
+ if (value.trim().length > 0) {
9
+ return value.trim();
10
+ }
11
+ return undefined;
12
+ }
13
+
3
14
  /**
4
15
  * Read the Telegram bot username from config.
5
16
  */
@@ -2,9 +2,9 @@ import {
2
2
  getCCCommand,
3
3
  loadCCCommandTemplate,
4
4
  } from "../../commands/cc-command-registry.js";
5
- import { getConfig } from "../../config/loader.js";
6
5
  import { RiskLevel } from "../../permissions/types.js";
7
6
  import type { ToolDefinition } from "../../providers/types.js";
7
+ import { getSecureKeyAsync } from "../../security/secure-keys.js";
8
8
  import type { WorkerProfile } from "../../swarm/worker-backend.js";
9
9
  import { getProfilePolicy } from "../../swarm/worker-backend.js";
10
10
  import { getLogger } from "../../util/logger.js";
@@ -203,12 +203,12 @@ export const claudeCodeTool: Tool = {
203
203
  const profilePolicy = getProfilePolicy(profileName);
204
204
 
205
205
  // Validate API key
206
- const config = getConfig();
207
- const apiKey = config.apiKeys.anthropic ?? process.env.ANTHROPIC_API_KEY;
206
+ const apiKey =
207
+ (await getSecureKeyAsync("anthropic")) ?? process.env.ANTHROPIC_API_KEY;
208
208
  if (!apiKey) {
209
209
  return {
210
210
  content:
211
- "Error: No Anthropic API key configured. Set it via config or ANTHROPIC_API_KEY environment variable.",
211
+ "Error: No Anthropic API key configured. Set it via `keys set anthropic <key>` or configure it from the Settings page under API Keys.",
212
212
  isError: true,
213
213
  };
214
214
  }