@vellumai/assistant 0.5.6 → 0.5.8

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 (442) hide show
  1. package/.env.example +16 -2
  2. package/ARCHITECTURE.md +6 -75
  3. package/Dockerfile +3 -2
  4. package/README.md +0 -2
  5. package/bun.lock +0 -414
  6. package/docker-entrypoint.sh +9 -0
  7. package/docs/architecture/keychain-broker.md +45 -240
  8. package/docs/architecture/memory.md +13 -11
  9. package/docs/architecture/security.md +0 -17
  10. package/docs/credential-execution-service.md +2 -2
  11. package/node_modules/@vellumai/ces-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
  13. package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
  14. package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
  15. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
  16. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +120 -1
  17. package/node_modules/@vellumai/credential-storage/package.json +1 -0
  18. package/node_modules/@vellumai/egress-proxy/package.json +1 -0
  19. package/package.json +2 -3
  20. package/src/__tests__/actor-token-service.test.ts +0 -114
  21. package/src/__tests__/approval-cascade.test.ts +0 -1
  22. package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
  23. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  24. package/src/__tests__/browser-skill-endstate.test.ts +6 -5
  25. package/src/__tests__/btw-routes.test.ts +0 -39
  26. package/src/__tests__/call-controller.test.ts +0 -1
  27. package/src/__tests__/call-domain.test.ts +0 -128
  28. package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
  29. package/src/__tests__/ces-startup-timeout.test.ts +40 -0
  30. package/src/__tests__/channel-approval-routes.test.ts +0 -5
  31. package/src/__tests__/channel-readiness-service.test.ts +1 -60
  32. package/src/__tests__/checker.test.ts +4 -2
  33. package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
  34. package/src/__tests__/config-schema-cmd.test.ts +0 -2
  35. package/src/__tests__/config-schema.test.ts +3 -1
  36. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
  37. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
  38. package/src/__tests__/conversation-agent-loop.test.ts +2 -4
  39. package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
  40. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
  41. package/src/__tests__/conversation-error.test.ts +15 -1
  42. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  43. package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
  44. package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
  45. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
  46. package/src/__tests__/conversation-queue.test.ts +0 -1
  47. package/src/__tests__/conversation-skill-tools.test.ts +0 -54
  48. package/src/__tests__/conversation-slash-queue.test.ts +0 -1
  49. package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
  50. package/src/__tests__/conversation-title-service.test.ts +87 -0
  51. package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
  52. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
  53. package/src/__tests__/credential-execution-client.test.ts +5 -2
  54. package/src/__tests__/credential-execution-feature-gates.test.ts +59 -30
  55. package/src/__tests__/credential-execution-managed-contract.test.ts +35 -20
  56. package/src/__tests__/credential-security-e2e.test.ts +1 -67
  57. package/src/__tests__/credential-security-invariants.test.ts +6 -50
  58. package/src/__tests__/credentials-cli.test.ts +82 -3
  59. package/src/__tests__/daemon-credential-client.test.ts +123 -0
  60. package/src/__tests__/db-migration-rollback.test.ts +2015 -1
  61. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
  62. package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
  63. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
  64. package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
  65. package/src/__tests__/guardian-routing-state.test.ts +0 -5
  66. package/src/__tests__/host-shell-tool.test.ts +6 -7
  67. package/src/__tests__/http-user-message-parity.test.ts +3 -103
  68. package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
  69. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
  70. package/src/__tests__/intent-routing.test.ts +0 -13
  71. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
  72. package/src/__tests__/journal-context.test.ts +335 -0
  73. package/src/__tests__/keychain-broker-client.test.ts +161 -22
  74. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
  75. package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
  76. package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
  77. package/src/__tests__/memory-recall-quality.test.ts +48 -17
  78. package/src/__tests__/memory-regressions.test.ts +408 -363
  79. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
  80. package/src/__tests__/migration-export-http.test.ts +2 -2
  81. package/src/__tests__/migration-import-commit-http.test.ts +2 -2
  82. package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
  83. package/src/__tests__/migration-validate-http.test.ts +2 -2
  84. package/src/__tests__/non-member-access-request.test.ts +2 -7
  85. package/src/__tests__/notification-decision-fallback.test.ts +4 -0
  86. package/src/__tests__/notification-decision-identity.test.ts +4 -0
  87. package/src/__tests__/notification-decision-strategy.test.ts +71 -0
  88. package/src/__tests__/oauth-cli.test.ts +5 -1
  89. package/src/__tests__/permission-types.test.ts +1 -0
  90. package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
  91. package/src/__tests__/provider-error-scenarios.test.ts +0 -267
  92. package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
  93. package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
  94. package/src/__tests__/qdrant-manager.test.ts +28 -2
  95. package/src/__tests__/registry.test.ts +0 -6
  96. package/src/__tests__/relay-server.test.ts +1 -2
  97. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
  98. package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
  99. package/src/__tests__/secret-onetime-send.test.ts +1 -1
  100. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
  101. package/src/__tests__/secure-keys.test.ts +95 -272
  102. package/src/__tests__/shell-identity.test.ts +96 -6
  103. package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
  104. package/src/__tests__/skill-feature-flags.test.ts +46 -45
  105. package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
  106. package/src/__tests__/skill-load-inline-command.test.ts +8 -12
  107. package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
  108. package/src/__tests__/skill-load-tool.test.ts +0 -2
  109. package/src/__tests__/skill-memory.test.ts +17 -3
  110. package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
  111. package/src/__tests__/skills.test.ts +0 -2
  112. package/src/__tests__/slack-inbound-verification.test.ts +0 -4
  113. package/src/__tests__/stale-approval-dedup.test.ts +171 -0
  114. package/src/__tests__/stt-hints.test.ts +437 -0
  115. package/src/__tests__/suggestion-routes.test.ts +1 -32
  116. package/src/__tests__/system-prompt.test.ts +0 -1
  117. package/src/__tests__/task-memory-cleanup.test.ts +14 -0
  118. package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
  119. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
  120. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
  121. package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
  122. package/src/__tests__/update-bulletin.test.ts +0 -2
  123. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
  124. package/src/__tests__/voice-quality.test.ts +58 -0
  125. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -7
  126. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
  127. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +220 -0
  128. package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
  129. package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
  130. package/src/acp/agent-process.ts +9 -1
  131. package/src/agent/loop.ts +1 -1
  132. package/src/approvals/guardian-request-resolvers.ts +164 -38
  133. package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
  134. package/src/calls/audio-store.test.ts +97 -0
  135. package/src/calls/audio-store.ts +205 -0
  136. package/src/calls/call-controller.ts +90 -8
  137. package/src/calls/call-domain.ts +3 -0
  138. package/src/calls/call-store.ts +10 -3
  139. package/src/calls/fish-audio-client.ts +129 -0
  140. package/src/calls/relay-server.ts +27 -0
  141. package/src/calls/stt-hints.ts +189 -0
  142. package/src/calls/tts-text-sanitizer.ts +61 -0
  143. package/src/calls/twilio-routes.ts +34 -5
  144. package/src/calls/types.ts +1 -0
  145. package/src/calls/voice-ingress-preflight.ts +0 -42
  146. package/src/calls/voice-quality.ts +38 -5
  147. package/src/calls/voice-session-bridge.ts +7 -12
  148. package/src/cli/commands/avatar.ts +2 -2
  149. package/src/cli/commands/config.ts +1 -4
  150. package/src/cli/commands/credentials.ts +128 -82
  151. package/src/cli/commands/doctor.ts +2 -2
  152. package/src/cli/commands/keys.ts +7 -7
  153. package/src/cli/commands/memory.ts +1 -1
  154. package/src/cli/commands/oauth/connections.ts +11 -29
  155. package/src/cli/commands/oauth/index.ts +7 -0
  156. package/src/cli/commands/oauth/platform.ts +525 -0
  157. package/src/cli/commands/platform.ts +3 -3
  158. package/src/cli/lib/daemon-credential-client.ts +284 -0
  159. package/src/cli.ts +1 -1
  160. package/src/config/assistant-feature-flags.ts +186 -5
  161. package/src/config/bundled-skills/AGENTS.md +34 -0
  162. package/src/config/bundled-skills/acp/SKILL.md +10 -0
  163. package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
  164. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  165. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  166. package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
  167. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
  168. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
  169. package/src/config/bundled-skills/settings/SKILL.md +15 -2
  170. package/src/config/bundled-skills/settings/TOOLS.json +47 -2
  171. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
  172. package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
  173. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
  174. package/src/config/bundled-skills/slack/SKILL.md +1 -1
  175. package/src/config/bundled-tool-registry.ts +5 -11
  176. package/src/config/defaults.ts +0 -2
  177. package/src/config/env-registry.ts +5 -5
  178. package/src/config/env.ts +21 -14
  179. package/src/config/feature-flag-registry.json +49 -9
  180. package/src/config/loader.ts +106 -42
  181. package/src/config/schema.ts +9 -29
  182. package/src/config/schemas/calls.ts +30 -0
  183. package/src/config/schemas/fish-audio.ts +39 -0
  184. package/src/config/schemas/inference.ts +2 -2
  185. package/src/config/schemas/journal.ts +16 -0
  186. package/src/config/schemas/memory-processing.ts +2 -2
  187. package/src/config/schemas/security.ts +0 -4
  188. package/src/config/types.ts +1 -1
  189. package/src/contacts/contact-store.ts +39 -0
  190. package/src/contacts/types.ts +2 -0
  191. package/src/credential-execution/approval-bridge.ts +1 -0
  192. package/src/credential-execution/executable-discovery.ts +28 -4
  193. package/src/credential-execution/feature-gates.ts +16 -0
  194. package/src/credential-execution/process-manager.ts +38 -0
  195. package/src/credential-execution/startup-timeout.ts +36 -0
  196. package/src/daemon/approval-generators.ts +3 -9
  197. package/src/daemon/assistant-attachments.ts +9 -0
  198. package/src/daemon/config-watcher.ts +5 -0
  199. package/src/daemon/conversation-error.ts +13 -1
  200. package/src/daemon/conversation-memory.ts +1 -2
  201. package/src/daemon/conversation-process.ts +18 -1
  202. package/src/daemon/conversation-surfaces.ts +30 -1
  203. package/src/daemon/conversation-tool-setup.ts +0 -105
  204. package/src/daemon/conversation.ts +21 -1
  205. package/src/daemon/guardian-action-generators.ts +3 -9
  206. package/src/daemon/handlers/config-vercel.ts +92 -0
  207. package/src/daemon/handlers/skills.ts +2 -15
  208. package/src/daemon/install-symlink.ts +195 -0
  209. package/src/daemon/lifecycle.ts +234 -51
  210. package/src/daemon/message-types/conversations.ts +4 -4
  211. package/src/daemon/message-types/diagnostics.ts +3 -22
  212. package/src/daemon/message-types/messages.ts +0 -2
  213. package/src/daemon/message-types/upgrades.ts +8 -0
  214. package/src/daemon/server.ts +32 -95
  215. package/src/events/domain-events.ts +2 -1
  216. package/src/inbound/platform-callback-registration.ts +3 -3
  217. package/src/instrument.ts +8 -5
  218. package/src/memory/app-store.ts +31 -0
  219. package/src/memory/conversation-title-service.ts +50 -1
  220. package/src/memory/db-init.ts +16 -0
  221. package/src/memory/indexer.ts +19 -10
  222. package/src/memory/items-extractor.ts +328 -321
  223. package/src/memory/job-handlers/conversation-starters.ts +4 -1
  224. package/src/memory/job-handlers/summarization.ts +26 -16
  225. package/src/memory/jobs-store.ts +63 -6
  226. package/src/memory/jobs-worker.ts +31 -7
  227. package/src/memory/journal-memory.ts +214 -0
  228. package/src/memory/migrations/001-job-deferrals.ts +19 -0
  229. package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
  230. package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
  231. package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
  232. package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
  233. package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
  234. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
  235. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
  236. package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
  237. package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
  238. package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
  239. package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
  240. package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
  241. package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
  242. package/src/memory/migrations/116-messages-fts.ts +106 -1
  243. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
  244. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
  245. package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
  246. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
  247. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
  248. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
  249. package/src/memory/migrations/141-rename-verification-table.ts +54 -0
  250. package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
  251. package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
  252. package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
  253. package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
  254. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
  255. package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
  256. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
  257. package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
  258. package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
  259. package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
  260. package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
  261. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
  262. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
  263. package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
  264. package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
  265. package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
  266. package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
  267. package/src/memory/migrations/index.ts +5 -0
  268. package/src/memory/migrations/registry.ts +98 -0
  269. package/src/memory/migrations/validate-migration-state.ts +137 -11
  270. package/src/memory/qdrant-circuit-breaker.ts +9 -0
  271. package/src/memory/qdrant-manager.ts +64 -7
  272. package/src/memory/retriever.test.ts +37 -25
  273. package/src/memory/retriever.ts +24 -49
  274. package/src/memory/schema/calls.ts +1 -0
  275. package/src/memory/schema/contacts.ts +1 -0
  276. package/src/memory/schema/memory-core.ts +2 -0
  277. package/src/memory/search/formatting.ts +7 -44
  278. package/src/memory/search/staleness.ts +4 -0
  279. package/src/memory/search/tier-classifier.ts +10 -2
  280. package/src/memory/search/types.ts +2 -5
  281. package/src/memory/task-memory-cleanup.ts +4 -3
  282. package/src/notifications/adapters/slack.ts +168 -6
  283. package/src/notifications/broadcaster.ts +1 -0
  284. package/src/notifications/copy-composer.ts +59 -2
  285. package/src/notifications/decision-engine.ts +4 -1
  286. package/src/notifications/signal.ts +2 -0
  287. package/src/notifications/types.ts +2 -0
  288. package/src/oauth/connection-resolver.ts +6 -4
  289. package/src/permissions/checker.ts +0 -38
  290. package/src/permissions/shell-identity.ts +76 -22
  291. package/src/permissions/types.ts +4 -2
  292. package/src/platform/client.ts +35 -7
  293. package/src/prompts/journal-context.ts +133 -0
  294. package/src/prompts/persona-resolver.ts +194 -0
  295. package/src/prompts/system-prompt.ts +44 -4
  296. package/src/prompts/templates/SOUL.md +10 -0
  297. package/src/prompts/templates/users/default.md +1 -0
  298. package/src/providers/provider-send-message.ts +3 -32
  299. package/src/providers/registry.ts +29 -179
  300. package/src/providers/types.ts +1 -1
  301. package/src/runtime/access-request-helper.ts +4 -0
  302. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
  303. package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
  304. package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
  305. package/src/runtime/auth/external-assistant-id.ts +13 -59
  306. package/src/runtime/auth/route-policy.ts +17 -1
  307. package/src/runtime/auth/token-service.ts +43 -138
  308. package/src/runtime/channel-readiness-service.ts +1 -16
  309. package/src/runtime/gateway-client.ts +47 -4
  310. package/src/runtime/guardian-decision-types.ts +45 -4
  311. package/src/runtime/http-server.ts +31 -3
  312. package/src/runtime/middleware/error-handler.ts +1 -9
  313. package/src/runtime/routes/access-request-decision.ts +2 -2
  314. package/src/runtime/routes/app-management-routes.ts +2 -1
  315. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
  316. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
  317. package/src/runtime/routes/audio-routes.ts +40 -0
  318. package/src/runtime/routes/btw-routes.ts +0 -17
  319. package/src/runtime/routes/channel-readiness-routes.ts +9 -4
  320. package/src/runtime/routes/conversation-query-routes.ts +63 -1
  321. package/src/runtime/routes/conversation-routes.ts +4 -44
  322. package/src/runtime/routes/debug-routes.ts +12 -9
  323. package/src/runtime/routes/diagnostics-routes.ts +1 -477
  324. package/src/runtime/routes/guardian-approval-interception.ts +168 -11
  325. package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
  326. package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
  327. package/src/runtime/routes/identity-routes.ts +19 -30
  328. package/src/runtime/routes/inbound-message-handler.ts +31 -1
  329. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
  330. package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
  331. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
  332. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
  333. package/src/runtime/routes/integrations/twilio.ts +52 -10
  334. package/src/runtime/routes/integrations/vercel.ts +89 -0
  335. package/src/runtime/routes/log-export-routes.ts +5 -0
  336. package/src/runtime/routes/memory-item-routes.test.ts +3 -3
  337. package/src/runtime/routes/memory-item-routes.ts +46 -14
  338. package/src/runtime/routes/migration-rollback-routes.ts +209 -0
  339. package/src/runtime/routes/migration-routes.ts +17 -1
  340. package/src/runtime/routes/notification-routes.ts +58 -0
  341. package/src/runtime/routes/schedule-routes.ts +65 -0
  342. package/src/runtime/routes/secret-routes.ts +141 -10
  343. package/src/runtime/routes/settings-routes.ts +41 -1
  344. package/src/runtime/routes/tts-routes.ts +96 -0
  345. package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
  346. package/src/runtime/routes/workspace-commit-routes.ts +62 -0
  347. package/src/runtime/routes/workspace-routes.test.ts +22 -1
  348. package/src/runtime/routes/workspace-routes.ts +1 -1
  349. package/src/runtime/routes/workspace-utils.ts +86 -2
  350. package/src/security/ces-credential-client.ts +75 -29
  351. package/src/security/ces-rpc-credential-backend.ts +86 -0
  352. package/src/security/credential-backend.ts +22 -92
  353. package/src/security/keychain-broker-client.ts +10 -2
  354. package/src/security/secure-keys.ts +113 -115
  355. package/src/skills/catalog-install.ts +6 -32
  356. package/src/skills/skill-memory.ts +1 -0
  357. package/src/subagent/manager.ts +2 -5
  358. package/src/telemetry/usage-telemetry-reporter.ts +4 -2
  359. package/src/tools/acp/spawn.ts +78 -1
  360. package/src/tools/calls/call-start.ts +1 -0
  361. package/src/tools/credentials/vault.ts +5 -3
  362. package/src/tools/executor.ts +0 -4
  363. package/src/tools/memory/definitions.ts +3 -2
  364. package/src/tools/memory/handlers.ts +10 -7
  365. package/src/tools/network/script-proxy/session-manager.ts +19 -4
  366. package/src/tools/network/web-fetch.ts +3 -1
  367. package/src/tools/skills/execute.ts +1 -1
  368. package/src/tools/terminal/safe-env.ts +1 -0
  369. package/src/tools/types.ts +0 -8
  370. package/src/util/browser.ts +15 -0
  371. package/src/util/errors.ts +0 -12
  372. package/src/util/platform.ts +4 -51
  373. package/src/workspace/git-service.ts +5 -2
  374. package/src/workspace/migrations/001-avatar-rename.ts +15 -0
  375. package/src/workspace/migrations/003-seed-device-id.ts +17 -1
  376. package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
  377. package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
  378. package/src/workspace/migrations/006-services-config.ts +49 -0
  379. package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
  380. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
  381. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
  382. package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
  383. package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
  384. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
  385. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
  386. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
  387. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
  388. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
  389. package/src/workspace/migrations/017-seed-persona-dirs.ts +96 -0
  390. package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
  391. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
  392. package/src/workspace/migrations/migrate-to-workspace-volume.ts +27 -5
  393. package/src/workspace/migrations/registry.ts +12 -0
  394. package/src/workspace/migrations/runner.ts +106 -2
  395. package/src/workspace/migrations/types.ts +4 -0
  396. package/src/workspace/provider-commit-message-generator.ts +12 -21
  397. package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
  398. package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
  399. package/src/__tests__/diagnostics-export.test.ts +0 -288
  400. package/src/__tests__/local-gateway-health.test.ts +0 -209
  401. package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
  402. package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
  403. package/src/__tests__/secret-ingress-handler.test.ts +0 -120
  404. package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
  405. package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
  406. package/src/__tests__/swarm-orchestrator.test.ts +0 -463
  407. package/src/__tests__/swarm-plan-validator.test.ts +0 -384
  408. package/src/__tests__/swarm-recursion.test.ts +0 -197
  409. package/src/__tests__/swarm-router-planner.test.ts +0 -234
  410. package/src/__tests__/swarm-tool.test.ts +0 -185
  411. package/src/__tests__/swarm-worker-backend.test.ts +0 -144
  412. package/src/__tests__/swarm-worker-runner.test.ts +0 -288
  413. package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
  414. package/src/commands/cc-command-registry.ts +0 -248
  415. package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
  416. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
  417. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
  418. package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
  419. package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
  420. package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
  421. package/src/config/schemas/swarm.ts +0 -82
  422. package/src/logfire.ts +0 -135
  423. package/src/memory/search/lexical.ts +0 -48
  424. package/src/providers/failover.ts +0 -186
  425. package/src/runtime/local-gateway-health.ts +0 -275
  426. package/src/security/secret-ingress.ts +0 -68
  427. package/src/swarm/backend-claude-code.ts +0 -225
  428. package/src/swarm/checkpoint.ts +0 -137
  429. package/src/swarm/graph-utils.ts +0 -53
  430. package/src/swarm/index.ts +0 -55
  431. package/src/swarm/limits.ts +0 -66
  432. package/src/swarm/orchestrator.ts +0 -424
  433. package/src/swarm/plan-validator.ts +0 -117
  434. package/src/swarm/router-planner.ts +0 -162
  435. package/src/swarm/router-prompts.ts +0 -39
  436. package/src/swarm/synthesizer.ts +0 -81
  437. package/src/swarm/types.ts +0 -72
  438. package/src/swarm/worker-backend.ts +0 -131
  439. package/src/swarm/worker-prompts.ts +0 -80
  440. package/src/swarm/worker-runner.ts +0 -170
  441. package/src/tools/claude-code/claude-code.ts +0 -610
  442. package/src/tools/swarm/delegate.ts +0 -205
@@ -20,7 +20,10 @@ import { initializeProviders } from "../../providers/registry.js";
20
20
  import { credentialKey } from "../../security/credential-key.js";
21
21
  import {
22
22
  deleteSecureKeyAsync,
23
+ getActiveBackendName,
23
24
  getSecureKeyAsync,
25
+ getSecureKeyResultAsync,
26
+ listSecureKeysAsync,
24
27
  setSecureKeyAsync,
25
28
  } from "../../security/secure-keys.js";
26
29
  import {
@@ -103,11 +106,16 @@ export async function handleAddSecret(
103
106
  req: Request,
104
107
  getCesClient?: () => CesClient | undefined,
105
108
  ): Promise<Response> {
106
- const body = (await req.json()) as {
107
- type?: string;
108
- name?: string;
109
- value?: string;
110
- };
109
+ let body: { type?: string; name?: string; value?: string };
110
+ try {
111
+ body = (await req.json()) as {
112
+ type?: string;
113
+ name?: string;
114
+ value?: string;
115
+ };
116
+ } catch {
117
+ return httpError("BAD_REQUEST", "Request body must be valid JSON", 400);
118
+ }
111
119
 
112
120
  const { type, name, value } = body;
113
121
 
@@ -178,7 +186,7 @@ export async function handleAddSecret(
178
186
  if (!stored) {
179
187
  return httpError(
180
188
  "INTERNAL_ERROR",
181
- "Failed to store API key in secure storage",
189
+ `Failed to store API key in secure storage (backend: ${getActiveBackendName()})`,
182
190
  500,
183
191
  );
184
192
  }
@@ -190,7 +198,7 @@ export async function handleAddSecret(
190
198
  }
191
199
 
192
200
  if (type === "credential") {
193
- const colonIdx = name.indexOf(":");
201
+ const colonIdx = name.lastIndexOf(":");
194
202
  if (colonIdx < 1 || colonIdx === name.length - 1) {
195
203
  return httpError(
196
204
  "BAD_REQUEST",
@@ -243,7 +251,7 @@ export async function handleAddSecret(
243
251
  if (!stored) {
244
252
  return httpError(
245
253
  "INTERNAL_ERROR",
246
- "Failed to store credential in secure storage",
254
+ `Failed to store credential in secure storage (backend: ${getActiveBackendName()})`,
247
255
  500,
248
256
  );
249
257
  }
@@ -308,12 +316,90 @@ export async function handleAddSecret(
308
316
  }
309
317
  }
310
318
 
311
- export async function handleDeleteSecret(req: Request): Promise<Response> {
319
+ export async function handleReadSecret(req: Request): Promise<Response> {
312
320
  const body = (await req.json()) as {
313
321
  type?: string;
314
322
  name?: string;
323
+ reveal?: boolean;
315
324
  };
316
325
 
326
+ const { type, name, reveal } = body;
327
+
328
+ if (!type || typeof type !== "string") {
329
+ return httpError("BAD_REQUEST", "type is required", 400);
330
+ }
331
+ if (!name || typeof name !== "string") {
332
+ return httpError("BAD_REQUEST", "name is required", 400);
333
+ }
334
+
335
+ try {
336
+ let accountKey: string;
337
+
338
+ if (type === "api_key") {
339
+ if (
340
+ !API_KEY_PROVIDERS.includes(name as (typeof API_KEY_PROVIDERS)[number])
341
+ ) {
342
+ return httpError(
343
+ "BAD_REQUEST",
344
+ `Unknown API key provider: ${name}. Valid providers: ${API_KEY_PROVIDERS.join(
345
+ ", ",
346
+ )}`,
347
+ 400,
348
+ );
349
+ }
350
+ accountKey = name;
351
+ } else if (type === "credential") {
352
+ const colonIdx = name.lastIndexOf(":");
353
+ if (colonIdx < 1 || colonIdx === name.length - 1) {
354
+ return httpError(
355
+ "BAD_REQUEST",
356
+ 'For credential type, name must be in "service:field" format (e.g. "github:api_token")',
357
+ 400,
358
+ );
359
+ }
360
+ const service = name.slice(0, colonIdx);
361
+ const field = name.slice(colonIdx + 1);
362
+ accountKey = credentialKey(service, field);
363
+ } else {
364
+ return httpError(
365
+ "BAD_REQUEST",
366
+ `Unknown secret type: ${type}. Valid types: api_key, credential`,
367
+ 400,
368
+ );
369
+ }
370
+
371
+ const { value, unreachable } = await getSecureKeyResultAsync(accountKey);
372
+ if (value === undefined) {
373
+ return Response.json({ found: false, unreachable });
374
+ }
375
+
376
+ if (reveal) {
377
+ return Response.json({ found: true, value, unreachable: false });
378
+ }
379
+
380
+ // Mask the value: show first 10 chars and last 4, hiding at least 3
381
+ const minHidden = 3;
382
+ const maxVisible = Math.max(1, value.length - minHidden);
383
+ const prefixLen = Math.min(10, maxVisible);
384
+ const suffixLen = Math.min(4, Math.max(0, maxVisible - prefixLen));
385
+ const masked = `${value.slice(0, prefixLen)}...${suffixLen > 0 ? value.slice(-suffixLen) : ""}`;
386
+
387
+ return Response.json({ found: true, masked, unreachable: false });
388
+ } catch (err) {
389
+ const message = err instanceof Error ? err.message : String(err);
390
+ log.error({ err, type, name }, "Failed to read secret via HTTP");
391
+ return httpError("INTERNAL_ERROR", message, 500);
392
+ }
393
+ }
394
+
395
+ export async function handleDeleteSecret(req: Request): Promise<Response> {
396
+ let body: { type?: string; name?: string };
397
+ try {
398
+ body = (await req.json()) as { type?: string; name?: string };
399
+ } catch {
400
+ return httpError("BAD_REQUEST", "Request body must be valid JSON", 400);
401
+ }
402
+
317
403
  const { type, name } = body;
318
404
 
319
405
  if (!type || typeof type !== "string") {
@@ -358,7 +444,7 @@ export async function handleDeleteSecret(req: Request): Promise<Response> {
358
444
  }
359
445
 
360
446
  if (type === "credential") {
361
- const colonIdx = name.indexOf(":");
447
+ const colonIdx = name.lastIndexOf(":");
362
448
  if (colonIdx < 1 || colonIdx === name.length - 1) {
363
449
  return httpError(
364
450
  "BAD_REQUEST",
@@ -418,6 +504,41 @@ export async function handleDeleteSecret(req: Request): Promise<Response> {
418
504
  }
419
505
  }
420
506
 
507
+ const CREDENTIAL_KEY_PREFIX = "credential/";
508
+
509
+ export async function handleListSecrets(): Promise<Response> {
510
+ try {
511
+ const { accounts, unreachable } = await listSecureKeysAsync();
512
+ if (unreachable) {
513
+ return Response.json(
514
+ { error: "Credential store is unreachable" },
515
+ { status: 503 },
516
+ );
517
+ }
518
+
519
+ const secrets = accounts.map((account) => {
520
+ if (account.startsWith(CREDENTIAL_KEY_PREFIX)) {
521
+ // credential/{service}/{field} → service:field
522
+ const rest = account.slice(CREDENTIAL_KEY_PREFIX.length);
523
+ const slashIdx = rest.indexOf("/");
524
+ if (slashIdx > 0 && slashIdx < rest.length - 1) {
525
+ return {
526
+ type: "credential" as const,
527
+ name: `${rest.slice(0, slashIdx)}:${rest.slice(slashIdx + 1)}`,
528
+ };
529
+ }
530
+ }
531
+ // API key providers are stored with their raw provider name
532
+ return { type: "api_key" as const, name: account };
533
+ });
534
+
535
+ return Response.json({ secrets });
536
+ } catch (err) {
537
+ const message = err instanceof Error ? err.message : String(err);
538
+ return httpError("INTERNAL_ERROR", message, 500);
539
+ }
540
+ }
541
+
421
542
  // ---------------------------------------------------------------------------
422
543
  // Route definitions
423
544
  // ---------------------------------------------------------------------------
@@ -441,5 +562,15 @@ export function secretRouteDefinitions(
441
562
  method: "DELETE",
442
563
  handler: async ({ req }) => handleDeleteSecret(req),
443
564
  },
565
+ {
566
+ endpoint: "secrets",
567
+ method: "GET",
568
+ handler: async () => handleListSecrets(),
569
+ },
570
+ {
571
+ endpoint: "secrets/read",
572
+ method: "POST",
573
+ handler: async ({ req }) => handleReadSecret(req),
574
+ },
444
575
  ];
445
576
  }
@@ -10,7 +10,10 @@
10
10
  import { readFileSync } from "node:fs";
11
11
  import { join } from "node:path";
12
12
 
13
- import { setIngressPublicBaseUrl } from "../../config/env.js";
13
+ import {
14
+ getPlatformBaseUrl,
15
+ setIngressPublicBaseUrl,
16
+ } from "../../config/env.js";
14
17
  import { loadRawConfig, saveRawConfig } from "../../config/loader.js";
15
18
  import { loadSkillCatalog } from "../../config/skills.js";
16
19
  import {
@@ -728,6 +731,43 @@ export function settingsRouteDefinitions(): RouteDefinition[] {
728
731
  handler: () => handleEnvVars(),
729
732
  },
730
733
 
734
+ // Platform config (GET / PUT)
735
+ {
736
+ endpoint: "config/platform",
737
+ method: "GET",
738
+ policyKey: "config/platform:GET",
739
+ handler: () => {
740
+ const raw = loadRawConfig();
741
+ const platform = (raw?.platform ?? {}) as Record<string, unknown>;
742
+ const baseUrl =
743
+ (platform.baseUrl as string | undefined) || getPlatformBaseUrl();
744
+ return Response.json({ baseUrl, success: true });
745
+ },
746
+ },
747
+ {
748
+ endpoint: "config/platform",
749
+ method: "PUT",
750
+ policyKey: "config/platform",
751
+ handler: async ({ req }) => {
752
+ try {
753
+ const body = (await req.json()) as { baseUrl?: string };
754
+ const value = (body.baseUrl ?? "").trim().replace(/\/+$/, "");
755
+ const raw = loadRawConfig();
756
+ const platform = (raw?.platform ?? {}) as Record<string, unknown>;
757
+ platform.baseUrl = value || undefined;
758
+ saveRawConfig({ ...raw, platform });
759
+ return Response.json({ baseUrl: value, success: true });
760
+ } catch (err) {
761
+ const message = err instanceof Error ? err.message : String(err);
762
+ log.error({ err }, "Failed to update platform config via HTTP");
763
+ return Response.json(
764
+ { baseUrl: "", success: false, error: message },
765
+ { status: 500 },
766
+ );
767
+ }
768
+ },
769
+ },
770
+
731
771
  // Ingress config (GET / PUT)
732
772
  {
733
773
  endpoint: "integrations/ingress/config",
@@ -0,0 +1,96 @@
1
+ /**
2
+ * HTTP route definitions for message text-to-speech synthesis.
3
+ *
4
+ * POST /v1/messages/:id/tts?conversationId=... — synthesize message text to audio
5
+ *
6
+ * Gated behind the `feature_flags.message-tts.enabled` assistant feature flag.
7
+ * Uses Fish Audio for synthesis when configured.
8
+ */
9
+
10
+ import { synthesizeWithFishAudio } from "../../calls/fish-audio-client.js";
11
+ import { sanitizeForTts } from "../../calls/tts-text-sanitizer.js";
12
+ import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
13
+ import { getConfig } from "../../config/loader.js";
14
+ import { getMessageContent } from "../../daemon/handlers/conversation-history.js";
15
+ import { getLogger } from "../../util/logger.js";
16
+ import { httpError } from "../http-errors.js";
17
+ import type { RouteDefinition } from "../http-router.js";
18
+
19
+ const log = getLogger("tts-routes");
20
+
21
+ const MESSAGE_TTS_FLAG = "feature_flags.message-tts.enabled" as const;
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Route definitions
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export function ttsRouteDefinitions(): RouteDefinition[] {
28
+ return [
29
+ {
30
+ endpoint: "messages/:id/tts",
31
+ method: "POST",
32
+ policyKey: "messages/tts",
33
+ handler: async ({ url, params }) => {
34
+ const config = getConfig();
35
+
36
+ if (!isAssistantFeatureFlagEnabled(MESSAGE_TTS_FLAG, config)) {
37
+ return httpError("FORBIDDEN", "Message TTS is not enabled", 403);
38
+ }
39
+
40
+ const messageId = params.id;
41
+ const conversationId =
42
+ url.searchParams.get("conversationId") ?? undefined;
43
+
44
+ const result = getMessageContent(messageId, conversationId);
45
+ if (!result) {
46
+ return httpError("NOT_FOUND", `Message ${messageId} not found`, 404);
47
+ }
48
+
49
+ if (!result.text) {
50
+ return httpError("BAD_REQUEST", "Message has no text content", 400);
51
+ }
52
+
53
+ const sanitizedText = sanitizeForTts(result.text);
54
+ if (!sanitizedText.trim()) {
55
+ return httpError(
56
+ "BAD_REQUEST",
57
+ "Message has no speakable text content",
58
+ 400,
59
+ );
60
+ }
61
+
62
+ const { fishAudio } = config;
63
+ if (!fishAudio?.referenceId) {
64
+ return httpError(
65
+ "SERVICE_UNAVAILABLE",
66
+ "Fish Audio TTS is not configured",
67
+ 503,
68
+ );
69
+ }
70
+
71
+ try {
72
+ const audioBuffer = await synthesizeWithFishAudio(
73
+ sanitizedText,
74
+ fishAudio,
75
+ );
76
+
77
+ const format = fishAudio.format ?? "mp3";
78
+ const contentType =
79
+ format === "wav"
80
+ ? "audio/wav"
81
+ : format === "opus"
82
+ ? "audio/opus"
83
+ : "audio/mpeg";
84
+
85
+ return new Response(new Uint8Array(audioBuffer), {
86
+ status: 200,
87
+ headers: { "Content-Type": contentType },
88
+ });
89
+ } catch (err) {
90
+ log.error({ err, messageId }, "TTS synthesis failed");
91
+ return httpError("INTERNAL_ERROR", "TTS synthesis failed", 502);
92
+ }
93
+ },
94
+ },
95
+ ];
96
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Upgrade broadcast endpoint — publishes service group update lifecycle
3
- * events (starting / complete) to all connected SSE clients.
3
+ * events (starting / progress / complete) to all connected SSE clients.
4
4
  *
5
5
  * Protected by a route policy restricting access to gateway service
6
6
  * principals only (`svc_gateway` with `internal.write` scope), following
@@ -11,6 +11,7 @@
11
11
 
12
12
  import type {
13
13
  ServiceGroupUpdateComplete,
14
+ ServiceGroupUpdateProgress,
14
15
  ServiceGroupUpdateStarting,
15
16
  } from "../../daemon/message-types/upgrades.js";
16
17
  import { buildAssistantEvent } from "../assistant-event.js";
@@ -86,6 +87,29 @@ export function upgradeBroadcastRouteDefinitions(): RouteDefinition[] {
86
87
  return Response.json({ ok: true });
87
88
  }
88
89
 
90
+ if (type === "progress") {
91
+ const { statusMessage } = body as { statusMessage?: unknown };
92
+
93
+ if (typeof statusMessage !== "string" || statusMessage.length === 0) {
94
+ return httpError(
95
+ "BAD_REQUEST",
96
+ "statusMessage is required and must be a non-empty string",
97
+ 400,
98
+ );
99
+ }
100
+
101
+ const message: ServiceGroupUpdateProgress = {
102
+ type: "service_group_update_progress",
103
+ statusMessage,
104
+ };
105
+
106
+ await assistantEventHub.publish(
107
+ buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, message),
108
+ );
109
+
110
+ return Response.json({ ok: true });
111
+ }
112
+
89
113
  if (type === "complete") {
90
114
  const { installedVersion, success, rolledBackToVersion } = body as {
91
115
  installedVersion?: unknown;
@@ -142,7 +166,7 @@ export function upgradeBroadcastRouteDefinitions(): RouteDefinition[] {
142
166
 
143
167
  return httpError(
144
168
  "BAD_REQUEST",
145
- 'type must be "starting" or "complete"',
169
+ 'type must be "starting", "progress", or "complete"',
146
170
  400,
147
171
  );
148
172
  },
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Workspace commit endpoint — creates a git commit in the workspace
3
+ * directory with all pending changes.
4
+ *
5
+ * Protected by a route policy restricting access to gateway service
6
+ * principals only (`svc_gateway` with `internal.write` scope), following
7
+ * the same pattern as other gateway-forwarded control-plane endpoints.
8
+ */
9
+
10
+ import { getWorkspaceDir } from "../../util/platform.js";
11
+ import { getWorkspaceGitService } from "../../workspace/git-service.js";
12
+ import { httpError } from "../http-errors.js";
13
+ import type { RouteDefinition } from "../http-router.js";
14
+
15
+ export function workspaceCommitRouteDefinitions(): RouteDefinition[] {
16
+ return [
17
+ {
18
+ endpoint: "admin/workspace-commit",
19
+ method: "POST",
20
+ handler: async ({ req }) => {
21
+ let body: unknown;
22
+ try {
23
+ body = await req.json();
24
+ } catch {
25
+ return httpError("BAD_REQUEST", "Invalid JSON body", 400);
26
+ }
27
+
28
+ if (!body || typeof body !== "object") {
29
+ return httpError(
30
+ "BAD_REQUEST",
31
+ "Request body must be a JSON object",
32
+ 400,
33
+ );
34
+ }
35
+
36
+ const { message } = body as { message?: unknown };
37
+
38
+ if (typeof message !== "string" || message.length === 0) {
39
+ return httpError(
40
+ "BAD_REQUEST",
41
+ "message is required and must be a non-empty string",
42
+ 400,
43
+ );
44
+ }
45
+
46
+ try {
47
+ await getWorkspaceGitService(getWorkspaceDir()).commitChanges(
48
+ message,
49
+ );
50
+ return Response.json({ ok: true });
51
+ } catch (err) {
52
+ const detail = err instanceof Error ? err.message : "Unknown error";
53
+ return httpError(
54
+ "INTERNAL_ERROR",
55
+ `Workspace commit failed: ${detail}`,
56
+ 500,
57
+ );
58
+ }
59
+ },
60
+ },
61
+ ];
62
+ }
@@ -190,9 +190,30 @@ describe("isTextMimeType", () => {
190
190
  expect(isTextMimeType("video/mp4")).toBe(false);
191
191
  });
192
192
 
193
- test("application/octet-stream is not text", () => {
193
+ test("application/octet-stream is not text without filename", () => {
194
194
  expect(isTextMimeType("application/octet-stream")).toBe(false);
195
195
  });
196
+
197
+ test("application/octet-stream with .py filename is text", () => {
198
+ expect(isTextMimeType("application/octet-stream", "script.py")).toBe(true);
199
+ });
200
+
201
+ test("application/octet-stream with .go filename is text", () => {
202
+ expect(isTextMimeType("application/octet-stream", "main.go")).toBe(true);
203
+ });
204
+
205
+ test("application/octet-stream with .rs filename is text", () => {
206
+ expect(isTextMimeType("application/octet-stream", "lib.rs")).toBe(true);
207
+ });
208
+
209
+ test("application/octet-stream with unknown extension is not text", () => {
210
+ expect(isTextMimeType("application/octet-stream", "data.bin")).toBe(false);
211
+ });
212
+
213
+ test("extension fallback only applies to application/octet-stream", () => {
214
+ // A binary plist has a specific MIME type — extension should not override it
215
+ expect(isTextMimeType("application/x-plist", "Info.plist")).toBe(false);
216
+ });
196
217
  });
197
218
 
198
219
  // ===========================================================================
@@ -119,7 +119,7 @@ function handleWorkspaceFile(ctx: RouteContext): Response {
119
119
  }
120
120
 
121
121
  const mimeType = Bun.file(resolved).type;
122
- const isText = isTextMimeType(mimeType);
122
+ const isText = isTextMimeType(mimeType, basename(resolved));
123
123
  const isBinary = !isText;
124
124
 
125
125
  let content: string | undefined = undefined;
@@ -85,10 +85,94 @@ const TEXT_MIME_PREFIXES = [
85
85
  "application/x-yaml",
86
86
  "application/toml",
87
87
  "application/x-sh",
88
+ "application/x-httpd-php",
89
+ "application/x-perl",
90
+ "application/x-sql",
91
+ "application/x-tex",
92
+ "application/vnd.dart",
88
93
  ];
89
94
 
90
- export function isTextMimeType(mimeType: string): boolean {
91
- return TEXT_MIME_PREFIXES.some((prefix) => mimeType.startsWith(prefix));
95
+ /**
96
+ * File extensions that are known text/code files but that Bun's MIME
97
+ * detection reports as `application/octet-stream`.
98
+ */
99
+ const TEXT_FILE_EXTENSIONS = new Set([
100
+ // Programming languages
101
+ "py",
102
+ "rb",
103
+ "go",
104
+ "rs",
105
+ "swift",
106
+ "kt",
107
+ "kts",
108
+ "cs",
109
+ "scala",
110
+ "ex",
111
+ "exs",
112
+ "erl",
113
+ "hs",
114
+ "clj",
115
+ "cljs",
116
+ "jl",
117
+ "zig",
118
+ "nim",
119
+ "v",
120
+ "sol",
121
+ "r",
122
+ "java",
123
+ "lua",
124
+ // Shell / scripting
125
+ "bash",
126
+ "zsh",
127
+ "fish",
128
+ "ps1",
129
+ "bat",
130
+ "cmd",
131
+ "awk",
132
+ // Web frameworks
133
+ "vue",
134
+ "svelte",
135
+ "scss",
136
+ "sass",
137
+ "less",
138
+ // Config / data
139
+ "cfg",
140
+ "conf",
141
+ "ini",
142
+ "properties",
143
+ "env",
144
+ "gradle",
145
+ "cmake",
146
+ // Markup / docs
147
+ "rst",
148
+ "adoc",
149
+ "org",
150
+ "tex",
151
+ "latex",
152
+ // Other text formats
153
+ "graphql",
154
+ "gql",
155
+ "proto",
156
+ "tf",
157
+ "hcl",
158
+ "diff",
159
+ "patch",
160
+ "log",
161
+ "lock",
162
+ ]);
163
+
164
+ export function isTextMimeType(mimeType: string, fileName?: string): boolean {
165
+ if (TEXT_MIME_PREFIXES.some((prefix) => mimeType.startsWith(prefix))) {
166
+ return true;
167
+ }
168
+ // Only fall back to extension check when the MIME type is genuinely unknown.
169
+ // Specific MIME types (e.g. application/x-plist for binary plists) should be
170
+ // trusted over the extension — overriding them risks corrupting binary files.
171
+ if (fileName && mimeType === "application/octet-stream") {
172
+ const ext = fileName.split(".").pop()?.toLowerCase();
173
+ if (ext && TEXT_FILE_EXTENSIONS.has(ext)) return true;
174
+ }
175
+ return false;
92
176
  }
93
177
 
94
178
  export const MAX_INLINE_TEXT_SIZE = 2 * 1024 * 1024; // 2 MB