@vellumai/assistant 0.5.6 → 0.5.7

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 (305) hide show
  1. package/.env.example +16 -2
  2. package/ARCHITECTURE.md +6 -75
  3. package/Dockerfile +1 -1
  4. package/README.md +0 -2
  5. package/bun.lock +0 -414
  6. package/docs/architecture/keychain-broker.md +45 -240
  7. package/docs/architecture/security.md +0 -17
  8. package/docs/credential-execution-service.md +2 -2
  9. package/node_modules/@vellumai/ces-contracts/package.json +1 -0
  10. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +119 -0
  11. package/node_modules/@vellumai/credential-storage/package.json +1 -0
  12. package/node_modules/@vellumai/egress-proxy/package.json +1 -0
  13. package/package.json +2 -3
  14. package/src/__tests__/actor-token-service.test.ts +0 -114
  15. package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
  16. package/src/__tests__/browser-skill-endstate.test.ts +6 -5
  17. package/src/__tests__/btw-routes.test.ts +0 -39
  18. package/src/__tests__/call-domain.test.ts +0 -128
  19. package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +0 -5
  21. package/src/__tests__/channel-readiness-service.test.ts +1 -60
  22. package/src/__tests__/checker.test.ts +4 -2
  23. package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
  24. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  25. package/src/__tests__/config-schema.test.ts +1 -1
  26. package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
  27. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  28. package/src/__tests__/conversation-skill-tools.test.ts +0 -54
  29. package/src/__tests__/conversation-title-service.test.ts +87 -0
  30. package/src/__tests__/credential-execution-feature-gates.test.ts +28 -14
  31. package/src/__tests__/credential-execution-managed-contract.test.ts +33 -18
  32. package/src/__tests__/credential-security-e2e.test.ts +0 -66
  33. package/src/__tests__/credential-security-invariants.test.ts +4 -45
  34. package/src/__tests__/credentials-cli.test.ts +78 -0
  35. package/src/__tests__/db-migration-rollback.test.ts +2015 -1
  36. package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
  37. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
  38. package/src/__tests__/guardian-routing-state.test.ts +0 -5
  39. package/src/__tests__/host-shell-tool.test.ts +6 -7
  40. package/src/__tests__/http-user-message-parity.test.ts +3 -103
  41. package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
  42. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
  43. package/src/__tests__/intent-routing.test.ts +0 -13
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
  45. package/src/__tests__/keychain-broker-client.test.ts +161 -22
  46. package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
  47. package/src/__tests__/migration-export-http.test.ts +2 -2
  48. package/src/__tests__/migration-import-commit-http.test.ts +2 -2
  49. package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
  50. package/src/__tests__/migration-validate-http.test.ts +2 -2
  51. package/src/__tests__/non-member-access-request.test.ts +0 -5
  52. package/src/__tests__/notification-decision-fallback.test.ts +4 -0
  53. package/src/__tests__/notification-decision-identity.test.ts +4 -0
  54. package/src/__tests__/permission-types.test.ts +1 -0
  55. package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
  56. package/src/__tests__/qdrant-manager.test.ts +28 -2
  57. package/src/__tests__/registry.test.ts +0 -6
  58. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
  59. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
  60. package/src/__tests__/secure-keys.test.ts +83 -263
  61. package/src/__tests__/shell-identity.test.ts +96 -6
  62. package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
  63. package/src/__tests__/skill-feature-flags.test.ts +46 -45
  64. package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
  65. package/src/__tests__/skill-load-inline-command.test.ts +8 -12
  66. package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
  67. package/src/__tests__/skill-load-tool.test.ts +0 -2
  68. package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
  69. package/src/__tests__/skills.test.ts +0 -2
  70. package/src/__tests__/slack-inbound-verification.test.ts +0 -4
  71. package/src/__tests__/suggestion-routes.test.ts +1 -32
  72. package/src/__tests__/system-prompt.test.ts +0 -1
  73. package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
  74. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
  75. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
  76. package/src/__tests__/update-bulletin.test.ts +0 -2
  77. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
  78. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -6
  79. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
  80. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +218 -0
  81. package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
  82. package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
  83. package/src/calls/audio-store.test.ts +97 -0
  84. package/src/calls/audio-store.ts +205 -0
  85. package/src/calls/call-controller.ts +85 -7
  86. package/src/calls/call-domain.ts +3 -0
  87. package/src/calls/call-store.ts +10 -3
  88. package/src/calls/fish-audio-client.ts +117 -0
  89. package/src/calls/relay-server.ts +27 -0
  90. package/src/calls/twilio-routes.ts +2 -1
  91. package/src/calls/types.ts +1 -0
  92. package/src/calls/voice-ingress-preflight.ts +0 -42
  93. package/src/calls/voice-quality.ts +26 -5
  94. package/src/calls/voice-session-bridge.ts +6 -12
  95. package/src/cli/commands/config.ts +1 -4
  96. package/src/cli/commands/credentials.ts +34 -4
  97. package/src/cli/commands/oauth/index.ts +7 -0
  98. package/src/cli/commands/oauth/platform.ts +179 -0
  99. package/src/cli/commands/platform.ts +3 -3
  100. package/src/config/assistant-feature-flags.ts +186 -5
  101. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  102. package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
  103. package/src/config/bundled-skills/settings/TOOLS.json +2 -2
  104. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
  105. package/src/config/bundled-tool-registry.ts +1 -11
  106. package/src/config/env-registry.ts +1 -1
  107. package/src/config/env.ts +8 -14
  108. package/src/config/feature-flag-registry.json +48 -8
  109. package/src/config/loader.ts +98 -31
  110. package/src/config/schema.ts +4 -13
  111. package/src/config/schemas/calls.ts +13 -0
  112. package/src/config/schemas/fish-audio.ts +39 -0
  113. package/src/config/schemas/security.ts +0 -4
  114. package/src/config/types.ts +0 -1
  115. package/src/contacts/contact-store.ts +39 -0
  116. package/src/contacts/types.ts +2 -0
  117. package/src/credential-execution/approval-bridge.ts +1 -0
  118. package/src/credential-execution/executable-discovery.ts +28 -4
  119. package/src/credential-execution/feature-gates.ts +16 -0
  120. package/src/credential-execution/process-manager.ts +38 -0
  121. package/src/daemon/assistant-attachments.ts +9 -0
  122. package/src/daemon/config-watcher.ts +5 -0
  123. package/src/daemon/conversation-tool-setup.ts +0 -105
  124. package/src/daemon/conversation.ts +10 -1
  125. package/src/daemon/handlers/config-vercel.ts +92 -0
  126. package/src/daemon/handlers/skills.ts +2 -15
  127. package/src/daemon/install-symlink.ts +195 -0
  128. package/src/daemon/lifecycle.ts +227 -51
  129. package/src/daemon/message-types/conversations.ts +3 -4
  130. package/src/daemon/message-types/diagnostics.ts +3 -22
  131. package/src/daemon/message-types/messages.ts +0 -2
  132. package/src/daemon/message-types/upgrades.ts +8 -0
  133. package/src/daemon/server.ts +30 -92
  134. package/src/events/domain-events.ts +2 -1
  135. package/src/inbound/platform-callback-registration.ts +3 -3
  136. package/src/instrument.ts +8 -5
  137. package/src/memory/conversation-title-service.ts +50 -1
  138. package/src/memory/db-init.ts +12 -0
  139. package/src/memory/items-extractor.ts +15 -1
  140. package/src/memory/job-handlers/conversation-starters.ts +4 -1
  141. package/src/memory/jobs-store.ts +30 -5
  142. package/src/memory/jobs-worker.ts +31 -7
  143. package/src/memory/migrations/001-job-deferrals.ts +19 -0
  144. package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
  145. package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
  146. package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
  147. package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
  148. package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
  149. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
  150. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
  151. package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
  152. package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
  153. package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
  154. package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
  155. package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
  156. package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
  157. package/src/memory/migrations/116-messages-fts.ts +106 -1
  158. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
  159. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
  160. package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
  161. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
  162. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
  163. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
  164. package/src/memory/migrations/141-rename-verification-table.ts +54 -0
  165. package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
  166. package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
  167. package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
  168. package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
  169. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
  170. package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
  171. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
  172. package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
  173. package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
  174. package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
  175. package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
  176. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
  177. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
  178. package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
  179. package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
  180. package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
  181. package/src/memory/migrations/index.ts +4 -0
  182. package/src/memory/migrations/registry.ts +90 -0
  183. package/src/memory/migrations/validate-migration-state.ts +137 -11
  184. package/src/memory/qdrant-circuit-breaker.ts +9 -0
  185. package/src/memory/qdrant-manager.ts +64 -7
  186. package/src/memory/schema/calls.ts +1 -0
  187. package/src/memory/schema/contacts.ts +1 -0
  188. package/src/notifications/decision-engine.ts +4 -1
  189. package/src/oauth/connection-resolver.ts +6 -4
  190. package/src/permissions/checker.ts +0 -38
  191. package/src/permissions/shell-identity.ts +76 -22
  192. package/src/permissions/types.ts +4 -2
  193. package/src/platform/client.ts +35 -7
  194. package/src/prompts/persona-resolver.ts +138 -0
  195. package/src/prompts/system-prompt.ts +36 -4
  196. package/src/prompts/templates/users/default.md +1 -0
  197. package/src/providers/registry.ts +27 -40
  198. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
  199. package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
  200. package/src/runtime/auth/external-assistant-id.ts +13 -59
  201. package/src/runtime/auth/route-policy.ts +15 -1
  202. package/src/runtime/auth/token-service.ts +43 -138
  203. package/src/runtime/channel-readiness-service.ts +1 -16
  204. package/src/runtime/http-server.ts +27 -2
  205. package/src/runtime/middleware/error-handler.ts +1 -9
  206. package/src/runtime/routes/audio-routes.ts +40 -0
  207. package/src/runtime/routes/btw-routes.ts +0 -17
  208. package/src/runtime/routes/conversation-query-routes.ts +63 -1
  209. package/src/runtime/routes/conversation-routes.ts +4 -44
  210. package/src/runtime/routes/diagnostics-routes.ts +1 -477
  211. package/src/runtime/routes/identity-routes.ts +18 -29
  212. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
  213. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
  214. package/src/runtime/routes/integrations/vercel.ts +89 -0
  215. package/src/runtime/routes/log-export-routes.ts +5 -0
  216. package/src/runtime/routes/memory-item-routes.ts +24 -6
  217. package/src/runtime/routes/migration-rollback-routes.ts +209 -0
  218. package/src/runtime/routes/migration-routes.ts +17 -1
  219. package/src/runtime/routes/notification-routes.ts +58 -0
  220. package/src/runtime/routes/schedule-routes.ts +65 -0
  221. package/src/runtime/routes/settings-routes.ts +41 -1
  222. package/src/runtime/routes/tts-routes.ts +86 -0
  223. package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
  224. package/src/runtime/routes/workspace-commit-routes.ts +62 -0
  225. package/src/runtime/routes/workspace-routes.test.ts +22 -1
  226. package/src/runtime/routes/workspace-routes.ts +1 -1
  227. package/src/runtime/routes/workspace-utils.ts +86 -2
  228. package/src/security/ces-credential-client.ts +59 -22
  229. package/src/security/ces-rpc-credential-backend.ts +85 -0
  230. package/src/security/credential-backend.ts +12 -88
  231. package/src/security/keychain-broker-client.ts +10 -2
  232. package/src/security/secure-keys.ts +94 -113
  233. package/src/skills/catalog-install.ts +13 -7
  234. package/src/telemetry/usage-telemetry-reporter.ts +4 -2
  235. package/src/tools/calls/call-start.ts +1 -0
  236. package/src/tools/executor.ts +0 -4
  237. package/src/tools/network/script-proxy/session-manager.ts +19 -4
  238. package/src/tools/network/web-fetch.ts +3 -1
  239. package/src/tools/skills/execute.ts +1 -1
  240. package/src/tools/types.ts +0 -8
  241. package/src/util/errors.ts +0 -12
  242. package/src/util/platform.ts +3 -50
  243. package/src/workspace/git-service.ts +5 -2
  244. package/src/workspace/migrations/001-avatar-rename.ts +15 -0
  245. package/src/workspace/migrations/003-seed-device-id.ts +17 -1
  246. package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
  247. package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
  248. package/src/workspace/migrations/006-services-config.ts +49 -0
  249. package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
  250. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
  251. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
  252. package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
  253. package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
  254. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
  255. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
  256. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
  257. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
  258. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
  259. package/src/workspace/migrations/017-seed-persona-dirs.ts +95 -0
  260. package/src/workspace/migrations/migrate-to-workspace-volume.ts +23 -1
  261. package/src/workspace/migrations/registry.ts +8 -0
  262. package/src/workspace/migrations/runner.ts +106 -2
  263. package/src/workspace/migrations/types.ts +4 -0
  264. package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
  265. package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
  266. package/src/__tests__/diagnostics-export.test.ts +0 -288
  267. package/src/__tests__/local-gateway-health.test.ts +0 -209
  268. package/src/__tests__/secret-ingress-handler.test.ts +0 -120
  269. package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
  270. package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
  271. package/src/__tests__/swarm-orchestrator.test.ts +0 -463
  272. package/src/__tests__/swarm-plan-validator.test.ts +0 -384
  273. package/src/__tests__/swarm-recursion.test.ts +0 -197
  274. package/src/__tests__/swarm-router-planner.test.ts +0 -234
  275. package/src/__tests__/swarm-tool.test.ts +0 -185
  276. package/src/__tests__/swarm-worker-backend.test.ts +0 -144
  277. package/src/__tests__/swarm-worker-runner.test.ts +0 -288
  278. package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
  279. package/src/commands/cc-command-registry.ts +0 -248
  280. package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
  281. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
  282. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
  283. package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
  284. package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
  285. package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
  286. package/src/config/schemas/swarm.ts +0 -82
  287. package/src/logfire.ts +0 -135
  288. package/src/runtime/local-gateway-health.ts +0 -275
  289. package/src/security/secret-ingress.ts +0 -68
  290. package/src/swarm/backend-claude-code.ts +0 -225
  291. package/src/swarm/checkpoint.ts +0 -137
  292. package/src/swarm/graph-utils.ts +0 -53
  293. package/src/swarm/index.ts +0 -55
  294. package/src/swarm/limits.ts +0 -66
  295. package/src/swarm/orchestrator.ts +0 -424
  296. package/src/swarm/plan-validator.ts +0 -117
  297. package/src/swarm/router-planner.ts +0 -162
  298. package/src/swarm/router-prompts.ts +0 -39
  299. package/src/swarm/synthesizer.ts +0 -81
  300. package/src/swarm/types.ts +0 -72
  301. package/src/swarm/worker-backend.ts +0 -131
  302. package/src/swarm/worker-prompts.ts +0 -80
  303. package/src/swarm/worker-runner.ts +0 -170
  304. package/src/tools/claude-code/claude-code.ts +0 -610
  305. package/src/tools/swarm/delegate.ts +0 -205
@@ -0,0 +1,199 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import { CesRpcMethod } from "@vellumai/ces-contracts";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Mock state
7
+ // ---------------------------------------------------------------------------
8
+
9
+ const callFn = mock(
10
+ async (_method: string, _request: unknown): Promise<unknown> => ({}),
11
+ );
12
+
13
+ const isReadyFn = mock((): boolean => true);
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Mock modules — before importing module under test
17
+ // ---------------------------------------------------------------------------
18
+
19
+ mock.module("../util/logger.js", () => ({
20
+ getLogger: () =>
21
+ new Proxy({} as Record<string, unknown>, {
22
+ get: () => () => {},
23
+ }),
24
+ }));
25
+
26
+ // Import after mocking
27
+ import type { CesClient } from "../credential-execution/client.js";
28
+ import { CesRpcCredentialBackend } from "../security/ces-rpc-credential-backend.js";
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Helpers
32
+ // ---------------------------------------------------------------------------
33
+
34
+ function createMockClient(): CesClient {
35
+ return {
36
+ handshake: mock(async () => ({ accepted: true })),
37
+ call: callFn as CesClient["call"],
38
+ updateAssistantApiKey: mock(async () => ({ updated: true })),
39
+ isReady: isReadyFn,
40
+ close: mock(() => {}),
41
+ };
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Tests
46
+ // ---------------------------------------------------------------------------
47
+
48
+ describe("CesRpcCredentialBackend", () => {
49
+ let client: CesClient;
50
+ let backend: CesRpcCredentialBackend;
51
+
52
+ beforeEach(() => {
53
+ callFn.mockClear();
54
+ isReadyFn.mockClear();
55
+
56
+ isReadyFn.mockReturnValue(true);
57
+
58
+ client = createMockClient();
59
+ backend = new CesRpcCredentialBackend(client);
60
+ });
61
+
62
+ test("has name 'ces-rpc'", () => {
63
+ expect(backend.name).toBe("ces-rpc");
64
+ });
65
+
66
+ // -------------------------------------------------------------------------
67
+ // isAvailable
68
+ // -------------------------------------------------------------------------
69
+
70
+ describe("isAvailable", () => {
71
+ test("returns true when client is ready", () => {
72
+ isReadyFn.mockReturnValue(true);
73
+ expect(backend.isAvailable()).toBe(true);
74
+ });
75
+
76
+ test("returns false when client is not ready", () => {
77
+ isReadyFn.mockReturnValue(false);
78
+ expect(backend.isAvailable()).toBe(false);
79
+ });
80
+ });
81
+
82
+ // -------------------------------------------------------------------------
83
+ // get
84
+ // -------------------------------------------------------------------------
85
+
86
+ describe("get", () => {
87
+ test("delegates to CesRpcMethod.GetCredential and returns value when found", async () => {
88
+ callFn.mockResolvedValue({ found: true, value: "my-secret" });
89
+
90
+ const result = await backend.get("test-account");
91
+
92
+ expect(callFn).toHaveBeenCalledWith(CesRpcMethod.GetCredential, {
93
+ account: "test-account",
94
+ });
95
+ expect(result).toEqual({ value: "my-secret", unreachable: false });
96
+ });
97
+
98
+ test("returns undefined value when credential not found", async () => {
99
+ callFn.mockResolvedValue({ found: false });
100
+
101
+ const result = await backend.get("missing-account");
102
+
103
+ expect(callFn).toHaveBeenCalledWith(CesRpcMethod.GetCredential, {
104
+ account: "missing-account",
105
+ });
106
+ expect(result).toEqual({ value: undefined, unreachable: false });
107
+ });
108
+
109
+ test("returns unreachable when RPC call throws", async () => {
110
+ callFn.mockRejectedValue(new Error("transport error"));
111
+
112
+ const result = await backend.get("broken-account");
113
+
114
+ expect(result).toEqual({ value: undefined, unreachable: true });
115
+ });
116
+ });
117
+
118
+ // -------------------------------------------------------------------------
119
+ // set
120
+ // -------------------------------------------------------------------------
121
+
122
+ describe("set", () => {
123
+ test("delegates to CesRpcMethod.SetCredential and returns true on success", async () => {
124
+ callFn.mockResolvedValue({ ok: true });
125
+
126
+ const result = await backend.set("test-account", "new-secret");
127
+
128
+ expect(callFn).toHaveBeenCalledWith(CesRpcMethod.SetCredential, {
129
+ account: "test-account",
130
+ value: "new-secret",
131
+ });
132
+ expect(result).toBe(true);
133
+ });
134
+
135
+ test("returns false when RPC call throws", async () => {
136
+ callFn.mockRejectedValue(new Error("transport error"));
137
+
138
+ const result = await backend.set("test-account", "new-secret");
139
+
140
+ expect(result).toBe(false);
141
+ });
142
+ });
143
+
144
+ // -------------------------------------------------------------------------
145
+ // delete
146
+ // -------------------------------------------------------------------------
147
+
148
+ describe("delete", () => {
149
+ test("delegates to CesRpcMethod.DeleteCredential and returns the result", async () => {
150
+ callFn.mockResolvedValue({ result: "deleted" });
151
+
152
+ const result = await backend.delete("test-account");
153
+
154
+ expect(callFn).toHaveBeenCalledWith(CesRpcMethod.DeleteCredential, {
155
+ account: "test-account",
156
+ });
157
+ expect(result).toBe("deleted");
158
+ });
159
+
160
+ test("returns not-found result from CES", async () => {
161
+ callFn.mockResolvedValue({ result: "not-found" });
162
+
163
+ const result = await backend.delete("nonexistent-account");
164
+
165
+ expect(result).toBe("not-found");
166
+ });
167
+
168
+ test("returns 'error' when RPC call throws", async () => {
169
+ callFn.mockRejectedValue(new Error("transport error"));
170
+
171
+ const result = await backend.delete("test-account");
172
+
173
+ expect(result).toBe("error");
174
+ });
175
+ });
176
+
177
+ // -------------------------------------------------------------------------
178
+ // list
179
+ // -------------------------------------------------------------------------
180
+
181
+ describe("list", () => {
182
+ test("delegates to CesRpcMethod.ListCredentials and returns accounts", async () => {
183
+ callFn.mockResolvedValue({ accounts: ["account-a", "account-b"] });
184
+
185
+ const result = await backend.list();
186
+
187
+ expect(callFn).toHaveBeenCalledWith(CesRpcMethod.ListCredentials, {});
188
+ expect(result).toEqual(["account-a", "account-b"]);
189
+ });
190
+
191
+ test("returns empty array when RPC call throws", async () => {
192
+ callFn.mockRejectedValue(new Error("transport error"));
193
+
194
+ const result = await backend.list();
195
+
196
+ expect(result).toEqual([]);
197
+ });
198
+ });
199
+ });
@@ -38,11 +38,6 @@ mock.module("../util/logger.js", () => ({
38
38
  }),
39
39
  }));
40
40
 
41
- // Mock security check to always pass
42
- mock.module("../security/secret-ingress.js", () => ({
43
- checkIngressForSecrets: () => ({ blocked: false }),
44
- }));
45
-
46
41
  // Mock render to return the raw content as text
47
42
  mock.module("../daemon/handlers/shared.js", () => ({
48
43
  renderHistoryContent: (content: unknown) => ({
@@ -1,15 +1,9 @@
1
- import { beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  let mockTwilioPhoneNumber: string | undefined;
4
4
  let mockRawConfig: Record<string, unknown> | undefined;
5
5
  let mockSecureKeys: Record<string, string>;
6
6
  let mockHasTwilioCredentials: boolean;
7
- let mockGatewayHealth = {
8
- target: "http://127.0.0.1:7830",
9
- healthy: true,
10
- localDeployment: true,
11
- error: undefined as string | undefined,
12
- };
13
7
 
14
8
  mock.module("../calls/twilio-rest.js", () => ({
15
9
  getPhoneNumberSid: async () => null,
@@ -57,14 +51,12 @@ mock.module("../runtime/channel-invite-transports/whatsapp.js", () => ({
57
51
  import type { ChannelId } from "../channels/types.js";
58
52
  import {
59
53
  ChannelReadinessService,
60
- createReadinessService,
61
54
  REMOTE_TTL_MS,
62
55
  } from "../runtime/channel-readiness-service.js";
63
56
  import type {
64
57
  ChannelProbe,
65
58
  ReadinessCheckResult,
66
59
  } from "../runtime/channel-readiness-types.js";
67
- import * as localGatewayHealth from "../runtime/local-gateway-health.js";
68
60
 
69
61
  // ── Test helpers ────────────────────────────────────────────────────────────
70
62
 
@@ -104,12 +96,6 @@ describe("ChannelReadinessService", () => {
104
96
  mockRawConfig = undefined;
105
97
  mockSecureKeys = {};
106
98
  mockHasTwilioCredentials = false;
107
- mockGatewayHealth = {
108
- target: "http://127.0.0.1:7830",
109
- healthy: true,
110
- localDeployment: true,
111
- error: undefined,
112
- };
113
99
  });
114
100
 
115
101
  test("local checks run on every call (no caching of local results)", async () => {
@@ -403,49 +389,4 @@ describe("ChannelReadinessService", () => {
403
389
 
404
390
  expect(probe.remoteCallCount).toBe(1);
405
391
  });
406
-
407
- test("voice readiness includes gateway_health when ingress is configured", async () => {
408
- mockHasTwilioCredentials = true;
409
- mockTwilioPhoneNumber = "+15550001111";
410
- mockRawConfig = {
411
- ingress: {
412
- enabled: true,
413
- publicBaseUrl: "https://voice.example.com",
414
- },
415
- };
416
- mockGatewayHealth = {
417
- target: "http://127.0.0.1:7830",
418
- healthy: false,
419
- localDeployment: true,
420
- error: "connect ECONNREFUSED 127.0.0.1:7830",
421
- };
422
-
423
- const probeLocalGatewayHealthSpy = spyOn(
424
- localGatewayHealth,
425
- "probeLocalGatewayHealth",
426
- ).mockImplementation(async () => ({
427
- ...mockGatewayHealth,
428
- }));
429
-
430
- let snapshot: Awaited<
431
- ReturnType<ChannelReadinessService["getReadiness"]>
432
- >[number];
433
- try {
434
- const readinessService = createReadinessService();
435
- [snapshot] = await readinessService.getReadiness("phone");
436
- } finally {
437
- probeLocalGatewayHealthSpy.mockRestore();
438
- }
439
-
440
- const gatewayHealthCheck = snapshot.localChecks.find(
441
- (check) => check.name === "gateway_health",
442
- );
443
- expect(gatewayHealthCheck).toBeDefined();
444
- expect(gatewayHealthCheck?.passed).toBe(false);
445
- expect(snapshot.reasons).toContainEqual({
446
- code: "gateway_health",
447
- text: "Local gateway is not serving requests at http://127.0.0.1:7830: connect ECONNREFUSED 127.0.0.1:7830",
448
- });
449
- expect(snapshot.ready).toBe(false);
450
- });
451
392
  });
@@ -1606,13 +1606,15 @@ describe("Permission Checker", () => {
1606
1606
  expect(options[0].description).toContain("compound");
1607
1607
  });
1608
1608
 
1609
- test("compound command via pipeline yields exact-only allowlist option", async () => {
1609
+ test("compound command via pipeline yields exact + action-key allowlist options", async () => {
1610
1610
  const options = await generateAllowlistOptions("bash", {
1611
1611
  command: "git log | grep fix",
1612
1612
  });
1613
- expect(options).toHaveLength(1);
1613
+ expect(options.length).toBeGreaterThanOrEqual(2);
1614
1614
  expect(options[0].description).toContain("compound");
1615
1615
  expect(options[0].pattern).toBe("git log | grep fix");
1616
+ // Pipeline action keys should be offered as broader options
1617
+ expect(options.some((o) => o.pattern.startsWith("action:"))).toBe(true);
1616
1618
  });
1617
1619
 
1618
1620
  test("compound command via && yields exact-only allowlist option", async () => {
@@ -0,0 +1,112 @@
1
+ // Guard test: assistant CLI commands must always classify as Low risk.
2
+ //
3
+ // The assistant uses its own CLI tools during normal operation. If these
4
+ // commands require user approval, it blocks autonomous assistant workflows.
5
+ // See #18982 / #18998 for the regression that motivated this guard.
6
+
7
+ import { mkdtempSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { describe, expect, mock, test } from "bun:test";
11
+
12
+ const guardTestDir = mkdtempSync(join(tmpdir(), "cli-risk-guard-test-"));
13
+
14
+ mock.module("../util/platform.js", () => ({
15
+ getRootDir: () => guardTestDir,
16
+ getDataDir: () => join(guardTestDir, "data"),
17
+ getWorkspaceSkillsDir: () => join(guardTestDir, "skills"),
18
+ isMacOS: () => process.platform === "darwin",
19
+ isLinux: () => process.platform === "linux",
20
+ isWindows: () => process.platform === "win32",
21
+ getPidPath: () => join(guardTestDir, "test.pid"),
22
+ getDbPath: () => join(guardTestDir, "test.db"),
23
+ getLogPath: () => join(guardTestDir, "test.log"),
24
+ ensureDataDir: () => {},
25
+ }));
26
+
27
+ mock.module("../util/logger.js", () => ({
28
+ getLogger: () =>
29
+ new Proxy({} as Record<string, unknown>, {
30
+ get: (_target: Record<string, unknown>, _prop: string) => {
31
+ return () => {};
32
+ },
33
+ }),
34
+ }));
35
+
36
+ mock.module("../config/loader.js", () => ({
37
+ getConfig: () => ({
38
+ permissions: { mode: "workspace" },
39
+ skills: { load: { extraDirs: [] } },
40
+ sandbox: { enabled: true },
41
+ }),
42
+ loadConfig: () => ({}),
43
+ invalidateConfigCache: () => {},
44
+ saveConfig: () => {},
45
+ loadRawConfig: () => ({}),
46
+ saveRawConfig: () => {},
47
+ getNestedValue: () => undefined,
48
+ setNestedValue: () => {},
49
+ }));
50
+
51
+ import { buildCliProgram } from "../cli/program.js";
52
+ import { classifyRisk } from "../permissions/checker.js";
53
+ import { RiskLevel } from "../permissions/types.js";
54
+
55
+ /**
56
+ * Assert that a command classifies as Low risk, with a descriptive failure
57
+ * message that guides developers toward the correct fix.
58
+ */
59
+ function expectLowRisk(command: string, actual: RiskLevel): void {
60
+ if (actual !== RiskLevel.Low) {
61
+ throw new Error(
62
+ `"${command}" classified as ${actual} instead of Low. ` +
63
+ `assistant CLI commands must always be Low risk — the assistant ` +
64
+ `uses its own CLI during normal operation. If you need risk ` +
65
+ `escalation for specific subcommands, add them to an allowlist ` +
66
+ `in this guard test with justification.`,
67
+ );
68
+ }
69
+ expect(actual).toBe(RiskLevel.Low);
70
+ }
71
+
72
+ // Dynamically extract subcommand names from the CLI program definition.
73
+ // This ensures new commands added to program.ts are automatically covered
74
+ // by this guard test without manual list maintenance.
75
+ const program = buildCliProgram();
76
+ const ASSISTANT_SUBCOMMANDS = program.commands.map((c) => c.name());
77
+
78
+ describe("CLI command risk guard: assistant commands", () => {
79
+ test("subcommand discovery found a reasonable number of commands", () => {
80
+ // Sanity check: if mocking breaks and no commands are registered,
81
+ // the risk guard would vacuously pass. Require a minimum count to
82
+ // catch that failure mode. Update this threshold when commands are
83
+ // removed (but it should only grow).
84
+ expect(ASSISTANT_SUBCOMMANDS.length).toBeGreaterThanOrEqual(20);
85
+ });
86
+
87
+ test("all assistant CLI subcommands classify as Low risk", async () => {
88
+ for (const subcommand of ASSISTANT_SUBCOMMANDS) {
89
+ const command = `assistant ${subcommand}`;
90
+ const risk = await classifyRisk("bash", { command });
91
+ expectLowRisk(command, risk);
92
+ }
93
+ });
94
+
95
+ test("bare assistant command classifies as Low risk", async () => {
96
+ const risk = await classifyRisk("bash", { command: "assistant" });
97
+ expectLowRisk("assistant", risk);
98
+ });
99
+
100
+ test("assistant with flags classifies as Low risk", async () => {
101
+ const flagCommands = [
102
+ "assistant --version",
103
+ "assistant --help",
104
+ "assistant doctor --verbose",
105
+ ];
106
+
107
+ for (const command of flagCommands) {
108
+ const risk = await classifyRisk("bash", { command });
109
+ expectLowRisk(command, risk);
110
+ }
111
+ });
112
+ });
@@ -88,7 +88,6 @@ mock.module("../config/loader.js", () => ({
88
88
  invalidateConfigCache: () => {},
89
89
  getNestedValue: () => undefined,
90
90
  setNestedValue: () => {},
91
- syncConfigToLockfile: () => {},
92
91
  }));
93
92
 
94
93
  import { Command } from "commander";
@@ -137,7 +137,6 @@ describe("AssistantConfigSchema", () => {
137
137
  action: "redact",
138
138
  entropyThreshold: 4.0,
139
139
  allowOneTimeSend: false,
140
- blockIngress: true,
141
140
  });
142
141
  expect(result.auditLog).toEqual({ retentionDays: 0 });
143
142
  });
@@ -638,6 +637,7 @@ describe("AssistantConfigSchema", () => {
638
637
  voice: {
639
638
  language: "en-US",
640
639
  transcriptionProvider: "Deepgram",
640
+ ttsProvider: "elevenlabs",
641
641
  },
642
642
  callerIdentity: {
643
643
  allowPerCallOverride: true,
@@ -31,11 +31,6 @@ mock.module("../util/logger.js", () => ({
31
31
  truncateForLog: (value: string) => value,
32
32
  }));
33
33
 
34
- // Mock security check to always pass
35
- mock.module("../security/secret-ingress.js", () => ({
36
- checkIngressForSecrets: () => ({ blocked: false }),
37
- }));
38
-
39
34
  // Mock render to return the raw content as text
40
35
  mock.module("../daemon/handlers/shared.js", () => ({
41
36
  renderHistoryContent: (content: unknown) => ({
@@ -111,10 +111,8 @@ mock.module("../util/platform.js", () => ({
111
111
  getTCPPort: () => 8765,
112
112
  isIOSPairingEnabled: () => false,
113
113
  isTCPEnabled: () => false,
114
- readLockfile: () => null,
115
114
  readPlatformToken: () => null,
116
115
  readSessionToken: () => null,
117
- writeLockfile: () => {},
118
116
  ensureDataDir: () => {},
119
117
  }));
120
118
 
@@ -209,11 +209,9 @@ mock.module("../util/logger.js", () => ({
209
209
  mock.module("../config/loader.js", () => ({
210
210
  getConfig: () => ({
211
211
  skills: { entries: {}, allowBundled: null },
212
- assistantFeatureFlagValues: {},
213
212
  }),
214
213
  loadConfig: () => ({
215
214
  skills: { entries: {}, allowBundled: null },
216
- assistantFeatureFlagValues: {},
217
215
  }),
218
216
  invalidateConfigCache: () => {},
219
217
  }));
@@ -1721,58 +1719,6 @@ describe("bundled skill: gmail", () => {
1721
1719
  });
1722
1720
  });
1723
1721
 
1724
- describe("bundled skill: claude-code", () => {
1725
- let sessionState: Map<string, string>;
1726
-
1727
- beforeEach(() => {
1728
- mockCatalog = [];
1729
- mockManifests = {};
1730
- mockRegisteredTools = new Map();
1731
- mockUnregisteredSkillIds = [];
1732
- mockSkillRefCount = new Map();
1733
- mockSkillRefCount = new Map();
1734
- mockVersionHashes = {};
1735
- mockVersionHashErrors = new Set();
1736
- sessionState = new Map<string, string>();
1737
- });
1738
-
1739
- test("claude-code skill activation registers claude_code in allowedToolNames", () => {
1740
- mockCatalog = [
1741
- makeSkill("claude-code", "/path/to/bundled-skills/claude-code"),
1742
- ];
1743
- mockManifests = { "claude-code": makeManifest(["claude_code"]) };
1744
-
1745
- const history: Message[] = [
1746
- ...skillLoadMessages('<loaded_skill id="claude-code" />'),
1747
- ];
1748
-
1749
- const result = projectSkillTools(history, {
1750
- previouslyActiveSkillIds: sessionState,
1751
- });
1752
-
1753
- expect(result.toolDefinitions).toEqual([]);
1754
- expect(result.allowedToolNames).toEqual(new Set(["claude_code"]));
1755
- });
1756
-
1757
- test("claude_code tool is absent when claude-code skill is not active", () => {
1758
- mockCatalog = [
1759
- makeSkill("claude-code", "/path/to/bundled-skills/claude-code"),
1760
- ];
1761
- mockManifests = { "claude-code": makeManifest(["claude_code"]) };
1762
-
1763
- const history: Message[] = [
1764
- { role: "user", content: [{ type: "text", text: "Hello" }] },
1765
- ];
1766
-
1767
- const result = projectSkillTools(history, {
1768
- previouslyActiveSkillIds: sessionState,
1769
- });
1770
-
1771
- expect(result.toolDefinitions).toHaveLength(0);
1772
- expect(result.allowedToolNames.has("claude_code")).toBe(false);
1773
- });
1774
- });
1775
-
1776
1722
  // ---------------------------------------------------------------------------
1777
1723
  // Bundled skill: app-builder
1778
1724
  // ---------------------------------------------------------------------------
@@ -107,6 +107,93 @@ describe("conversation-title-service", () => {
107
107
  );
108
108
  });
109
109
 
110
+ test("regeneration extracts text from JSON content blocks", async () => {
111
+ mockGetMessages.mockReturnValueOnce([
112
+ {
113
+ role: "user",
114
+ content: JSON.stringify([
115
+ { type: "text", text: "Help me plan the kickoff" },
116
+ ]),
117
+ },
118
+ {
119
+ role: "assistant",
120
+ content: JSON.stringify([
121
+ { type: "text", text: "Sure, here's a plan" },
122
+ { type: "tool_use", id: "toolu_1", name: "web_search", input: {} },
123
+ ]),
124
+ },
125
+ {
126
+ role: "user",
127
+ content: JSON.stringify([{ type: "text", text: "Looks good" }]),
128
+ },
129
+ ]);
130
+
131
+ const provider = {
132
+ name: "test-provider",
133
+ sendMessage: mock(async () => {
134
+ throw new Error("should not call directly");
135
+ }),
136
+ };
137
+
138
+ await regenerateConversationTitle({ conversationId: "conv-1", provider });
139
+
140
+ // The prompt sent to the sidechain should contain plain text, not raw JSON
141
+ const prompt = (mockRunBtwSidechain.mock.calls[0] as any)?.[0]
142
+ ?.content as string;
143
+ expect(prompt).not.toContain('"type":"text"');
144
+ expect(prompt).not.toContain('"type":"tool_use"');
145
+ // Tool metadata should NOT appear in the title prompt
146
+ expect(prompt).not.toContain("Tool use");
147
+ expect(prompt).not.toContain("web_search");
148
+ expect(prompt).toContain("Help me plan the kickoff");
149
+ expect(prompt).toContain("Sure, here's a plan");
150
+ expect(prompt).toContain("Looks good");
151
+ });
152
+
153
+ test("regeneration extracts text from tool_result content blocks", async () => {
154
+ mockGetMessages.mockReturnValueOnce([
155
+ {
156
+ role: "user",
157
+ content: JSON.stringify([
158
+ { type: "text", text: "Search for restaurants" },
159
+ ]),
160
+ },
161
+ {
162
+ role: "assistant",
163
+ content: JSON.stringify([
164
+ { type: "tool_use", id: "toolu_1", name: "web_search", input: {} },
165
+ ]),
166
+ },
167
+ {
168
+ role: "user",
169
+ content: JSON.stringify([
170
+ {
171
+ type: "tool_result",
172
+ tool_use_id: "toolu_1",
173
+ content: "Found 3 restaurants nearby",
174
+ },
175
+ ]),
176
+ },
177
+ ]);
178
+
179
+ const provider = {
180
+ name: "test-provider",
181
+ sendMessage: mock(async () => {
182
+ throw new Error("should not call directly");
183
+ }),
184
+ };
185
+
186
+ await regenerateConversationTitle({ conversationId: "conv-1", provider });
187
+
188
+ const prompt = (mockRunBtwSidechain.mock.calls[0] as any)?.[0]
189
+ ?.content as string;
190
+ expect(prompt).not.toContain('"type":"tool_result"');
191
+ // Tool-only assistant message should be skipped entirely
192
+ expect(prompt).not.toContain("Tool use");
193
+ expect(prompt).toContain("Search for restaurants");
194
+ expect(prompt).toContain("Found 3 restaurants nearby");
195
+ });
196
+
110
197
  test("uses the BTW side-chain helper for title regeneration", async () => {
111
198
  const provider = {
112
199
  name: "test-provider",