@vellumai/assistant 0.4.48 → 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 (252) hide show
  1. package/ARCHITECTURE.md +2 -2
  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/runbook-trusted-contacts.md +3 -8
  6. package/hook-templates/debug-prompt-logger/hook.json +1 -1
  7. package/hook-templates/debug-prompt-logger/run.sh +1 -3
  8. package/package.json +1 -1
  9. package/src/__tests__/actor-token-service.test.ts +0 -1
  10. package/src/__tests__/anthropic-provider.test.ts +156 -0
  11. package/src/__tests__/approval-cascade.test.ts +810 -0
  12. package/src/__tests__/approval-primitive.test.ts +0 -1
  13. package/src/__tests__/approval-routes-http.test.ts +2 -0
  14. package/src/__tests__/assistant-attachments.test.ts +12 -34
  15. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
  16. package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
  17. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  18. package/src/__tests__/channel-guardian.test.ts +0 -2
  19. package/src/__tests__/channel-readiness-routes.test.ts +15 -6
  20. package/src/__tests__/channel-readiness-service.test.ts +10 -9
  21. package/src/__tests__/checker.test.ts +9 -29
  22. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  23. package/src/__tests__/computer-use-tools.test.ts +2 -19
  24. package/src/__tests__/config-watcher.test.ts +0 -1
  25. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  26. package/src/__tests__/context-image-dimensions.test.ts +332 -0
  27. package/src/__tests__/context-token-estimator.test.ts +196 -13
  28. package/src/__tests__/conversation-attention-store.test.ts +0 -1
  29. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  30. package/src/__tests__/conversation-routes-guardian-reply.test.ts +144 -0
  31. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  32. package/src/__tests__/credential-metadata-store.test.ts +64 -73
  33. package/src/__tests__/credential-security-invariants.test.ts +13 -7
  34. package/src/__tests__/credential-vault-unit.test.ts +280 -49
  35. package/src/__tests__/credential-vault.test.ts +138 -16
  36. package/src/__tests__/credentials-cli.test.ts +71 -0
  37. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  38. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  39. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  40. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
  41. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
  42. package/src/__tests__/guardian-routing-invariants.test.ts +0 -1
  43. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
  44. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
  45. package/src/__tests__/heartbeat-service.test.ts +0 -1
  46. package/src/__tests__/host-cu-proxy.test.ts +629 -0
  47. package/src/__tests__/host-shell-tool.test.ts +27 -15
  48. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  49. package/src/__tests__/ingress-url-consistency.test.ts +14 -21
  50. package/src/__tests__/integration-status.test.ts +32 -51
  51. package/src/__tests__/intent-routing.test.ts +0 -1
  52. package/src/__tests__/invite-routes-http.test.ts +10 -9
  53. package/src/__tests__/keychain-broker-client.test.ts +11 -43
  54. package/src/__tests__/notification-routing-intent.test.ts +0 -1
  55. package/src/__tests__/oauth-cli.test.ts +373 -14
  56. package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
  57. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  58. package/src/__tests__/oauth-store.test.ts +756 -0
  59. package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
  60. package/src/__tests__/provider-error-scenarios.test.ts +0 -1
  61. package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
  62. package/src/__tests__/public-ingress-urls.test.ts +15 -21
  63. package/src/__tests__/recording-handler.test.ts +3 -4
  64. package/src/__tests__/registry.test.ts +2 -2
  65. package/src/__tests__/runtime-events-sse.test.ts +55 -7
  66. package/src/__tests__/schedule-store.test.ts +0 -1
  67. package/src/__tests__/scheduler-recurrence.test.ts +0 -1
  68. package/src/__tests__/scoped-approval-grants.test.ts +0 -1
  69. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
  70. package/src/__tests__/secret-ingress-handler.test.ts +0 -1
  71. package/src/__tests__/send-endpoint-busy.test.ts +21 -6
  72. package/src/__tests__/sequence-store.test.ts +0 -1
  73. package/src/__tests__/session-init.benchmark.test.ts +4 -5
  74. package/src/__tests__/skill-include-graph.test.ts +66 -0
  75. package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
  76. package/src/__tests__/skill-load-tool.test.ts +149 -1
  77. package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
  78. package/src/__tests__/skills-uninstall.test.ts +1 -1
  79. package/src/__tests__/skills.test.ts +3 -3
  80. package/src/__tests__/slack-channel-config.test.ts +67 -3
  81. package/src/__tests__/slack-share-routes.test.ts +17 -19
  82. package/src/__tests__/system-prompt.test.ts +0 -1
  83. package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
  84. package/src/__tests__/terminal-tools.test.ts +4 -3
  85. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  86. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  87. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
  88. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  89. package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
  90. package/src/__tests__/tool-executor.test.ts +0 -1
  91. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
  92. package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
  93. package/src/__tests__/trust-store.test.ts +1 -22
  94. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  95. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  96. package/src/__tests__/twilio-routes.test.ts +0 -16
  97. package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
  98. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  99. package/src/agent/ax-tree-compaction.test.ts +235 -0
  100. package/src/agent/loop.ts +76 -130
  101. package/src/calls/call-domain.ts +1 -6
  102. package/src/calls/relay-server.ts +9 -13
  103. package/src/calls/twilio-config.ts +2 -7
  104. package/src/calls/twilio-routes.ts +1 -2
  105. package/src/calls/voice-ingress-preflight.ts +1 -1
  106. package/src/cli/commands/browser-relay.ts +18 -12
  107. package/src/cli/commands/completions.ts +0 -3
  108. package/src/cli/commands/credentials.ts +101 -15
  109. package/src/cli/commands/oauth/apps.ts +255 -0
  110. package/src/cli/commands/oauth/connections.ts +299 -0
  111. package/src/cli/commands/oauth/index.ts +52 -0
  112. package/src/cli/commands/oauth/providers.ts +242 -0
  113. package/src/cli/commands/skills.ts +4 -338
  114. package/src/cli/program.ts +1 -5
  115. package/src/cli/reference.ts +1 -3
  116. package/src/config/assistant-feature-flags.ts +0 -3
  117. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  118. package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
  119. package/src/config/bundled-skills/computer-use/TOOLS.json +22 -4
  120. package/src/config/bundled-skills/google-calendar/calendar-client.ts +21 -16
  121. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -4
  122. package/src/config/bundled-skills/settings/SKILL.md +1 -1
  123. package/src/config/bundled-skills/settings/TOOLS.json +2 -8
  124. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
  125. package/src/config/env-registry.ts +14 -83
  126. package/src/config/env.ts +11 -50
  127. package/src/config/feature-flag-registry.json +16 -16
  128. package/src/config/loader.ts +0 -6
  129. package/src/config/schema.ts +3 -1
  130. package/src/config/skills.ts +21 -2
  131. package/src/context/image-dimensions.ts +229 -0
  132. package/src/context/token-estimator.ts +75 -12
  133. package/src/context/window-manager.ts +49 -10
  134. package/src/daemon/assistant-attachments.ts +1 -13
  135. package/src/daemon/handlers/config-ingress.ts +8 -33
  136. package/src/daemon/handlers/config-slack-channel.ts +49 -46
  137. package/src/daemon/handlers/config-telegram.ts +32 -16
  138. package/src/daemon/handlers/sessions.ts +10 -24
  139. package/src/daemon/handlers/shared.ts +0 -130
  140. package/src/daemon/host-cu-proxy.ts +401 -0
  141. package/src/daemon/lifecycle.ts +36 -68
  142. package/src/daemon/message-protocol.ts +3 -0
  143. package/src/daemon/message-types/computer-use.ts +2 -119
  144. package/src/daemon/message-types/host-cu.ts +19 -0
  145. package/src/daemon/message-types/messages.ts +3 -0
  146. package/src/daemon/server.ts +14 -21
  147. package/src/daemon/session-agent-loop-handlers.ts +2 -0
  148. package/src/daemon/session-attachments.ts +1 -2
  149. package/src/daemon/session-slash.ts +1 -1
  150. package/src/daemon/session-surfaces.ts +40 -28
  151. package/src/daemon/session-tool-setup.ts +2 -9
  152. package/src/daemon/session.ts +138 -15
  153. package/src/daemon/tool-side-effects.ts +2 -8
  154. package/src/daemon/watch-handler.ts +2 -2
  155. package/src/events/tool-metrics-listener.ts +2 -2
  156. package/src/hooks/manager.ts +1 -4
  157. package/src/inbound/public-ingress-urls.ts +7 -7
  158. package/src/logfire.ts +16 -5
  159. package/src/memory/conversation-key-store.ts +21 -0
  160. package/src/memory/db-init.ts +4 -0
  161. package/src/memory/migrations/149-oauth-tables.ts +60 -0
  162. package/src/memory/migrations/index.ts +1 -0
  163. package/src/memory/schema/index.ts +1 -0
  164. package/src/memory/schema/oauth.ts +65 -0
  165. package/src/messaging/provider.ts +4 -4
  166. package/src/messaging/providers/gmail/client.ts +82 -2
  167. package/src/messaging/providers/gmail/people-client.ts +10 -10
  168. package/src/messaging/providers/telegram-bot/adapter.ts +17 -17
  169. package/src/messaging/providers/whatsapp/adapter.ts +11 -8
  170. package/src/messaging/registry.ts +2 -32
  171. package/src/notifications/copy-composer.ts +0 -5
  172. package/src/notifications/signal.ts +4 -5
  173. package/src/oauth/byo-connection.test.ts +126 -25
  174. package/src/oauth/byo-connection.ts +22 -6
  175. package/src/oauth/connect-orchestrator.ts +113 -57
  176. package/src/oauth/connect-types.ts +17 -23
  177. package/src/oauth/connection-resolver.ts +35 -11
  178. package/src/oauth/connection.ts +1 -1
  179. package/src/oauth/manual-token-connection.ts +104 -0
  180. package/src/oauth/oauth-store.ts +496 -0
  181. package/src/oauth/platform-connection.test.ts +29 -0
  182. package/src/oauth/platform-connection.ts +6 -5
  183. package/src/oauth/provider-behaviors.ts +124 -0
  184. package/src/oauth/scope-policy.ts +9 -2
  185. package/src/oauth/seed-providers.ts +161 -0
  186. package/src/oauth/token-persistence.ts +74 -78
  187. package/src/permissions/checker.ts +3 -3
  188. package/src/permissions/defaults.ts +0 -1
  189. package/src/permissions/prompter.ts +10 -1
  190. package/src/permissions/trust-store.ts +13 -0
  191. package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
  192. package/src/prompts/system-prompt.ts +28 -40
  193. package/src/providers/anthropic/client.ts +133 -24
  194. package/src/providers/retry.ts +1 -27
  195. package/src/runtime/auth/route-policy.ts +0 -3
  196. package/src/runtime/channel-reply-delivery.ts +0 -40
  197. package/src/runtime/gateway-client.ts +0 -7
  198. package/src/runtime/http-server.ts +8 -6
  199. package/src/runtime/http-types.ts +2 -2
  200. package/src/runtime/middleware/twilio-validation.ts +1 -11
  201. package/src/runtime/pending-interactions.ts +14 -12
  202. package/src/runtime/routes/channel-delivery-routes.ts +0 -1
  203. package/src/runtime/routes/conversation-routes.ts +73 -19
  204. package/src/runtime/routes/events-routes.ts +21 -11
  205. package/src/runtime/routes/host-cu-routes.ts +97 -0
  206. package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
  207. package/src/runtime/routes/integrations/slack/share.ts +6 -7
  208. package/src/runtime/routes/log-export-routes.ts +126 -8
  209. package/src/runtime/routes/settings-routes.ts +55 -48
  210. package/src/runtime/routes/surface-action-routes.ts +1 -1
  211. package/src/runtime/routes/watch-routes.ts +128 -0
  212. package/src/schedule/integration-status.ts +10 -9
  213. package/src/security/credential-key.ts +0 -156
  214. package/src/security/keychain-broker-client.ts +5 -6
  215. package/src/security/oauth2.ts +1 -1
  216. package/src/security/token-manager.ts +119 -46
  217. package/src/skills/catalog-install.ts +358 -0
  218. package/src/skills/include-graph.ts +32 -0
  219. package/src/telegram/bot-username.ts +2 -3
  220. package/src/tools/browser/network-recorder.ts +1 -1
  221. package/src/tools/browser/network-recording-types.ts +1 -1
  222. package/src/tools/computer-use/definitions.ts +46 -11
  223. package/src/tools/computer-use/registry.ts +4 -5
  224. package/src/tools/credentials/broker.ts +1 -2
  225. package/src/tools/credentials/metadata-store.ts +17 -121
  226. package/src/tools/credentials/vault.ts +94 -167
  227. package/src/tools/registry.ts +2 -7
  228. package/src/tools/skills/load.ts +62 -3
  229. package/src/tools/watch/watch-state.ts +0 -12
  230. package/src/util/logger.ts +7 -41
  231. package/src/util/platform.ts +9 -28
  232. package/src/watcher/providers/google-calendar.ts +2 -1
  233. package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
  234. package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
  235. package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
  236. package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
  237. package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
  238. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
  239. package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
  240. package/src/cli/commands/dev.ts +0 -129
  241. package/src/cli/commands/map.ts +0 -391
  242. package/src/cli/commands/oauth.ts +0 -77
  243. package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +0 -16
  244. package/src/daemon/computer-use-session.ts +0 -1026
  245. package/src/daemon/ride-shotgun-handler.ts +0 -569
  246. package/src/oauth/provider-base-urls.ts +0 -21
  247. package/src/oauth/provider-profiles.ts +0 -192
  248. package/src/prompts/computer-use-prompt.ts +0 -98
  249. package/src/runtime/routes/computer-use-routes.ts +0 -641
  250. package/src/runtime/telegram-streaming-delivery.test.ts +0 -729
  251. package/src/runtime/telegram-streaming-delivery.ts +0 -393
  252. package/src/tools/computer-use/request-computer-control.ts +0 -56
@@ -17,6 +17,7 @@ import {
17
17
  import { getConfig } from "../../config/loader.js";
18
18
  import { renderHistoryContent } from "../../daemon/handlers/shared.js";
19
19
  import { HostBashProxy } from "../../daemon/host-bash-proxy.js";
20
+ import { HostCuProxy } from "../../daemon/host-cu-proxy.js";
20
21
  import { HostFileProxy } from "../../daemon/host-file-proxy.js";
21
22
  import type { ServerMessage } from "../../daemon/message-protocol.js";
22
23
  import {
@@ -449,6 +450,12 @@ function makeHubPublisher(
449
450
  conversationId,
450
451
  kind: "host_file",
451
452
  });
453
+ } else if (msg.type === "host_cu_request") {
454
+ pendingInteractions.register(msg.requestId, {
455
+ session,
456
+ conversationId,
457
+ kind: "host_cu",
458
+ });
452
459
  }
453
460
 
454
461
  // ServerMessage is a large union; sessionId exists on most but not all variants.
@@ -640,9 +647,14 @@ export async function handleSendMessage(
640
647
  });
641
648
  session.setHostFileProxy(fileProxy);
642
649
  }
650
+ if (!session.isProcessing() || !session.hostCuProxy) {
651
+ const cuProxy = new HostCuProxy(onEvent);
652
+ session.setHostCuProxy(cuProxy);
653
+ }
643
654
  } else if (!session.isProcessing()) {
644
655
  session.setHostBashProxy(undefined);
645
656
  session.setHostFileProxy(undefined);
657
+ session.setHostCuProxy(undefined);
646
658
  }
647
659
  // Wire sendToClient to the SSE hub so all subsystems can reach the HTTP client.
648
660
  // Called after setHostBashProxy so updateSender targets the current proxy.
@@ -679,7 +691,13 @@ export async function handleSendMessage(
679
691
  attachments,
680
692
  session,
681
693
  onEvent,
682
- approvalConversationGenerator: deps.approvalConversationGenerator,
694
+ // Desktop path: disable NL classification to avoid consuming non-decision
695
+ // messages while a tool confirmation is pending. Deterministic code-prefix
696
+ // and callback parsing remain active. Mirrors session-process.ts behavior.
697
+ approvalConversationGenerator:
698
+ sourceChannel === "vellum"
699
+ ? undefined
700
+ : deps.approvalConversationGenerator,
683
701
  verifiedActorExternalUserId,
684
702
  verifiedActorPrincipalId,
685
703
  });
@@ -687,6 +705,7 @@ export async function handleSendMessage(
687
705
  return Response.json(
688
706
  {
689
707
  accepted: true,
708
+ conversationId: mapping.conversationId,
690
709
  ...(inlineReplyResult.messageId
691
710
  ? { messageId: inlineReplyResult.messageId }
692
711
  : {}),
@@ -751,7 +770,10 @@ export async function handleSendMessage(
751
770
  pendingInteractions.removeBySession(session);
752
771
  }
753
772
 
754
- return Response.json({ accepted: true, queued: true }, { status: 202 });
773
+ return Response.json(
774
+ { accepted: true, queued: true, conversationId: mapping.conversationId },
775
+ { status: 202 },
776
+ );
755
777
  }
756
778
 
757
779
  // Session is idle — persist and fire agent loop immediately
@@ -782,6 +804,7 @@ export async function handleSendMessage(
782
804
 
783
805
  if (slashResult.kind === "unknown") {
784
806
  session.processing = true;
807
+ let cleanupDeferred = false;
785
808
  try {
786
809
  const provenance = provenanceFromTrustContext(session.trustContext);
787
810
  const channelMeta = {
@@ -818,26 +841,54 @@ export async function handleSendMessage(
818
841
  sourceInterface,
819
842
  );
820
843
 
821
- // Emit fresh model info before the text delta so the client has
822
- // up-to-date configuredProviders when rendering /model, /models,
823
- // and provider shortcut commands (/gpt4, /opus, etc.).
824
- if (isModelSlashCommand(rawContent) || isProviderShortcut(rawContent)) {
825
- onEvent(buildModelInfoEvent());
826
- }
827
-
828
- onEvent({ type: "assistant_text_delta", text: slashResult.message });
829
- onEvent({
830
- type: "message_complete",
831
- sessionId: mapping.conversationId,
832
- });
844
+ // Snapshot model info now so the deferred callback cannot observe
845
+ // a config change from a concurrent request.
846
+ const modelInfoEvent =
847
+ isModelSlashCommand(rawContent) || isProviderShortcut(rawContent)
848
+ ? buildModelInfoEvent()
849
+ : null;
833
850
 
834
- return Response.json(
835
- { accepted: true, messageId: persisted.id },
851
+ const response = Response.json(
852
+ {
853
+ accepted: true,
854
+ messageId: persisted.id,
855
+ conversationId: mapping.conversationId,
856
+ },
836
857
  { status: 202 },
837
858
  );
859
+
860
+ // Defer event publishing to next tick so the HTTP response reaches the
861
+ // client first. This ensures the client's serverToLocalSessionMap is
862
+ // populated before SSE events arrive, preventing dropped events in new
863
+ // desktop threads.
864
+ //
865
+ // session.processing and drainQueue are also deferred so the current
866
+ // slash command's events are emitted before the next queued message
867
+ // starts processing.
868
+ const conversationId = mapping.conversationId;
869
+ const message = slashResult.message;
870
+ setTimeout(() => {
871
+ if (modelInfoEvent) {
872
+ onEvent(modelInfoEvent);
873
+ }
874
+ onEvent({ type: "assistant_text_delta", text: message });
875
+ onEvent({
876
+ type: "message_complete",
877
+ sessionId: conversationId,
878
+ });
879
+ session.processing = false;
880
+ session.drainQueue().catch(() => {});
881
+ }, 0);
882
+
883
+ cleanupDeferred = true;
884
+ return response;
838
885
  } finally {
839
- session.processing = false;
840
- session.drainQueue().catch(() => {});
886
+ // No-op for the slash-command early-return path (handled inside
887
+ // setTimeout above), but still needed for error paths.
888
+ if (!cleanupDeferred && session.processing) {
889
+ session.processing = false;
890
+ session.drainQueue().catch(() => {});
891
+ }
841
892
  }
842
893
  }
843
894
 
@@ -874,7 +925,10 @@ export async function handleSendMessage(
874
925
  );
875
926
  });
876
927
 
877
- return Response.json({ accepted: true, messageId }, { status: 202 });
928
+ return Response.json(
929
+ { accepted: true, messageId, conversationId: mapping.conversationId },
930
+ { status: 202 },
931
+ );
878
932
  }
879
933
 
880
934
  async function generateLlmSuggestion(
@@ -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;
@@ -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,7 +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 { credentialKey } from "../../../../security/credential-key.js";
15
+ import { getConnectionByProvider } from "../../../../oauth/oauth-store.js";
16
16
  import { getSecureKey } from "../../../../security/secure-keys.js";
17
17
  import { getLogger } from "../../../../util/logger.js";
18
18
  import { httpError } from "../../../http-errors.js";
@@ -25,14 +25,13 @@ const log = getLogger("slack-share");
25
25
  // ---------------------------------------------------------------------------
26
26
 
27
27
  /**
28
- * Resolve the Slack bot token from secure storage.
29
- * Prefers the OAuth integration token, falls back to the legacy channel token.
28
+ * Resolve the Slack bot token from the OAuth connection store.
30
29
  */
31
30
  function resolveSlackToken(): string | undefined {
32
- return (
33
- getSecureKey(credentialKey("integration:slack", "access_token")) ??
34
- getSecureKey(credentialKey("slack_channel", "bot_token"))
35
- );
31
+ const conn = getConnectionByProvider("integration:slack");
32
+ return conn
33
+ ? getSecureKey(`oauth_connection/${conn.id}/access_token`)
34
+ : undefined;
36
35
  }
37
36
 
38
37
  // ---------------------------------------------------------------------------
@@ -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