@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
package/ARCHITECTURE.md CHANGED
@@ -641,7 +641,7 @@ The assistant feature-flag resolver (`src/config/assistant-feature-flags.ts`) is
641
641
  | **3. `skill_load` tool** | `executeSkillLoad()` in `tools/skills/load.ts` | If the model attempts to load a flagged-off skill by name, the tool returns an error: `"skill is currently unavailable (disabled by feature flag)"`. |
642
642
  | **4. Runtime tool projection** | `projectSkillTools()` in `daemon/session-skill-tools.ts` | Even if a skill was previously active in a session (has `<loaded_skill>` markers in history), the per-turn projection drops it when the flag is OFF. Already-registered tools are unregistered. |
643
643
  | **5. Included child skills** | `executeSkillLoad()` in `tools/skills/load.ts` | When a parent skill includes children via the `includes` directive, each child is independently checked against its feature flag. Flagged-off children are silently excluded from the loaded skill content. |
644
- | **6. Skill install gate** | `handleSkillsInstall()` in `daemon/handlers/skills.ts` | When a client requests skill installation, the handler checks the skill's feature flag before proceeding. If the flag is OFF, the install is rejected with an error. |
644
+ | **6. Skill install gate** | `installSkill()` in `daemon/handlers/skills.ts` | When a client requests skill installation, the function checks the skill's feature flag before proceeding. If the flag is OFF, the install is rejected with an error. |
645
645
 
646
646
  All six enforcement points derive the flag key via `skillFlagKey(skill)` — which returns `undefined` for ungated skills, short-circuiting the check — and then call `isAssistantFeatureFlagEnabled(flagKey, config)` for consistency.
647
647
 
@@ -657,7 +657,7 @@ All six enforcement points derive the flag key via `skillFlagKey(skill)` — whi
657
657
  | `src/tools/skills/load.ts` | `executeSkillLoad()` — enforcement points 3 and 5 |
658
658
  | `src/daemon/session-skill-tools.ts` | `projectSkillTools()` — enforcement point 4 |
659
659
  | `src/config/schema.ts` | `assistantFeatureFlagValues` field definition in `AssistantConfig` (Zod schema) |
660
- | `src/daemon/handlers/skills.ts` | `handleSkillsList()` — uses `resolveSkillStates()` for client responses; `handleSkillsInstall()` — enforcement point 6 |
660
+ | `src/daemon/handlers/skills.ts` | `listSkills()` — uses `resolveSkillStates()` for client responses; `installSkill()` — enforcement point 6 |
661
661
  | `meta/feature-flags/feature-flag-registry.json` | Unified feature flag registry (repo root) — all declared flags with scope, label, default values, and descriptions |
662
662
  | `src/config/feature-flag-registry.json` | Bundled copy of the unified registry for compiled binary resolution |
663
663
 
@@ -163,21 +163,34 @@ XPC provides stronger caller identity guarantees via audit tokens and code requi
163
163
 
164
164
  ## Callsite Policy
165
165
 
166
- ### Runtime request handlers (secret-routes, etc.)
166
+ ### Async-first policy
167
167
 
168
- All runtime HTTP handlers that write or delete secrets **must** use the async APIs (`setSecureKeyAsync`, `deleteSecureKeyAsync`). These are the primary entry points for macOS app flows and must go through the broker to reach keychain.
168
+ **All credential access should use the async functions** (`getSecureKeyAsync`, `setSecureKeyAsync`, `deleteSecureKeyAsync`). The async variants are the primary API: they check the encrypted store first (instant) and fall back to the keychain broker, ensuring secrets stored in the macOS Keychain are always reachable. The sync variants (`getSecureKey`, `setSecureKey`, `deleteSecureKey`) are **deprecated** and bypass the keychain broker entirely.
169
169
 
170
- ### CLI commands (keys, credentials)
170
+ New code must not introduce sync secure-key calls. Existing sync call sites should be converted to async when their surrounding code paths support it.
171
171
 
172
- CLI commands may use sync APIs (`setSecureKey`, `deleteSecureKey`, `getSecureKey`) since they run outside the macOS app process and the broker may not be available. The sync path uses the encrypted store directly, which is correct for headless/CLI environments.
172
+ ### Runtime request handlers (secret-routes, etc.)
173
+
174
+ All runtime HTTP handlers that write or delete secrets **must** use the async APIs (`setSecureKeyAsync`, `deleteSecureKeyAsync`). These are the primary entry points for macOS app flows and must go through the broker to reach keychain.
173
175
 
174
176
  ### Gateway (credential-reader)
175
177
 
176
178
  The gateway reads credentials via async `readCredential()` which tries the broker first (native async UDS), falling back to the encrypted store. The gateway never writes credentials — that responsibility belongs to the assistant runtime.
177
179
 
178
- ### Startup / initialization code
180
+ ### Known sync exceptions
181
+
182
+ The migration from sync to async secure-key functions is complete for all call sites except provider initialization paths that require synchronous access. The following call sites still use the deprecated sync variants.
183
+
184
+ #### Provider initialization (must remain sync)
185
+
186
+ These call sites run in synchronous initialization contexts where async I/O is not feasible:
187
+
188
+ | File | Sync functions used | Reason |
189
+ | -------------------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
190
+ | `assistant/src/providers/managed-proxy/context.ts` | `getSecureKey` | Provider context initialization is synchronous. The managed proxy context must resolve credentials before the first request can be processed, and the initialization path does not support awaiting. |
191
+ | `assistant/src/providers/registry.ts` | `getSecureKey` | Provider registry initialization is synchronous. API keys must be resolved at provider construction time, and the call chain from config watcher through to provider init is fully synchronous. |
179
192
 
180
- Sync APIs are acceptable for startup paths (e.g. provider initialization, config loading) where async is impractical or the broker may not yet be available.
193
+ Any new sync usage requires explicit justification and should be documented here.
181
194
 
182
195
  ## Migration
183
196
 
@@ -293,8 +293,8 @@ The embedding backend is selected based on `memory.embeddings.provider` config:
293
293
 
294
294
  - `auto` (default): Tries local → OpenAI → Gemini → Ollama, using the first available.
295
295
  - `local`: ONNX-based local model (bge-small-en-v1.5). Lazy-loaded to avoid crashing in compiled binaries where onnxruntime-node is unavailable.
296
- - `openai`: OpenAI text-embedding-3-small. Requires `apiKeys.openai`.
297
- - `gemini`: Gemini gemini-embedding-001. Requires `apiKeys.gemini`. Only backend supporting multimodal embeddings (images, audio, video).
296
+ - `openai`: OpenAI text-embedding-3-small. Requires an OpenAI API key in secure storage.
297
+ - `gemini`: Gemini gemini-embedding-001. Requires a Gemini API key in secure storage. Only backend supporting multimodal embeddings (images, audio, video).
298
298
  - `ollama`: Ollama nomic-embed-text. Requires Ollama to be configured.
299
299
 
300
300
  An in-memory LRU vector cache (32 MB cap, keyed by `sha256(provider + model + content)`) avoids redundant embedding calls for identical content. Sparse embeddings are generated in-process (no external calls).
@@ -570,7 +570,7 @@ graph TB
570
570
 
571
571
  **Commit message LLM fallback chain**: The generator runs a sequence of pre-flight checks before calling the LLM. Each check that fails produces a machine-readable `llmFallbackReason` in the structured log output and immediately returns a deterministic message. The checks, in order:
572
572
  1. `disabled` — `commitMessageLLM.enabled` is `false` or `useConfiguredProvider` is `false`
573
- 2. `missing_provider_api_key` — the configured provider's API key is not set in `config.apiKeys` (skipped for keyless providers like Ollama that run without an API key)
573
+ 2. `missing_provider_api_key` — the configured provider's API key is not found in secure storage (skipped for keyless providers like Ollama that run without an API key)
574
574
  3. `breaker_open` — the generator's internal circuit breaker is open after consecutive LLM failures (exponential backoff)
575
575
  4. `insufficient_budget` — the remaining turn budget (`deadlineMs - Date.now()`) is below `minRemainingTurnBudgetMs`
576
576
  5. `missing_fast_model` — no fast model could be resolved for the configured provider (see below); the provider is **not** called
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.4.51",
3
+ "version": "0.4.53",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
@@ -79,7 +79,6 @@ mock.module("../config/loader.js", () => ({
79
79
  },
80
80
  rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
81
81
  timeouts: { permissionTimeoutSec: 300 },
82
- apiKeys: {},
83
82
  skills: { entries: {}, allowBundled: true },
84
83
  permissions: { mode: "workspace" },
85
84
  }),
@@ -203,6 +202,9 @@ mock.module("../memory/llm-usage-store.js", () => ({
203
202
  mock.module("../agent/loop.js", () => ({
204
203
  AgentLoop: class {
205
204
  constructor() {}
205
+ getToolTokenBudget() {
206
+ return 0;
207
+ }
206
208
  async run(
207
209
  _messages: Message[],
208
210
  _onEvent: (event: AgentEvent) => void,
@@ -41,7 +41,6 @@ mock.module("../config/loader.js", () => ({
41
41
 
42
42
  model: "test",
43
43
  provider: "test",
44
- apiKeys: {},
45
44
  memory: { enabled: false },
46
45
  rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
47
46
  secretDetection: { enabled: false },
@@ -33,7 +33,6 @@ mock.module("../config/loader.js", () => ({
33
33
 
34
34
  model: "test",
35
35
  provider: "test",
36
- apiKeys: {},
37
36
  memory: { enabled: false },
38
37
  rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
39
38
  }),
@@ -32,7 +32,6 @@ mock.module("../config/loader.js", () => ({
32
32
 
33
33
  model: "test",
34
34
  provider: "test",
35
- apiKeys: {},
36
35
  memory: { enabled: false },
37
36
  rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
38
37
  }),
@@ -40,7 +40,6 @@ mock.module("../config/loader.js", () => ({
40
40
 
41
41
  model: "test",
42
42
  provider: "test",
43
- apiKeys: {},
44
43
  memory: { enabled: false },
45
44
  rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
46
45
  secretDetection: { enabled: false },
@@ -30,7 +30,6 @@ mock.module("../config/loader.js", () => ({
30
30
 
31
31
  model: "test",
32
32
  provider: "test",
33
- apiKeys: {},
34
33
  memory: { enabled: false },
35
34
  rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
36
35
  }),
@@ -27,11 +27,16 @@ const geminiGenerateContentFn = mock(async () => geminiGenerateContentResult);
27
27
 
28
28
  mock.module("../config/loader.js", () => ({
29
29
  getConfig: () => ({
30
- apiKeys: { gemini: mockGeminiKey },
31
30
  imageGenModel: "gemini-2.5-flash-image",
32
31
  }),
33
32
  }));
34
33
 
34
+ mock.module("../security/secure-keys.js", () => ({
35
+ getSecureKeyAsync: async (name: string) =>
36
+ name === "gemini" ? mockGeminiKey : null,
37
+ getSecureKey: () => null,
38
+ }));
39
+
35
40
  mock.module("../util/platform.js", () => ({
36
41
  getWorkspaceDir: () => mockWorkspaceDir,
37
42
  }));
@@ -59,8 +59,11 @@ let mockGetCredentialMetadata: ReturnType<typeof mock>;
59
59
 
60
60
  mock.module("../security/secure-keys.js", () => ({
61
61
  getSecureKey: (...args: unknown[]) => mockGetSecureKey(...args),
62
+ getSecureKeyAsync: async (...args: unknown[]) => mockGetSecureKey(...args),
62
63
  setSecureKey: () => true,
64
+ setSecureKeyAsync: async () => true,
63
65
  deleteSecureKey: () => "deleted",
66
+ deleteSecureKeyAsync: async () => "deleted",
64
67
  listSecureKeys: () => [],
65
68
  getBackendType: () => "encrypted",
66
69
  _resetBackend: () => {},
@@ -54,6 +54,17 @@ mock.module("../daemon/session-tool-setup.js", () => ({
54
54
  buildToolDefinitions: () => MOCK_TOOLS,
55
55
  }));
56
56
 
57
+ const mockCheckIngressForSecrets = mock((content: string) => ({
58
+ blocked: false,
59
+ userNotice: "",
60
+ detectedTypes: [] as string[],
61
+ normalizedContent: content,
62
+ }));
63
+
64
+ mock.module("../security/secret-ingress.js", () => ({
65
+ checkIngressForSecrets: mockCheckIngressForSecrets,
66
+ }));
67
+
57
68
  // ---------------------------------------------------------------------------
58
69
  // Imports (after mocks)
59
70
  // ---------------------------------------------------------------------------
@@ -218,6 +229,34 @@ describe("POST /v1/btw", () => {
218
229
  expect(body.error.message).toContain("content");
219
230
  });
220
231
 
232
+ test("returns 422 when content includes a blocked secret", async () => {
233
+ mockCheckIngressForSecrets.mockReturnValueOnce({
234
+ blocked: true,
235
+ userNotice: "Secret detected",
236
+ detectedTypes: ["api_key"],
237
+ normalizedContent: "sk-test-123",
238
+ });
239
+
240
+ const provider = makeMockProvider();
241
+ const session = makeMockSession(provider);
242
+ const res = await callHandler(
243
+ { conversationKey: "key", content: "sk-test-123" },
244
+ { sendMessageDeps: makeSendMessageDeps(session) },
245
+ );
246
+
247
+ expect(res.status).toBe(422);
248
+ const body = (await res.json()) as {
249
+ accepted: boolean;
250
+ error: string;
251
+ message: string;
252
+ detectedTypes: string[];
253
+ };
254
+ expect(body.accepted).toBe(false);
255
+ expect(body.error).toBe("secret_blocked");
256
+ expect(body.detectedTypes).toEqual(["api_key"]);
257
+ expect(provider.sendMessage).not.toHaveBeenCalled();
258
+ });
259
+
221
260
  // -- Service unavailability (503) --
222
261
 
223
262
  test("returns 503 when sendMessageDeps is unavailable", async () => {
@@ -43,7 +43,6 @@ mock.module("../config/loader.js", () => {
43
43
 
44
44
  provider: "anthropic",
45
45
  providerOrder: ["anthropic"],
46
- apiKeys: { anthropic: "test-key" },
47
46
  calls: {
48
47
  enabled: true,
49
48
  provider: "twilio",
@@ -115,6 +115,7 @@ mock.module("../calls/twilio-provider.js", () => ({
115
115
 
116
116
  mock.module("../security/secure-keys.js", () => ({
117
117
  getSecureKey: () => null,
118
+ getSecureKeyAsync: async () => null,
118
119
  }));
119
120
 
120
121
  mock.module("../config/loader.js", () => ({
@@ -53,7 +53,6 @@ mock.module("../config/loader.js", () => ({
53
53
 
54
54
  model: "test",
55
55
  provider: "test",
56
- apiKeys: {},
57
56
  memory: { enabled: false },
58
57
  rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
59
58
  secretDetection: { enabled: false },
@@ -62,7 +61,6 @@ mock.module("../config/loader.js", () => ({
62
61
  loadConfig: () => ({
63
62
  model: "test",
64
63
  provider: "test",
65
- apiKeys: {},
66
64
  memory: { enabled: false },
67
65
  rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
68
66
  secretDetection: { enabled: false },
@@ -106,6 +104,7 @@ mock.module("../calls/twilio-config.js", () => ({
106
104
  // Mock secure keys
107
105
  mock.module("../security/secure-keys.js", () => ({
108
106
  getSecureKey: () => null,
107
+ getSecureKeyAsync: async () => null,
109
108
  }));
110
109
 
111
110
  mock.module("../calls/voice-ingress-preflight.js", () => ({
@@ -761,7 +761,7 @@ describe("canonical-guardian-store", () => {
761
761
 
762
762
  // ── expireAllPendingCanonicalRequests ───────────────────────────────
763
763
 
764
- test("expireAllPendingCanonicalRequests transitions all pending to expired", () => {
764
+ test("expireAllPendingCanonicalRequests transitions interaction-bound pending to expired", () => {
765
765
  const req1 = createCanonicalGuardianRequest({
766
766
  kind: "tool_approval",
767
767
  sourceType: "desktop",
@@ -770,7 +770,7 @@ describe("canonical-guardian-store", () => {
770
770
  expiresAt: new Date(Date.now() + 60_000).toISOString(),
771
771
  });
772
772
  const req2 = createCanonicalGuardianRequest({
773
- kind: "tool_approval",
773
+ kind: "pending_question",
774
774
  sourceType: "channel",
775
775
  conversationId: "conv-bulk-2",
776
776
  guardianPrincipalId: TEST_PRINCIPAL,
@@ -784,6 +784,37 @@ describe("canonical-guardian-store", () => {
784
784
  expect(getCanonicalGuardianRequest(req2.id)!.status).toBe("expired");
785
785
  });
786
786
 
787
+ test("expireAllPendingCanonicalRequests does not expire persistent kinds (access_request, tool_grant_request)", () => {
788
+ const accessReq = createCanonicalGuardianRequest({
789
+ kind: "access_request",
790
+ sourceType: "channel",
791
+ conversationId: "conv-bulk-persist-1",
792
+ guardianPrincipalId: TEST_PRINCIPAL,
793
+ });
794
+ const grantReq = createCanonicalGuardianRequest({
795
+ kind: "tool_grant_request",
796
+ sourceType: "channel",
797
+ conversationId: "conv-bulk-persist-2",
798
+ guardianPrincipalId: TEST_PRINCIPAL,
799
+ });
800
+ // Also create an interaction-bound request to verify selective expiry
801
+ const toolApproval = createCanonicalGuardianRequest({
802
+ kind: "tool_approval",
803
+ sourceType: "desktop",
804
+ conversationId: "conv-bulk-persist-3",
805
+ guardianPrincipalId: TEST_PRINCIPAL,
806
+ });
807
+
808
+ const count = expireAllPendingCanonicalRequests();
809
+ expect(count).toBe(1); // Only tool_approval expired
810
+
811
+ expect(getCanonicalGuardianRequest(accessReq.id)!.status).toBe("pending");
812
+ expect(getCanonicalGuardianRequest(grantReq.id)!.status).toBe("pending");
813
+ expect(getCanonicalGuardianRequest(toolApproval.id)!.status).toBe(
814
+ "expired",
815
+ );
816
+ });
817
+
787
818
  test("expireAllPendingCanonicalRequests does not affect already-resolved requests", () => {
788
819
  const approved = createCanonicalGuardianRequest({
789
820
  kind: "tool_approval",
@@ -49,6 +49,7 @@ mock.module("../config/loader.js", () => ({
49
49
 
50
50
  mock.module("../security/secure-keys.js", () => ({
51
51
  getSecureKey: (key: string) => mockSecureKeys[key] ?? null,
52
+ getSecureKeyAsync: async (key: string) => mockSecureKeys[key] ?? null,
52
53
  }));
53
54
 
54
55
  mock.module("../email/service.js", () => ({
@@ -49,6 +49,7 @@ mock.module("../email/service.js", () => ({
49
49
 
50
50
  mock.module("../security/secure-keys.js", () => ({
51
51
  getSecureKey: (key: string) => mockSecureKeys[key] ?? null,
52
+ getSecureKeyAsync: async (key: string) => mockSecureKeys[key] ?? null,
52
53
  }));
53
54
 
54
55
  mock.module("../runtime/channel-invite-transports/whatsapp.js", () => ({
@@ -29,11 +29,15 @@ mock.module("../util/logger.js", () => ({
29
29
  mock.module("../config/loader.js", () => ({
30
30
  getConfig: () => ({
31
31
  ui: {},
32
-
33
- apiKeys: { anthropic: "test-key" },
34
32
  }),
35
33
  }));
36
34
 
35
+ mock.module("../security/secure-keys.js", () => ({
36
+ getSecureKeyAsync: async (name: string) =>
37
+ name === "anthropic" ? "fake-anthropic-key" : null,
38
+ getSecureKey: () => null,
39
+ }));
40
+
37
41
  // ---------------------------------------------------------------------------
38
42
  // Imports (after mocks)
39
43
  // ---------------------------------------------------------------------------
@@ -33,11 +33,16 @@ mock.module("../util/logger.js", () => ({
33
33
  mock.module("../config/loader.js", () => ({
34
34
  getConfig: () => ({
35
35
  ui: {},
36
-
37
- apiKeys: { anthropic: "test-key" },
38
36
  }),
39
37
  }));
40
38
 
39
+ // Mock secure-keys — provide a fake Anthropic API key
40
+ mock.module("../security/secure-keys.js", () => ({
41
+ getSecureKeyAsync: async (name: string) =>
42
+ name === "anthropic" ? "fake-anthropic-key" : null,
43
+ getSecureKey: () => null,
44
+ }));
45
+
41
46
  import { claudeCodeTool } from "../tools/claude-code/claude-code.js";
42
47
  import type { ToolContext } from "../tools/types.js";
43
48
 
@@ -278,13 +278,12 @@ describe("config loader backfill", () => {
278
278
  expect(contentAfter).toBe(contentBefore);
279
279
  });
280
280
 
281
- test("does not write apiKeys or dataDir during backfill", () => {
281
+ test("does not write dataDir during backfill", () => {
282
282
  writeConfig({ provider: "anthropic" });
283
283
 
284
284
  loadConfig();
285
285
 
286
286
  const raw = readConfig();
287
- expect(raw.apiKeys).toBeUndefined();
288
287
  expect(raw.dataDir).toBeUndefined();
289
288
  });
290
289
 
@@ -67,7 +67,10 @@ import {
67
67
  resolveVoiceQualityProfile,
68
68
  } from "../calls/voice-quality.js";
69
69
  import { invalidateConfigCache, loadConfig } from "../config/loader.js";
70
- import { AssistantConfigSchema } from "../config/schema.js";
70
+ import {
71
+ AssistantConfigSchema,
72
+ DEFAULT_ELEVENLABS_VOICE_ID,
73
+ } from "../config/schema.js";
71
74
  import { _setStorePath } from "../security/encrypted-store.js";
72
75
  import { _setBackend } from "../security/secure-keys.js";
73
76
 
@@ -89,7 +92,6 @@ describe("AssistantConfigSchema", () => {
89
92
  expect(result.provider).toBe("anthropic");
90
93
  expect(result.model).toBe("claude-opus-4-6");
91
94
  expect(result.maxTokens).toBe(16000);
92
- expect(result.apiKeys).toEqual({});
93
95
  expect(result.thinking).toEqual({
94
96
  enabled: false,
95
97
  streamThinking: false,
@@ -137,7 +139,6 @@ describe("AssistantConfigSchema", () => {
137
139
  provider: "openai",
138
140
  model: "gpt-4",
139
141
  maxTokens: 4096,
140
- apiKeys: { openai: "sk-test" },
141
142
  thinking: { enabled: true },
142
143
  timeouts: {
143
144
  shellDefaultTimeoutSec: 30,
@@ -358,13 +359,6 @@ describe("AssistantConfigSchema", () => {
358
359
  expect(result.success).toBe(false);
359
360
  });
360
361
 
361
- test("rejects non-string apiKeys values", () => {
362
- const result = AssistantConfigSchema.safeParse({
363
- apiKeys: { anthropic: 123 },
364
- });
365
- expect(result.success).toBe(false);
366
- });
367
-
368
362
  test("accepts partial nested objects with defaults", () => {
369
363
  const result = AssistantConfigSchema.parse({
370
364
  timeouts: { shellDefaultTimeoutSec: 30 },
@@ -906,10 +900,10 @@ describe("resolveVoiceQualityProfile", () => {
906
900
  expect(profile.voice).toBe("test-voice-id");
907
901
  });
908
902
 
909
- test("defaults to Rachel voice ID when elevenlabs.voiceId is not set", () => {
903
+ test("defaults to Amelia voice ID when elevenlabs.voiceId is not set", () => {
910
904
  const config = AssistantConfigSchema.parse({});
911
905
  const profile = resolveVoiceQualityProfile(config);
912
- expect(profile.voice).toBe("21m00Tcm4TlvDq8ikWAM");
906
+ expect(profile.voice).toBe(DEFAULT_ELEVENLABS_VOICE_ID);
913
907
  });
914
908
 
915
909
  test("applies voice tuning params from elevenlabs config", () => {
@@ -1165,31 +1159,6 @@ describe("loadConfig with schema validation", () => {
1165
1159
  expect(config.permissions.mode).toBe("workspace");
1166
1160
  });
1167
1161
 
1168
- test("does not mutate default apiKeys when fallback config is overridden by env keys", () => {
1169
- const originalAnthropicApiKey = process.env.ANTHROPIC_API_KEY;
1170
- try {
1171
- const testKey = ["test", "in", "memory", "default", "leak"].join("-");
1172
- process.env.ANTHROPIC_API_KEY = testKey;
1173
- writeConfig("this is not a config object");
1174
-
1175
- const configWithEnv = loadConfig();
1176
- expect(configWithEnv.apiKeys.anthropic).toBe(testKey);
1177
-
1178
- invalidateConfigCache();
1179
- delete process.env.ANTHROPIC_API_KEY;
1180
- writeConfig("still not a config object");
1181
-
1182
- const configWithoutEnv = loadConfig();
1183
- expect(configWithoutEnv.apiKeys.anthropic).toBeUndefined();
1184
- } finally {
1185
- if (originalAnthropicApiKey !== undefined) {
1186
- process.env.ANTHROPIC_API_KEY = originalAnthropicApiKey;
1187
- } else {
1188
- delete process.env.ANTHROPIC_API_KEY;
1189
- }
1190
- }
1191
- });
1192
-
1193
1162
  // ── Calls config (loader integration) ──────────────────────────────
1194
1163
 
1195
1164
  test("loads calls config from file", () => {
@@ -30,7 +30,6 @@ mock.module("../config/loader.js", () => ({
30
30
  ui: {},
31
31
  model: "claude-opus-4-6",
32
32
  provider: "anthropic",
33
- apiKeys: { anthropic: "test-key" },
34
33
  memory: { enabled: false },
35
34
  rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
36
35
  secretDetection: { enabled: false },
@@ -371,7 +371,7 @@ describe("CredentialBroker.serverUse", () => {
371
371
  expect(r2.result).toBe("triggered");
372
372
  });
373
373
 
374
- test("different services with same field name are independent (serverUseById)", () => {
374
+ test("different services with same field name are independent (serverUseById)", async () => {
375
375
  const meta1 = upsertCredentialMetadata("github", "api_token", {
376
376
  allowedTools: ["github_api"],
377
377
  });
@@ -382,7 +382,7 @@ describe("CredentialBroker.serverUse", () => {
382
382
  setSecureKey(credentialKey("gitlab", "api_token"), "gl_tok");
383
383
 
384
384
  // github credential should not serve gitlab tool
385
- const r1 = broker.serverUseById({
385
+ const r1 = await broker.serverUseById({
386
386
  credentialId: meta1.credentialId,
387
387
  requestingTool: "gitlab_api",
388
388
  });
@@ -447,7 +447,7 @@ describe("CredentialBroker.serverUseById", () => {
447
447
  rmSync(TEST_DIR, { recursive: true, force: true });
448
448
  });
449
449
 
450
- test("returns metadata and injection templates for valid credential", () => {
450
+ test("returns metadata and injection templates for valid credential", async () => {
451
451
  const meta = upsertCredentialMetadata("fal", "api_key", {
452
452
  allowedTools: ["media_proxy"],
453
453
  injectionTemplates: [
@@ -461,7 +461,7 @@ describe("CredentialBroker.serverUseById", () => {
461
461
  });
462
462
  setSecureKey(credentialKey("fal", "api_key"), "fal-secret-key");
463
463
 
464
- const result = broker.serverUseById({
464
+ const result = await broker.serverUseById({
465
465
  credentialId: meta.credentialId,
466
466
  requestingTool: "media_proxy",
467
467
  });
@@ -480,13 +480,13 @@ describe("CredentialBroker.serverUseById", () => {
480
480
  expect(serialized).not.toContain("fal-secret-key");
481
481
  });
482
482
 
483
- test("denies when requesting tool is not in allowed list", () => {
483
+ test("denies when requesting tool is not in allowed list", async () => {
484
484
  const meta = upsertCredentialMetadata("fal", "api_key", {
485
485
  allowedTools: ["media_proxy"],
486
486
  });
487
487
  setSecureKey(credentialKey("fal", "api_key"), "fal-secret-key");
488
488
 
489
- const result = broker.serverUseById({
489
+ const result = await broker.serverUseById({
490
490
  credentialId: meta.credentialId,
491
491
  requestingTool: "unauthorized_tool",
492
492
  });
@@ -498,8 +498,8 @@ describe("CredentialBroker.serverUseById", () => {
498
498
  expect(result.reason).toContain("media_proxy");
499
499
  });
500
500
 
501
- test("returns not found for unknown credential ID", () => {
502
- const result = broker.serverUseById({
501
+ test("returns not found for unknown credential ID", async () => {
502
+ const result = await broker.serverUseById({
503
503
  credentialId: "nonexistent-id",
504
504
  requestingTool: "media_proxy",
505
505
  });
@@ -510,14 +510,14 @@ describe("CredentialBroker.serverUseById", () => {
510
510
  expect(result.reason).toContain("nonexistent-id");
511
511
  });
512
512
 
513
- test("denies when credential has domain restrictions", () => {
513
+ test("denies when credential has domain restrictions", async () => {
514
514
  const meta = upsertCredentialMetadata("github", "oauth_token", {
515
515
  allowedTools: ["media_proxy"],
516
516
  allowedDomains: ["github.com"],
517
517
  });
518
518
  setSecureKey(credentialKey("github", "oauth_token"), "gho_test");
519
519
 
520
- const result = broker.serverUseById({
520
+ const result = await broker.serverUseById({
521
521
  credentialId: meta.credentialId,
522
522
  requestingTool: "media_proxy",
523
523
  });
@@ -528,13 +528,13 @@ describe("CredentialBroker.serverUseById", () => {
528
528
  expect(result.reason).toContain("cannot be used server-side");
529
529
  });
530
530
 
531
- test("returns empty injection templates when credential has none", () => {
531
+ test("returns empty injection templates when credential has none", async () => {
532
532
  const meta = upsertCredentialMetadata("vercel", "api_token", {
533
533
  allowedTools: ["media_proxy"],
534
534
  });
535
535
  setSecureKey(credentialKey("vercel", "api_token"), "test-vercel-token");
536
536
 
537
- const result = broker.serverUseById({
537
+ const result = await broker.serverUseById({
538
538
  credentialId: meta.credentialId,
539
539
  requestingTool: "media_proxy",
540
540
  });
@@ -544,13 +544,13 @@ describe("CredentialBroker.serverUseById", () => {
544
544
  expect(result.injectionTemplates).toEqual([]);
545
545
  });
546
546
 
547
- test("denies with empty allowedTools and suggests updating credential", () => {
547
+ test("denies with empty allowedTools and suggests updating credential", async () => {
548
548
  const meta = upsertCredentialMetadata("stripe", "secret_key", {
549
549
  allowedTools: [],
550
550
  });
551
551
  setSecureKey(credentialKey("stripe", "secret_key"), "sk_test_xyz");
552
552
 
553
- const result = broker.serverUseById({
553
+ const result = await broker.serverUseById({
554
554
  credentialId: meta.credentialId,
555
555
  requestingTool: "media_proxy",
556
556
  });
@@ -561,7 +561,7 @@ describe("CredentialBroker.serverUseById", () => {
561
561
  expect(result.reason).toContain("credential_store");
562
562
  });
563
563
 
564
- test("denies when metadata exists but no stored secret value", () => {
564
+ test("denies when metadata exists but no stored secret value", async () => {
565
565
  const meta = upsertCredentialMetadata("fal", "api_key", {
566
566
  allowedTools: ["media_proxy"],
567
567
  injectionTemplates: [
@@ -575,7 +575,7 @@ describe("CredentialBroker.serverUseById", () => {
575
575
  });
576
576
  // No setSecureKey — metadata exists but value doesn't
577
577
 
578
- const result = broker.serverUseById({
578
+ const result = await broker.serverUseById({
579
579
  credentialId: meta.credentialId,
580
580
  requestingTool: "media_proxy",
581
581
  });