@vellumai/assistant 0.4.46 → 0.4.49

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 (382) hide show
  1. package/ARCHITECTURE.md +7 -7
  2. package/README.md +2 -23
  3. package/docs/architecture/integrations.md +45 -41
  4. package/docs/architecture/keychain-broker.md +3 -3
  5. package/docs/architecture/security.md +5 -5
  6. package/docs/runbook-trusted-contacts.md +3 -8
  7. package/hook-templates/debug-prompt-logger/hook.json +1 -1
  8. package/hook-templates/debug-prompt-logger/run.sh +1 -3
  9. package/package.json +1 -1
  10. package/src/__tests__/actor-token-service.test.ts +0 -1
  11. package/src/__tests__/anthropic-provider.test.ts +156 -0
  12. package/src/__tests__/approval-cascade.test.ts +810 -0
  13. package/src/__tests__/approval-primitive.test.ts +0 -1
  14. package/src/__tests__/approval-routes-http.test.ts +2 -0
  15. package/src/__tests__/assistant-attachments.test.ts +12 -34
  16. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
  17. package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
  18. package/src/__tests__/browser-fill-credential.test.ts +5 -2
  19. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  20. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
  21. package/src/__tests__/channel-guardian.test.ts +0 -2
  22. package/src/__tests__/channel-readiness-routes.test.ts +35 -25
  23. package/src/__tests__/channel-readiness-service.test.ts +10 -9
  24. package/src/__tests__/checker.test.ts +9 -29
  25. package/src/__tests__/cli.test.ts +23 -0
  26. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  27. package/src/__tests__/computer-use-tools.test.ts +2 -19
  28. package/src/__tests__/config-watcher.test.ts +0 -1
  29. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  30. package/src/__tests__/context-image-dimensions.test.ts +332 -0
  31. package/src/__tests__/context-token-estimator.test.ts +196 -13
  32. package/src/__tests__/conversation-attention-store.test.ts +0 -1
  33. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  34. package/src/__tests__/conversation-routes-guardian-reply.test.ts +144 -0
  35. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  36. package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
  37. package/src/__tests__/credential-broker-server-use.test.ts +22 -21
  38. package/src/__tests__/credential-broker.test.ts +2 -1
  39. package/src/__tests__/credential-metadata-store.test.ts +239 -26
  40. package/src/__tests__/credential-resolve.test.ts +5 -4
  41. package/src/__tests__/credential-security-e2e.test.ts +8 -8
  42. package/src/__tests__/credential-security-invariants.test.ts +111 -7
  43. package/src/__tests__/credential-vault-unit.test.ts +287 -54
  44. package/src/__tests__/credential-vault.test.ts +406 -12
  45. package/src/__tests__/credentials-cli.test.ts +82 -6
  46. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  47. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  48. package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
  49. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  50. package/src/__tests__/gemini-image-service.test.ts +75 -45
  51. package/src/__tests__/gemini-provider.test.ts +9 -6
  52. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
  53. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
  54. package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
  55. package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
  56. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
  57. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
  58. package/src/__tests__/guardian-grant-minting.test.ts +35 -0
  59. package/src/__tests__/guardian-routing-invariants.test.ts +0 -1
  60. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
  61. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
  62. package/src/__tests__/heartbeat-service.test.ts +0 -1
  63. package/src/__tests__/host-cu-proxy.test.ts +629 -0
  64. package/src/__tests__/host-shell-tool.test.ts +27 -15
  65. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  66. package/src/__tests__/ingress-url-consistency.test.ts +14 -21
  67. package/src/__tests__/integration-status.test.ts +38 -25
  68. package/src/__tests__/intent-routing.test.ts +0 -1
  69. package/src/__tests__/invite-routes-http.test.ts +10 -9
  70. package/src/__tests__/keychain-broker-client.test.ts +11 -43
  71. package/src/__tests__/managed-proxy-context.test.ts +5 -3
  72. package/src/__tests__/media-generate-image.test.ts +63 -2
  73. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
  74. package/src/__tests__/messaging-send-tool.test.ts +4 -6
  75. package/src/__tests__/notification-routing-intent.test.ts +0 -1
  76. package/src/__tests__/oauth-cli.test.ts +373 -14
  77. package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
  78. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  79. package/src/__tests__/oauth-store.test.ts +756 -0
  80. package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
  81. package/src/__tests__/provider-error-scenarios.test.ts +0 -1
  82. package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
  83. package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
  84. package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
  85. package/src/__tests__/public-ingress-urls.test.ts +15 -21
  86. package/src/__tests__/recording-handler.test.ts +3 -4
  87. package/src/__tests__/registry.test.ts +2 -2
  88. package/src/__tests__/runtime-events-sse.test.ts +55 -7
  89. package/src/__tests__/schedule-store.test.ts +0 -1
  90. package/src/__tests__/scheduler-recurrence.test.ts +0 -1
  91. package/src/__tests__/schema-transforms.test.ts +226 -0
  92. package/src/__tests__/scoped-approval-grants.test.ts +0 -1
  93. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
  94. package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
  95. package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
  96. package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
  97. package/src/__tests__/secret-ingress-handler.test.ts +0 -1
  98. package/src/__tests__/secret-onetime-send.test.ts +5 -3
  99. package/src/__tests__/send-endpoint-busy.test.ts +21 -6
  100. package/src/__tests__/sequence-store.test.ts +0 -1
  101. package/src/__tests__/session-init.benchmark.test.ts +4 -5
  102. package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
  103. package/src/__tests__/skill-include-graph.test.ts +66 -0
  104. package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
  105. package/src/__tests__/skill-load-tool.test.ts +149 -1
  106. package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
  107. package/src/__tests__/skills-uninstall.test.ts +3 -3
  108. package/src/__tests__/skills.test.ts +3 -12
  109. package/src/__tests__/slack-channel-config.test.ts +76 -11
  110. package/src/__tests__/slack-share-routes.test.ts +17 -14
  111. package/src/__tests__/system-prompt.test.ts +0 -1
  112. package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
  113. package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
  114. package/src/__tests__/terminal-tools.test.ts +4 -3
  115. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  116. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  117. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
  118. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  119. package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
  120. package/src/__tests__/tool-executor.test.ts +0 -1
  121. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
  122. package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
  123. package/src/__tests__/trust-store.test.ts +1 -22
  124. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  125. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  126. package/src/__tests__/twilio-config.test.ts +2 -1
  127. package/src/__tests__/twilio-provider.test.ts +4 -2
  128. package/src/__tests__/twilio-routes.test.ts +5 -20
  129. package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
  130. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  131. package/src/agent/ax-tree-compaction.test.ts +235 -0
  132. package/src/agent/loop.ts +76 -130
  133. package/src/calls/call-domain.ts +8 -10
  134. package/src/calls/relay-server.ts +9 -13
  135. package/src/calls/twilio-config.ts +4 -8
  136. package/src/calls/twilio-provider.ts +2 -1
  137. package/src/calls/twilio-rest.ts +2 -1
  138. package/src/calls/twilio-routes.ts +1 -2
  139. package/src/calls/voice-ingress-preflight.ts +1 -1
  140. package/src/cli/commands/browser-relay.ts +46 -15
  141. package/src/cli/commands/completions.ts +0 -3
  142. package/src/cli/commands/credentials.ts +110 -23
  143. package/src/cli/commands/oauth/apps.ts +255 -0
  144. package/src/cli/commands/oauth/connections.ts +299 -0
  145. package/src/cli/commands/oauth/index.ts +52 -0
  146. package/src/cli/commands/oauth/providers.ts +242 -0
  147. package/src/cli/commands/skills.ts +4 -338
  148. package/src/cli/program.ts +1 -5
  149. package/src/cli/reference.ts +1 -3
  150. package/src/cli.ts +3 -2
  151. package/src/config/assistant-feature-flags.ts +0 -3
  152. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  153. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
  154. package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
  155. package/src/config/bundled-skills/computer-use/TOOLS.json +22 -4
  156. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
  157. package/src/config/bundled-skills/gmail/SKILL.md +4 -4
  158. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
  159. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
  160. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
  161. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
  162. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
  163. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
  164. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
  165. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
  166. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
  167. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
  168. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
  169. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
  170. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
  171. package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
  172. package/src/config/bundled-skills/google-calendar/calendar-client.ts +90 -44
  173. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
  174. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
  175. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
  176. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
  177. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
  178. package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
  179. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
  180. package/src/config/bundled-skills/messaging/SKILL.md +6 -6
  181. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
  182. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
  183. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
  184. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
  185. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
  186. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
  187. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
  188. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
  189. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
  190. package/src/config/bundled-skills/messaging/tools/shared.ts +12 -15
  191. package/src/config/bundled-skills/settings/SKILL.md +1 -1
  192. package/src/config/bundled-skills/settings/TOOLS.json +2 -8
  193. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
  194. package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
  195. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
  196. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
  197. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
  198. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
  199. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
  200. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
  201. package/src/config/env-registry.ts +14 -83
  202. package/src/config/env.ts +11 -50
  203. package/src/config/feature-flag-registry.json +16 -16
  204. package/src/config/schema.ts +3 -1
  205. package/src/config/skills.ts +21 -2
  206. package/src/context/image-dimensions.ts +229 -0
  207. package/src/context/token-estimator.ts +75 -12
  208. package/src/context/window-manager.ts +49 -10
  209. package/src/daemon/assistant-attachments.ts +1 -13
  210. package/src/daemon/guardian-action-generators.ts +4 -5
  211. package/src/daemon/handlers/config-ingress.ts +8 -33
  212. package/src/daemon/handlers/config-slack-channel.ts +76 -56
  213. package/src/daemon/handlers/config-telegram.ts +53 -24
  214. package/src/daemon/handlers/sessions.ts +10 -24
  215. package/src/daemon/handlers/shared.ts +0 -130
  216. package/src/daemon/host-cu-proxy.ts +401 -0
  217. package/src/daemon/lifecycle.ts +39 -63
  218. package/src/daemon/message-protocol.ts +3 -0
  219. package/src/daemon/message-types/computer-use.ts +2 -119
  220. package/src/daemon/message-types/host-cu.ts +19 -0
  221. package/src/daemon/message-types/integrations.ts +1 -0
  222. package/src/daemon/message-types/messages.ts +3 -0
  223. package/src/daemon/server.ts +14 -21
  224. package/src/daemon/session-agent-loop-handlers.ts +2 -0
  225. package/src/daemon/session-attachments.ts +1 -2
  226. package/src/daemon/session-messaging.ts +3 -1
  227. package/src/daemon/session-slash.ts +1 -1
  228. package/src/daemon/session-surfaces.ts +40 -28
  229. package/src/daemon/session-tool-setup.ts +20 -11
  230. package/src/daemon/session.ts +139 -16
  231. package/src/daemon/tool-side-effects.ts +2 -8
  232. package/src/daemon/watch-handler.ts +2 -2
  233. package/src/email/providers/index.ts +2 -1
  234. package/src/events/tool-metrics-listener.ts +2 -2
  235. package/src/hooks/manager.ts +1 -4
  236. package/src/inbound/public-ingress-urls.ts +7 -7
  237. package/src/instrument.ts +15 -1
  238. package/src/logfire.ts +16 -5
  239. package/src/media/app-icon-generator.ts +30 -4
  240. package/src/media/avatar-router.ts +26 -3
  241. package/src/media/gemini-image-service.ts +28 -2
  242. package/src/memory/conversation-key-store.ts +21 -0
  243. package/src/memory/db-init.ts +4 -0
  244. package/src/memory/guardian-action-store.ts +1 -1
  245. package/src/memory/migrations/149-oauth-tables.ts +60 -0
  246. package/src/memory/migrations/index.ts +1 -0
  247. package/src/memory/schema/guardian.ts +1 -1
  248. package/src/memory/schema/index.ts +1 -0
  249. package/src/memory/schema/oauth.ts +65 -0
  250. package/src/messaging/provider.ts +19 -13
  251. package/src/messaging/providers/gmail/adapter.ts +40 -23
  252. package/src/messaging/providers/gmail/client.ts +283 -122
  253. package/src/messaging/providers/gmail/people-client.ts +32 -24
  254. package/src/messaging/providers/slack/adapter.ts +29 -19
  255. package/src/messaging/providers/slack/client.ts +265 -78
  256. package/src/messaging/providers/telegram-bot/adapter.ts +19 -18
  257. package/src/messaging/providers/whatsapp/adapter.ts +17 -11
  258. package/src/messaging/registry.ts +2 -31
  259. package/src/notifications/copy-composer.ts +0 -5
  260. package/src/notifications/signal.ts +4 -5
  261. package/src/oauth/byo-connection.test.ts +537 -0
  262. package/src/oauth/byo-connection.ts +128 -0
  263. package/src/oauth/connect-orchestrator.ts +139 -56
  264. package/src/oauth/connect-types.ts +17 -23
  265. package/src/oauth/connection-resolver.ts +58 -0
  266. package/src/oauth/connection.ts +38 -0
  267. package/src/oauth/manual-token-connection.ts +104 -0
  268. package/src/oauth/oauth-store.ts +496 -0
  269. package/src/oauth/platform-connection.test.ts +192 -0
  270. package/src/oauth/platform-connection.ts +111 -0
  271. package/src/oauth/provider-behaviors.ts +124 -0
  272. package/src/oauth/scope-policy.ts +9 -2
  273. package/src/oauth/seed-providers.ts +161 -0
  274. package/src/oauth/token-persistence.ts +74 -78
  275. package/src/permissions/checker.ts +8 -4
  276. package/src/permissions/defaults.ts +0 -1
  277. package/src/permissions/prompter.ts +10 -1
  278. package/src/permissions/trust-store.ts +13 -0
  279. package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
  280. package/src/prompts/system-prompt.ts +70 -45
  281. package/src/providers/anthropic/client.ts +133 -24
  282. package/src/providers/gemini/client.ts +15 -6
  283. package/src/providers/managed-proxy/constants.ts +2 -2
  284. package/src/providers/managed-proxy/context.ts +5 -1
  285. package/src/providers/ratelimit.ts +17 -0
  286. package/src/providers/registry.ts +2 -2
  287. package/src/providers/retry.ts +1 -27
  288. package/src/runtime/AGENTS.md +17 -0
  289. package/src/runtime/auth/route-policy.ts +0 -3
  290. package/src/runtime/channel-invite-transports/telegram.ts +2 -1
  291. package/src/runtime/channel-readiness-service.ts +168 -195
  292. package/src/runtime/channel-readiness-types.ts +4 -0
  293. package/src/runtime/channel-reply-delivery.ts +0 -40
  294. package/src/runtime/gateway-client.ts +0 -7
  295. package/src/runtime/guardian-action-conversation-turn.ts +1 -3
  296. package/src/runtime/guardian-action-followup-executor.ts +1 -1
  297. package/src/runtime/guardian-action-message-composer.ts +3 -23
  298. package/src/runtime/http-server.ts +17 -10
  299. package/src/runtime/http-types.ts +2 -3
  300. package/src/runtime/middleware/rate-limiter.ts +74 -20
  301. package/src/runtime/middleware/twilio-validation.ts +1 -11
  302. package/src/runtime/pending-interactions.ts +14 -12
  303. package/src/runtime/routes/channel-delivery-routes.ts +0 -1
  304. package/src/runtime/routes/channel-readiness-routes.ts +2 -0
  305. package/src/runtime/routes/conversation-routes.ts +73 -19
  306. package/src/runtime/routes/diagnostics-routes.ts +11 -9
  307. package/src/runtime/routes/events-routes.ts +21 -11
  308. package/src/runtime/routes/guardian-approval-interception.ts +20 -5
  309. package/src/runtime/routes/host-cu-routes.ts +97 -0
  310. package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
  311. package/src/runtime/routes/integrations/slack/share.ts +6 -6
  312. package/src/runtime/routes/integrations/twilio.ts +6 -5
  313. package/src/runtime/routes/log-export-routes.ts +126 -8
  314. package/src/runtime/routes/secret-routes.ts +3 -2
  315. package/src/runtime/routes/settings-routes.ts +113 -48
  316. package/src/runtime/routes/surface-action-routes.ts +1 -1
  317. package/src/runtime/routes/watch-routes.ts +128 -0
  318. package/src/schedule/integration-status.ts +10 -8
  319. package/src/security/credential-key.ts +14 -0
  320. package/src/security/keychain-broker-client.ts +5 -6
  321. package/src/security/oauth2.ts +1 -1
  322. package/src/security/token-manager.ts +145 -43
  323. package/src/skills/catalog-install.ts +358 -0
  324. package/src/skills/include-graph.ts +32 -0
  325. package/src/telegram/bot-username.ts +2 -3
  326. package/src/tools/apps/definitions.ts +0 -5
  327. package/src/tools/assets/materialize.ts +0 -5
  328. package/src/tools/assets/search.ts +0 -5
  329. package/src/tools/browser/headless-browser.ts +1 -67
  330. package/src/tools/browser/network-recorder.ts +1 -1
  331. package/src/tools/browser/network-recording-types.ts +1 -1
  332. package/src/tools/claude-code/claude-code.ts +0 -5
  333. package/src/tools/computer-use/definitions.ts +46 -11
  334. package/src/tools/computer-use/registry.ts +4 -5
  335. package/src/tools/credentials/broker.ts +5 -4
  336. package/src/tools/credentials/metadata-store.ts +22 -74
  337. package/src/tools/credentials/resolve.ts +2 -1
  338. package/src/tools/credentials/vault.ts +139 -151
  339. package/src/tools/filesystem/edit.ts +1 -6
  340. package/src/tools/filesystem/read.ts +0 -5
  341. package/src/tools/filesystem/write.ts +1 -6
  342. package/src/tools/host-filesystem/edit.ts +1 -6
  343. package/src/tools/host-filesystem/read.ts +1 -6
  344. package/src/tools/host-filesystem/write.ts +1 -6
  345. package/src/tools/mcp/mcp-tool-factory.ts +18 -1
  346. package/src/tools/memory/definitions.ts +0 -5
  347. package/src/tools/network/web-fetch.ts +0 -5
  348. package/src/tools/network/web-search.ts +0 -5
  349. package/src/tools/registry.ts +2 -7
  350. package/src/tools/schema-transforms.ts +99 -0
  351. package/src/tools/skills/load.ts +62 -8
  352. package/src/tools/swarm/delegate.ts +0 -5
  353. package/src/tools/system/avatar-generator.ts +0 -5
  354. package/src/tools/ui-surface/definitions.ts +0 -15
  355. package/src/tools/watch/screen-watch.ts +0 -5
  356. package/src/tools/watch/watch-state.ts +0 -12
  357. package/src/util/logger.ts +7 -41
  358. package/src/util/platform.ts +9 -28
  359. package/src/version.ts +10 -0
  360. package/src/watcher/providers/github.ts +51 -52
  361. package/src/watcher/providers/gmail.ts +88 -80
  362. package/src/watcher/providers/google-calendar.ts +94 -86
  363. package/src/watcher/providers/linear.ts +87 -93
  364. package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
  365. package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
  366. package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
  367. package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
  368. package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
  369. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
  370. package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
  371. package/src/cli/commands/dev.ts +0 -129
  372. package/src/cli/commands/map.ts +0 -391
  373. package/src/cli/commands/oauth.ts +0 -77
  374. package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +0 -16
  375. package/src/daemon/computer-use-session.ts +0 -1020
  376. package/src/daemon/ride-shotgun-handler.ts +0 -567
  377. package/src/oauth/provider-profiles.ts +0 -192
  378. package/src/prompts/computer-use-prompt.ts +0 -98
  379. package/src/runtime/routes/computer-use-routes.ts +0 -641
  380. package/src/runtime/telegram-streaming-delivery.test.ts +0 -597
  381. package/src/runtime/telegram-streaming-delivery.ts +0 -383
  382. package/src/tools/computer-use/request-computer-control.ts +0 -61
@@ -186,6 +186,7 @@ async function handleDiagnosticsExport(body: {
186
186
  // been persisted — the user message and in-flight tool/usage data are
187
187
  // still captured.
188
188
  let anchorMessage;
189
+ let anchorIsFallback = false;
189
190
  if (anchorMessageId) {
190
191
  anchorMessage = db
191
192
  .select()
@@ -220,6 +221,7 @@ async function handleDiagnosticsExport(body: {
220
221
  .orderBy(desc(messages.createdAt))
221
222
  .limit(1)
222
223
  .get();
224
+ anchorIsFallback = true;
223
225
  }
224
226
 
225
227
  // 2. Compute the export time range.
@@ -250,15 +252,15 @@ async function handleDiagnosticsExport(body: {
250
252
  rangeStart =
251
253
  precedingUserMessage?.createdAt ?? anchorMessage.createdAt - 2000;
252
254
 
253
- // When the anchor is not an assistant message (e.g. the fallback "any
254
- // message" path hit because the assistant reply hasn't been persisted
255
- // yet), extend the range to the current time so in-flight tool
256
- // invocations and usage recorded after the user message are captured.
257
- const anchorIsAssistant = anchorMessage.role === "assistant";
258
- rangeEnd = anchorIsAssistant ? anchorMessage.createdAt : now;
259
- usageRangeEnd = anchorIsAssistant
260
- ? anchorMessage.createdAt + 5000
261
- : now + 5000;
255
+ // When the anchor was selected via the fallback "any message" path
256
+ // (because the assistant reply hasn't been persisted yet), extend the
257
+ // range to the current time so in-flight tool invocations and usage
258
+ // recorded after the user message are captured. An explicit anchor to a
259
+ // non-assistant message uses the message's own timestamp.
260
+ rangeEnd = anchorIsFallback ? now : anchorMessage.createdAt;
261
+ usageRangeEnd = anchorIsFallback
262
+ ? now + 5000
263
+ : anchorMessage.createdAt + 5000;
262
264
  } else {
263
265
  // No messages at all — use the current time so we capture any
264
266
  // in-flight LLM usage or tool invocations.
@@ -7,12 +7,17 @@
7
7
  * is called. The AuthContext is threaded through from the HTTP server
8
8
  * layer, so no additional actor-token verification is needed here.
9
9
  *
10
- * Subscribers receive all assistant events scoped to the given conversation.
10
+ * When `conversationKey` is provided, subscribers receive events scoped to
11
+ * that conversation. When omitted, subscribers receive events from ALL
12
+ * conversations for this assistant (unfiltered).
11
13
  */
12
14
 
13
15
  import { getOrCreateConversation } from "../../memory/conversation-key-store.js";
14
16
  import { formatSseFrame, formatSseHeartbeat } from "../assistant-event.js";
15
- import type { AssistantEventSubscription } from "../assistant-event-hub.js";
17
+ import type {
18
+ AssistantEventFilter,
19
+ AssistantEventSubscription,
20
+ } from "../assistant-event-hub.js";
16
21
  import {
17
22
  AssistantEventHub,
18
23
  assistantEventHub,
@@ -26,10 +31,12 @@ import type { RouteDefinition } from "../http-router.js";
26
31
  const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
27
32
 
28
33
  /**
29
- * Stream assistant events as Server-Sent Events for a specific conversation.
34
+ * Stream assistant events as Server-Sent Events.
30
35
  *
31
36
  * Query params:
32
- * conversationKey -- required; scopes the stream to one conversation.
37
+ * conversationKey -- optional; when provided, scopes the stream to one
38
+ * conversation. When omitted, the stream delivers events
39
+ * from ALL conversations for this assistant.
33
40
  *
34
41
  * Options (for testing):
35
42
  * hub -- override the event hub (defaults to process singleton).
@@ -56,15 +63,21 @@ export function handleSubscribeAssistantEvents(
56
63
  // scope and principal type requirements.
57
64
 
58
65
  const conversationKey = url.searchParams.get("conversationKey");
59
- if (!conversationKey) {
60
- return httpError("BAD_REQUEST", "conversationKey is required", 400);
66
+ if (url.searchParams.has("conversationKey") && !conversationKey?.trim()) {
67
+ return httpError("BAD_REQUEST", "conversationKey must not be empty", 400);
61
68
  }
62
69
 
63
70
  const hub = options?.hub ?? assistantEventHub;
64
71
  const heartbeatIntervalMs =
65
72
  options?.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
66
73
 
67
- const mapping = getOrCreateConversation(conversationKey);
74
+ const filter: AssistantEventFilter = {
75
+ assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
76
+ };
77
+ if (conversationKey) {
78
+ const mapping = getOrCreateConversation(conversationKey);
79
+ filter.sessionId = mapping.conversationId;
80
+ }
68
81
  const encoder = new TextEncoder();
69
82
 
70
83
  // -- Eager subscribe --------------------------------------------------------
@@ -90,10 +103,7 @@ export function handleSubscribeAssistantEvents(
90
103
 
91
104
  try {
92
105
  sub = hub.subscribe(
93
- {
94
- assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
95
- sessionId: mapping.conversationId,
96
- },
106
+ filter,
97
107
  (event) => {
98
108
  const controller = controllerRef;
99
109
  if (!controller) return;
@@ -10,6 +10,7 @@ import { applyGuardianDecision } from "../../approvals/guardian-decision-primiti
10
10
  import type { ChannelId } from "../../channels/types.js";
11
11
  import type { TrustContext } from "../../daemon/session-runtime-assembly.js";
12
12
  import {
13
+ getAllPendingApprovalsByGuardianChat,
13
14
  getPendingApprovalForRequest,
14
15
  getUnresolvedApprovalForRequest,
15
16
  updateApprovalDecision,
@@ -123,7 +124,7 @@ export async function handleApprovalInterception(
123
124
  // Only guardians can approve via reaction — non-guardian reactions are
124
125
  // silently ignored to prevent self-approval.
125
126
  if (callbackData?.startsWith("reaction:")) {
126
- if (trustCtx.trustClass !== "guardian") {
127
+ if (trustCtx.trustClass !== "guardian" || !actorExternalId) {
127
128
  return { handled: true, type: "stale_ignored" };
128
129
  }
129
130
  const reactionDecision = parseReactionCallbackData(callbackData);
@@ -131,13 +132,27 @@ export async function handleApprovalInterception(
131
132
  // Unknown emoji — ignore silently
132
133
  return { handled: true, type: "stale_ignored" };
133
134
  }
134
- const pending = getApprovalInfoByConversation(conversationId);
135
- if (pending.length === 0) {
135
+
136
+ const allPending = getAllPendingApprovalsByGuardianChat(
137
+ sourceChannel,
138
+ conversationExternalId,
139
+ );
140
+ const guardianPending = allPending.filter(
141
+ (approval) => approval.guardianExternalUserId === actorExternalId,
142
+ );
143
+ if (guardianPending.length !== 1) {
136
144
  return { handled: true, type: "stale_ignored" };
137
145
  }
138
- const result = handleChannelDecision(conversationId, reactionDecision);
146
+
147
+ const result = applyGuardianDecision({
148
+ approval: guardianPending[0],
149
+ decision: reactionDecision,
150
+ actorPrincipalId: undefined,
151
+ actorExternalUserId: actorExternalId,
152
+ actorChannel: sourceChannel,
153
+ });
139
154
  if (result.applied) {
140
- return { handled: true, type: "decision_applied" };
155
+ return { handled: true, type: "guardian_decision_applied" };
141
156
  }
142
157
  return { handled: true, type: "stale_ignored" };
143
158
  }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Route handler for host CU (computer-use) result submissions.
3
+ *
4
+ * Resolves pending host CU proxy requests by requestId when the desktop
5
+ * client returns observation results via HTTP.
6
+ */
7
+ import { requireBoundGuardian } from "../auth/require-bound-guardian.js";
8
+ import type { AuthContext } from "../auth/types.js";
9
+ import { httpError } from "../http-errors.js";
10
+ import type { RouteDefinition } from "../http-router.js";
11
+ import * as pendingInteractions from "../pending-interactions.js";
12
+
13
+ /**
14
+ * POST /v1/host-cu-result — resolve a pending host CU request by requestId.
15
+ * Requires AuthContext with guardian-bound actor.
16
+ */
17
+ export async function handleHostCuResult(
18
+ req: Request,
19
+ authContext: AuthContext,
20
+ ): Promise<Response> {
21
+ const authError = requireBoundGuardian(authContext);
22
+ if (authError) return authError;
23
+
24
+ const body = (await req.json()) as {
25
+ requestId?: string;
26
+ axTree?: string;
27
+ axDiff?: string;
28
+ screenshot?: string;
29
+ screenshotWidthPx?: number;
30
+ screenshotHeightPx?: number;
31
+ screenWidthPt?: number;
32
+ screenHeightPt?: number;
33
+ executionResult?: string;
34
+ executionError?: string;
35
+ secondaryWindows?: string;
36
+ userGuidance?: string;
37
+ };
38
+
39
+ const { requestId } = body;
40
+
41
+ if (!requestId || typeof requestId !== "string") {
42
+ return httpError("BAD_REQUEST", "requestId is required", 400);
43
+ }
44
+
45
+ // Peek first (non-destructive) so we can validate the interaction kind
46
+ // without accidentally consuming a confirmation or secret interaction.
47
+ const peeked = pendingInteractions.get(requestId);
48
+ if (!peeked) {
49
+ return httpError(
50
+ "NOT_FOUND",
51
+ "No pending interaction found for this requestId",
52
+ 404,
53
+ );
54
+ }
55
+
56
+ if (peeked.kind !== "host_cu") {
57
+ return httpError(
58
+ "CONFLICT",
59
+ `Pending interaction is of kind "${peeked.kind}", expected "host_cu"`,
60
+ 409,
61
+ );
62
+ }
63
+
64
+ // Validation passed — consume the pending interaction.
65
+ const interaction = pendingInteractions.resolve(requestId)!;
66
+
67
+ interaction.session.resolveHostCu(requestId, {
68
+ axTree: body.axTree,
69
+ axDiff: body.axDiff,
70
+ screenshot: body.screenshot,
71
+ screenshotWidthPx: body.screenshotWidthPx,
72
+ screenshotHeightPx: body.screenshotHeightPx,
73
+ screenWidthPt: body.screenWidthPt,
74
+ screenHeightPt: body.screenHeightPt,
75
+ executionResult: body.executionResult,
76
+ executionError: body.executionError,
77
+ secondaryWindows: body.secondaryWindows,
78
+ userGuidance: body.userGuidance,
79
+ });
80
+
81
+ return Response.json({ accepted: true });
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Route definitions
86
+ // ---------------------------------------------------------------------------
87
+
88
+ export function hostCuRouteDefinitions(): RouteDefinition[] {
89
+ return [
90
+ {
91
+ endpoint: "host-cu-result",
92
+ method: "POST",
93
+ handler: async ({ req, authContext }) =>
94
+ handleHostCuResult(req, authContext),
95
+ },
96
+ ];
97
+ }
@@ -31,12 +31,8 @@ import type {
31
31
  ApprovalCopyGenerator,
32
32
  MessageProcessor,
33
33
  } from "../../http-types.js";
34
- import { TelegramStreamingDelivery } from "../../telegram-streaming-delivery.js";
35
34
  import { resolveRoutingState } from "../../trust-context-resolver.js";
36
- import {
37
- deliverAttachmentsOnly,
38
- deliverReplyViaCallback,
39
- } from "../channel-delivery-routes.js";
35
+ import { deliverReplyViaCallback } from "../channel-delivery-routes.js";
40
36
  import { deliverGeneratedApprovalPrompt } from "../guardian-approval-prompt.js";
41
37
 
42
38
  const log = getLogger("runtime-http");
@@ -112,12 +108,6 @@ export function processChannelMessageInBackground(
112
108
  } = params;
113
109
 
114
110
  (async () => {
115
- const boundGuardianActor = isBoundGuardianActor({
116
- trustClass: trustCtx.trustClass,
117
- guardianExternalUserId: trustCtx.guardianExternalUserId,
118
- requesterExternalUserId: trustCtx.requesterExternalUserId,
119
- });
120
-
121
111
  const typingCallbackUrl = shouldEmitTelegramTyping(
122
112
  sourceChannel,
123
113
  replyCallbackUrl,
@@ -181,16 +171,6 @@ export function processChannelMessageInBackground(
181
171
  }
182
172
  }
183
173
 
184
- const telegramStreaming =
185
- sourceChannel === "telegram" && replyCallbackUrl
186
- ? new TelegramStreamingDelivery({
187
- callbackUrl: replyCallbackUrl,
188
- chatId: externalChatId,
189
- mintBearerToken,
190
- assistantId,
191
- })
192
- : undefined;
193
-
194
174
  try {
195
175
  const cmdIntent =
196
176
  commandIntent && typeof commandIntent.type === "string"
@@ -218,9 +198,6 @@ export function processChannelMessageInBackground(
218
198
  trustContext: trustCtx,
219
199
  isInteractive: resolveRoutingState(trustCtx).promptWaitingAllowed,
220
200
  ...(cmdIntent ? { commandIntent: cmdIntent } : {}),
221
- ...(telegramStreaming
222
- ? { onEvent: (msg) => telegramStreaming.onEvent(msg) }
223
- : {}),
224
201
  },
225
202
  sourceChannel,
226
203
  sourceInterface,
@@ -228,94 +205,18 @@ export function processChannelMessageInBackground(
228
205
  deliveryCrud.linkMessage(eventId, userMessageId);
229
206
  deliveryStatus.markProcessed(eventId);
230
207
 
231
- if (telegramStreaming) {
232
- // Retrieve approval metadata from pending interactions (if any)
233
- // so approval buttons can be attached to the final streamed message.
234
- // Approval prompts are guardian-only and must never be attached for
235
- // non-guardian or unverified actors.
236
- const prompt = boundGuardianActor
237
- ? getChannelApprovalPrompt(conversationId)
238
- : undefined;
239
- const pending = boundGuardianActor
240
- ? getApprovalInfoByConversation(conversationId)
241
- : [];
242
- const approvalMeta =
243
- prompt && pending.length > 0
244
- ? buildApprovalUIMetadata(prompt, pending[0])
245
- : undefined;
246
- try {
247
- await telegramStreaming.finish(approvalMeta);
248
- deliveryChannels.updateDeliveredSegmentCount(eventId, 1);
249
- } catch (err) {
250
- log.error(
251
- { err, conversationId },
252
- "Telegram streaming finalization failed",
253
- );
254
- // Fallback: deliver approval as a standalone message so buttons
255
- // are not permanently lost when finish() fails.
256
- if (approvalMeta && replyCallbackUrl) {
257
- try {
258
- await deliverChannelReply(
259
- replyCallbackUrl,
260
- {
261
- chatId: externalChatId,
262
- text: approvalMeta.plainTextFallback ?? "Action needed:",
263
- approval: approvalMeta,
264
- assistantId,
265
- },
266
- mintBearerToken(),
267
- );
268
- } catch (fallbackErr) {
269
- log.error(
270
- { err: fallbackErr, conversationId },
271
- "Fallback approval delivery also failed",
272
- );
273
- }
274
- }
275
- }
276
- }
277
-
278
208
  if (replyCallbackUrl) {
279
- // Streaming fully succeeded — only send attachments since text
280
- // was already delivered via streaming edits.
281
- const streamingFullyDelivered =
282
- telegramStreaming?.hasDeliveredText &&
283
- telegramStreaming.finishSucceeded;
284
-
285
- if (streamingFullyDelivered) {
286
- await deliverAttachmentsOnly(
287
- conversationId,
288
- externalChatId,
289
- replyCallbackUrl,
290
- mintBearerToken(),
291
- assistantId,
292
- );
293
- } else {
294
- // Non-streaming path, or streaming partially failed (some text
295
- // was delivered but finish/finalization threw). In the partial
296
- // failure case the user has a truncated message, so we deliver
297
- // the full response to ensure nothing is lost.
298
- if (
299
- telegramStreaming?.hasDeliveredText &&
300
- !telegramStreaming.finishSucceeded
301
- ) {
302
- log.warn(
303
- { conversationId },
304
- "Telegram streaming partially failed — falling back to full text delivery",
305
- );
306
- }
307
- await deliverReplyViaCallback(
308
- conversationId,
309
- externalChatId,
310
- replyCallbackUrl,
311
- mintBearerToken(),
312
- assistantId,
313
- {
314
- onSegmentDelivered: (count) =>
315
- deliveryChannels.updateDeliveredSegmentCount(eventId, count),
316
- },
317
- );
318
- }
209
+ await deliverReplyViaCallback(
210
+ conversationId,
211
+ externalChatId,
212
+ replyCallbackUrl,
213
+ mintBearerToken(),
214
+ assistantId,
215
+ {
216
+ onSegmentDelivered: (count) =>
217
+ deliveryChannels.updateDeliveredSegmentCount(eventId, count),
218
+ },
219
+ );
319
220
  }
320
221
  } catch (err) {
321
222
  log.error(
@@ -12,6 +12,7 @@ import {
12
12
  userInfo,
13
13
  } from "../../../../messaging/providers/slack/client.js";
14
14
  import type { SlackConversation } from "../../../../messaging/providers/slack/types.js";
15
+ import { getConnectionByProvider } from "../../../../oauth/oauth-store.js";
15
16
  import { getSecureKey } from "../../../../security/secure-keys.js";
16
17
  import { getLogger } from "../../../../util/logger.js";
17
18
  import { httpError } from "../../../http-errors.js";
@@ -24,14 +25,13 @@ const log = getLogger("slack-share");
24
25
  // ---------------------------------------------------------------------------
25
26
 
26
27
  /**
27
- * Resolve the Slack bot token from secure storage.
28
- * Prefers the OAuth integration token, falls back to the legacy channel token.
28
+ * Resolve the Slack bot token from the OAuth connection store.
29
29
  */
30
30
  function resolveSlackToken(): string | undefined {
31
- return (
32
- getSecureKey("credential:integration:slack:access_token") ??
33
- getSecureKey("credential:slack_channel:bot_token")
34
- );
31
+ const conn = getConnectionByProvider("integration:slack");
32
+ return conn
33
+ ? getSecureKey(`oauth_connection/${conn.id}/access_token`)
34
+ : undefined;
35
35
  }
36
36
 
37
37
  // ---------------------------------------------------------------------------
@@ -21,6 +21,7 @@ import {
21
21
  import { loadRawConfig, saveRawConfig } from "../../../config/loader.js";
22
22
  import { syncTwilioWebhooks } from "../../../daemon/handlers/config-ingress.js";
23
23
  import type { IngressConfig } from "../../../inbound/public-ingress-urls.js";
24
+ import { credentialKey } from "../../../security/credential-key.js";
24
25
  import {
25
26
  deleteSecureKeyAsync,
26
27
  setSecureKeyAsync,
@@ -136,7 +137,7 @@ export async function handleSetTwilioCredentials(
136
137
  // validation (gateway/src/credential-reader.ts), while the assistant reads
137
138
  // from config via resolveAccountSid(). Both stores must stay in sync.
138
139
  const sidStored = await setSecureKeyAsync(
139
- "credential:twilio:account_sid",
140
+ credentialKey("twilio", "account_sid"),
140
141
  body.accountSid,
141
142
  );
142
143
  if (!sidStored) {
@@ -148,11 +149,11 @@ export async function handleSetTwilioCredentials(
148
149
  }
149
150
 
150
151
  const tokenStored = await setSecureKeyAsync(
151
- "credential:twilio:auth_token",
152
+ credentialKey("twilio", "auth_token"),
152
153
  body.authToken,
153
154
  );
154
155
  if (!tokenStored) {
155
- await deleteSecureKeyAsync("credential:twilio:account_sid");
156
+ await deleteSecureKeyAsync(credentialKey("twilio", "account_sid"));
156
157
  return Response.json({
157
158
  success: false,
158
159
  hasCredentials: false,
@@ -202,8 +203,8 @@ export async function handleSetTwilioCredentials(
202
203
  * DELETE /v1/integrations/twilio/credentials
203
204
  */
204
205
  export async function handleClearTwilioCredentials(): Promise<Response> {
205
- const r1 = await deleteSecureKeyAsync("credential:twilio:account_sid");
206
- const r2 = await deleteSecureKeyAsync("credential:twilio:auth_token");
206
+ const r1 = await deleteSecureKeyAsync(credentialKey("twilio", "account_sid"));
207
+ const r2 = await deleteSecureKeyAsync(credentialKey("twilio", "auth_token"));
207
208
 
208
209
  if (r1 === "error" || r2 === "error") {
209
210
  return Response.json(
@@ -14,7 +14,11 @@ import { desc } from "drizzle-orm";
14
14
  import { getDb } from "../../memory/db.js";
15
15
  import { toolInvocations } from "../../memory/schema.js";
16
16
  import { getLogger } from "../../util/logger.js";
17
- import { getDataDir, getRootDir } from "../../util/platform.js";
17
+ import {
18
+ getDataDir,
19
+ getRootDir,
20
+ getWorkspaceConfigPath,
21
+ } from "../../util/platform.js";
18
22
  import { httpError } from "../http-errors.js";
19
23
  import type { RouteDefinition } from "../http-router.js";
20
24
 
@@ -31,6 +35,7 @@ interface ExportResponse {
31
35
  success: true;
32
36
  auditRows: Array<Record<string, unknown>>;
33
37
  logFiles: Record<string, string>;
38
+ configSnapshot?: Record<string, unknown>;
34
39
  }
35
40
 
36
41
  /**
@@ -83,21 +88,134 @@ async function handleExport(body: ExportRequestBody): Promise<Response> {
83
88
  }
84
89
  }
85
90
 
91
+ // --- Sanitized config snapshot ---
92
+ const configSnapshot = readSanitizedConfig();
93
+
86
94
  log.info(
87
- { auditCount: auditRows.length, logFileCount: Object.keys(logFiles).length, totalBytes },
95
+ {
96
+ auditCount: auditRows.length,
97
+ logFileCount: Object.keys(logFiles).length,
98
+ totalBytes,
99
+ hasConfig: configSnapshot !== undefined,
100
+ },
88
101
  "Export completed",
89
102
  );
90
103
 
91
- const payload: ExportResponse = { success: true, auditRows, logFiles };
104
+ const payload: ExportResponse = {
105
+ success: true,
106
+ auditRows,
107
+ logFiles,
108
+ configSnapshot,
109
+ };
92
110
  return Response.json(payload);
93
111
  } catch (err) {
94
112
  const message = err instanceof Error ? err.message : String(err);
95
113
  log.error({ err }, "Failed to export");
96
- return httpError(
97
- "INTERNAL_ERROR",
98
- `Failed to export: ${message}`,
99
- 500,
100
- );
114
+ return httpError("INTERNAL_ERROR", `Failed to export: ${message}`, 500);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Replaces a string value with a presence flag: "(set)" if truthy, "(empty)" otherwise.
120
+ */
121
+ function redactStringValue(val: unknown): string {
122
+ return val ? "(set)" : "(empty)";
123
+ }
124
+
125
+ /**
126
+ * Reads the workspace config.json and strips sensitive fields.
127
+ * Returns undefined if the file is missing or unreadable.
128
+ */
129
+ function readSanitizedConfig(): Record<string, unknown> | undefined {
130
+ const configPath = getWorkspaceConfigPath();
131
+ if (!existsSync(configPath)) return undefined;
132
+
133
+ try {
134
+ const raw = readFileSync(configPath, "utf-8");
135
+ const config = JSON.parse(raw) as Record<string, unknown>;
136
+
137
+ // Strip API key values — preserve which providers have keys configured
138
+ if (config.apiKeys && typeof config.apiKeys === "object") {
139
+ const keys = config.apiKeys as Record<string, unknown>;
140
+ config.apiKeys = Object.fromEntries(
141
+ Object.keys(keys).map((k) => [k, redactStringValue(keys[k])]),
142
+ );
143
+ }
144
+
145
+ // Strip ingress webhook secret
146
+ if (config.ingress && typeof config.ingress === "object") {
147
+ const ingress = config.ingress as Record<string, unknown>;
148
+ if (ingress.webhook && typeof ingress.webhook === "object") {
149
+ const webhook = ingress.webhook as Record<string, unknown>;
150
+ webhook.secret = redactStringValue(webhook.secret);
151
+ ingress.webhook = webhook;
152
+ }
153
+ config.ingress = ingress;
154
+ }
155
+
156
+ // Strip skill-level API keys and env vars
157
+ if (config.skills && typeof config.skills === "object") {
158
+ const skills = config.skills as Record<string, unknown>;
159
+ if (skills.entries && typeof skills.entries === "object") {
160
+ const entries = skills.entries as Record<string, unknown>;
161
+ for (const name of Object.keys(entries)) {
162
+ const entry = entries[name];
163
+ if (entry && typeof entry === "object") {
164
+ const e = entry as Record<string, unknown>;
165
+ if ("apiKey" in e) e.apiKey = redactStringValue(e.apiKey);
166
+ if (e.env && typeof e.env === "object") {
167
+ const env = e.env as Record<string, unknown>;
168
+ e.env = Object.fromEntries(
169
+ Object.keys(env).map((k) => [k, redactStringValue(env[k])]),
170
+ );
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ // Strip Twilio accountSid
178
+ if (config.twilio && typeof config.twilio === "object") {
179
+ const twilio = config.twilio as Record<string, unknown>;
180
+ twilio.accountSid = redactStringValue(twilio.accountSid);
181
+ config.twilio = twilio;
182
+ }
183
+
184
+ // Strip MCP transport headers (SSE/streamable-http) and env vars (stdio)
185
+ if (config.mcp && typeof config.mcp === "object") {
186
+ const mcp = config.mcp as Record<string, unknown>;
187
+ if (mcp.servers && typeof mcp.servers === "object") {
188
+ const servers = mcp.servers as Record<string, unknown>;
189
+ for (const name of Object.keys(servers)) {
190
+ const server = servers[name];
191
+ if (server && typeof server === "object") {
192
+ const s = server as Record<string, unknown>;
193
+ if (s.transport && typeof s.transport === "object") {
194
+ const transport = s.transport as Record<string, unknown>;
195
+ if (transport.headers && typeof transport.headers === "object") {
196
+ const headers = transport.headers as Record<string, unknown>;
197
+ transport.headers = Object.fromEntries(
198
+ Object.keys(headers).map((k) => [
199
+ k,
200
+ redactStringValue(headers[k]),
201
+ ]),
202
+ );
203
+ }
204
+ if (transport.env && typeof transport.env === "object") {
205
+ const env = transport.env as Record<string, unknown>;
206
+ transport.env = Object.fromEntries(
207
+ Object.keys(env).map((k) => [k, redactStringValue(env[k])]),
208
+ );
209
+ }
210
+ }
211
+ }
212
+ }
213
+ }
214
+ }
215
+
216
+ return config;
217
+ } catch {
218
+ return undefined;
101
219
  }
102
220
  }
103
221
 
@@ -4,6 +4,7 @@ import {
4
4
  invalidateConfigCache,
5
5
  } from "../../config/loader.js";
6
6
  import { initializeProviders } from "../../providers/registry.js";
7
+ import { credentialKey } from "../../security/credential-key.js";
7
8
  import {
8
9
  deleteSecureKeyAsync,
9
10
  getSecureKeyAsync,
@@ -78,7 +79,7 @@ export async function handleAddSecret(req: Request): Promise<Response> {
78
79
  assertMetadataWritable();
79
80
  const service = name.slice(0, colonIdx);
80
81
  const field = name.slice(colonIdx + 1);
81
- const key = `credential:${service}:${field}`;
82
+ const key = credentialKey(service, field);
82
83
  const stored = await setSecureKeyAsync(key, value);
83
84
  if (!stored) {
84
85
  return httpError(
@@ -164,7 +165,7 @@ export async function handleDeleteSecret(req: Request): Promise<Response> {
164
165
  const service = name.slice(0, colonIdx);
165
166
  const field = name.slice(colonIdx + 1);
166
167
  assertMetadataWritable();
167
- const key = `credential:${service}:${field}`;
168
+ const key = credentialKey(service, field);
168
169
  // Check existence first — the broker always returns "deleted" even
169
170
  // for keys that don't exist, so we need a pre-check for 404 semantics.
170
171
  const existing = await getSecureKeyAsync(key);