mixdog 1.3.5 → 1.4.0

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 (341) hide show
  1. package/NOTICE.md +2 -2
  2. package/README.md +2 -2
  3. package/package.json +2 -11
  4. package/scripts/daemon-stub.mjs +1 -1
  5. package/scripts/gate-local.mjs +4 -2
  6. package/scripts/native-release-status.mjs +2 -2
  7. package/scripts/recall-history-cases.json +1 -1
  8. package/scripts/suite-health.mjs +10 -1
  9. package/scripts/test.mjs +7 -11
  10. package/src/agents/reviewer/AGENT.md +7 -11
  11. package/src/agents/reviewer/agent.json +1 -1
  12. package/src/defaults/shipped-history.json +17 -1
  13. package/src/defaults/skills/browser-use/SKILL.md +59 -11
  14. package/src/defaults/skills/code-tidy/SKILL.md +142 -270
  15. package/src/defaults/skills/code-tidy/references/agent-cleanup.md +4 -4
  16. package/src/defaults/skills/code-tidy/references/cleanup-report.md +31 -76
  17. package/src/defaults/skills/code-tidy/references/dead-code.md +6 -12
  18. package/src/defaults/skills/computer-use/SKILL.md +33 -9
  19. package/src/defaults/skills/setup/SKILL.md +22 -0
  20. package/src/defaults/skills/setup/references/actions.md +39 -5
  21. package/src/defaults/skills/setup/references/surfaces.md +11 -2
  22. package/src/defaults/skills/visualize/SKILL.md +2 -1
  23. package/src/lib/rules-builder.cjs +10 -8
  24. package/src/rules/agent/AGENT.md +6 -6
  25. package/src/rules/lead/LEAD.md +18 -20
  26. package/src/rules/shared/05-tool-calls.md +5 -6
  27. package/src/rules/shared/10-tool-workflow.md +2 -0
  28. package/src/rules/shared/60-verification.md +7 -5
  29. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -3
  30. package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +3 -1
  31. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +3 -1
  32. package/src/runtime/agent/orchestrator/config.mjs +17 -1
  33. package/src/runtime/agent/orchestrator/context/collect.mjs +3 -0
  34. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +72 -67
  35. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +54 -1
  36. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +3 -3
  37. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +176 -72
  38. package/src/runtime/agent/orchestrator/mcp/client.mjs +19 -0
  39. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +181 -133
  40. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +42 -6
  41. package/src/runtime/agent/orchestrator/providers/anthropic-cache-warm.mjs +165 -0
  42. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +4 -1
  44. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/gzip-post.mjs +14 -14
  45. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +145 -66
  46. package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +25 -3
  47. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +64 -33
  48. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +135 -87
  49. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +123 -102
  50. package/src/runtime/agent/orchestrator/providers/cache-warm-admission.mjs +90 -0
  51. package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +25 -9
  52. package/src/runtime/agent/orchestrator/providers/cursor.mjs +22 -13
  53. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +17 -3
  54. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +52 -10
  55. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +8 -1
  56. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +1 -0
  57. package/src/runtime/agent/orchestrator/providers/npm-cli-version.mjs +11 -3
  58. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +2 -1
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -9
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-send/send-context.mjs +22 -3
  61. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +43 -5
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/handshake-failure.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/stream-failure.mjs +6 -3
  64. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +65 -1
  65. package/src/runtime/agent/orchestrator/providers/quota-reading.mjs +38 -12
  66. package/src/runtime/agent/orchestrator/providers/registry.mjs +26 -0
  67. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +39 -7
  68. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +6 -0
  69. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +12 -7
  70. package/src/runtime/agent/orchestrator/runtime-core/compaction-config-keys.mjs +62 -0
  71. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +37 -16
  72. package/src/runtime/agent/orchestrator/runtime-core/deferred-tool-delta.mjs +100 -23
  73. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +19 -4
  75. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +125 -30
  76. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +792 -0
  77. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +133 -22
  78. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +475 -58
  79. package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +42 -17
  80. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -1
  81. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +11 -5
  82. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +0 -2
  83. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -4
  84. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +1 -1
  85. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +18 -22
  86. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +32 -3
  87. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +3 -1
  88. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +1 -1
  89. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +17 -3
  90. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -2
  91. package/src/runtime/agent/orchestrator/session/loop/tool-exec/after-hook.mjs +2 -1
  92. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +3 -2
  93. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +16 -2
  94. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +16 -1
  95. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +10 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -10
  97. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +12 -3
  98. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -2
  99. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +19 -1
  100. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +133 -90
  101. package/src/runtime/agent/orchestrator/session/manager/ask-turn-state.mjs +31 -4
  102. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +30 -18
  103. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +23 -13
  104. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +11 -2
  105. package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +13 -0
  106. package/src/runtime/agent/orchestrator/session/manager/route-fallback-chain.mjs +129 -0
  107. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +20 -2
  108. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
  109. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -0
  110. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +1 -1
  111. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -2
  112. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +102 -15
  113. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +18 -5
  114. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +19 -8
  115. package/src/runtime/agent/orchestrator/session/prompt-cache-idle-gaps.mjs +113 -0
  116. package/src/runtime/agent/orchestrator/session/prompt-cache-touch-store.mjs +127 -0
  117. package/src/runtime/agent/orchestrator/session/routine-result.mjs +36 -9
  118. package/src/runtime/agent/orchestrator/session/sensitive-projection.mjs +100 -0
  119. package/src/runtime/agent/orchestrator/session/sensitive-values.mjs +257 -0
  120. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +20 -7
  121. package/src/runtime/agent/orchestrator/session/side-question.mjs +18 -0
  122. package/src/runtime/agent/orchestrator/session/skill-update-notice.mjs +60 -0
  123. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +6 -3
  124. package/src/runtime/agent/orchestrator/session/store/save-delta.mjs +9 -5
  125. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +9 -8
  126. package/src/runtime/agent/orchestrator/session/store/session-persistence-events.mjs +29 -0
  127. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +8 -6
  128. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +12 -2
  129. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -14
  130. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +84 -3
  131. package/src/runtime/agent/orchestrator/session/stored-attachment-history.mjs +32 -0
  132. package/src/runtime/agent/orchestrator/session/thinking-binding.mjs +108 -0
  133. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/session/tool-batch/record-outcome.mjs +2 -1
  135. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +18 -1
  136. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +9 -7
  137. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-text.mjs +2 -2
  138. package/src/runtime/agent/orchestrator/session/turn-failure.mjs +5 -1
  139. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +5 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +12 -1
  141. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/misuse-guard.mjs +240 -0
  142. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +1 -1
  143. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/shell-cwd.mjs +38 -0
  144. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +25 -5
  145. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +65 -7
  146. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +7 -1
  147. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +36 -13
  148. package/src/runtime/browser-bridge/action-schema.mjs +13 -2
  149. package/src/runtime/browser-bridge/client.mjs +73 -8
  150. package/src/runtime/browser-bridge/input-fields.mjs +8 -9
  151. package/src/runtime/browser-bridge/mail-links.mjs +107 -0
  152. package/src/runtime/browser-bridge/tool-defs.mjs +1 -1
  153. package/src/runtime/channels/lib/config.mjs +6 -61
  154. package/src/runtime/channels/lib/crash-log.mjs +1 -1
  155. package/src/runtime/channels/lib/event-pipeline.mjs +4 -19
  156. package/src/runtime/channels/lib/event-queue.mjs +3 -18
  157. package/src/runtime/channels/lib/owned-runtime/automation-services.mjs +2 -2
  158. package/src/runtime/channels/lib/owned-runtime/automation-start.mjs +3 -4
  159. package/src/runtime/channels/lib/owned-runtime/config-reload.mjs +4 -59
  160. package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +33 -115
  161. package/src/runtime/channels/lib/owned-runtime/owned-stop.mjs +5 -22
  162. package/src/runtime/channels/lib/owned-runtime/state.mjs +2 -8
  163. package/src/runtime/channels/lib/owned-runtime.mjs +6 -23
  164. package/src/runtime/channels/lib/parent-bridge.mjs +2 -2
  165. package/src/runtime/channels/lib/runtime-paths.mjs +4 -43
  166. package/src/runtime/channels/lib/scheduler.mjs +33 -147
  167. package/src/runtime/channels/lib/schedules-db.mjs +34 -11
  168. package/src/runtime/channels/lib/status-snapshot.mjs +1 -1
  169. package/src/runtime/channels/lib/tool-dispatch.mjs +1 -2
  170. package/src/runtime/channels/lib/webhooks-db.mjs +5 -9
  171. package/src/runtime/channels/lib/worker-bootstrap.mjs +0 -1
  172. package/src/runtime/channels/lib/worker-ipc.mjs +0 -1
  173. package/src/runtime/channels/lib/worker-main.mjs +10 -34
  174. package/src/runtime/computer-bridge/action-schema.mjs +42 -1
  175. package/src/runtime/computer-bridge/actions.mjs +9 -1
  176. package/src/runtime/computer-bridge/client.mjs +180 -8
  177. package/src/runtime/computer-bridge/core-actions.mjs +2 -2
  178. package/src/runtime/computer-bridge/error-recovery.mjs +27 -2
  179. package/src/runtime/computer-bridge/linux-readiness.mjs +65 -23
  180. package/src/runtime/computer-bridge/pending-continuation.mjs +1 -1
  181. package/src/runtime/connectors/connector-accounts-mcp.mjs +230 -0
  182. package/src/runtime/connectors/gmail-app-password.mjs +172 -39
  183. package/src/runtime/connectors/google-mcp.mjs +10 -12
  184. package/src/runtime/memory/lib/pg/adapter.mjs +77 -70
  185. package/src/runtime/memory/lib/pg/supervisor.mjs +24 -12
  186. package/src/runtime/memory/lib/runtime-fetcher.mjs +23 -5
  187. package/src/runtime/session-search/session-search-db.mjs +73 -9
  188. package/src/runtime/session-search/session-search-ingest.mjs +110 -20
  189. package/src/runtime/shared/abort-race.mjs +89 -0
  190. package/src/runtime/shared/agent-avatar.d.mts +0 -1
  191. package/src/runtime/shared/agent-kind.mjs +4 -3
  192. package/src/runtime/shared/bounded-download.mjs +8 -3
  193. package/src/runtime/shared/config.mjs +2 -13
  194. package/src/runtime/shared/connector-accounts.mjs +20 -10
  195. package/src/runtime/shared/connector-catalog.d.mts +29 -0
  196. package/src/runtime/shared/connector-catalog.mjs +150 -0
  197. package/src/runtime/shared/connector-triggers.mjs +22 -19
  198. package/src/runtime/shared/defaults-separation.mjs +2 -14
  199. package/src/runtime/shared/delegated-approval-host.mjs +103 -11
  200. package/src/runtime/shared/err-text.mjs +24 -15
  201. package/src/runtime/shared/file-permissions.d.mts +0 -8
  202. package/src/runtime/shared/file-permissions.mjs +0 -11
  203. package/src/runtime/shared/git-direct.test-support.mjs +19 -0
  204. package/src/runtime/shared/heartbeat-interval.d.mts +4 -0
  205. package/src/runtime/shared/heartbeat-interval.mjs +19 -0
  206. package/src/runtime/shared/idle-auto-clear-gate.mjs +104 -0
  207. package/src/runtime/shared/llm/usage-rollup.mjs +1 -1
  208. package/src/runtime/shared/mixy-presence.mjs +22 -0
  209. package/src/runtime/shared/orchestration.d.mts +2 -0
  210. package/src/runtime/shared/pg-schema-db.mjs +27 -16
  211. package/src/runtime/shared/profile-config.cjs +1 -1
  212. package/src/runtime/shared/prompt-origin.d.mts +4 -0
  213. package/src/runtime/shared/prompt-origin.mjs +21 -0
  214. package/src/runtime/shared/provider-accounts.mjs +7 -1
  215. package/src/runtime/shared/provider-display-name.mjs +29 -0
  216. package/src/runtime/shared/provider-timeout-phase.mjs +2 -0
  217. package/src/runtime/shared/routine-trigger.d.mts +2 -0
  218. package/src/runtime/shared/routine-trigger.mjs +10 -0
  219. package/src/runtime/shared/sensitive-path-rules.d.mts +2 -10
  220. package/src/runtime/shared/sensitive-path-rules.mjs +19 -41
  221. package/src/runtime/shared/session-activity-signal.mjs +34 -1
  222. package/src/runtime/shared/session-sidebar-host.d.mts +1 -1
  223. package/src/runtime/shared/session-sidebar-host.mjs +7 -3
  224. package/src/runtime/shared/session-turn-prelude.mjs +18 -0
  225. package/src/runtime/shared/tool-result-summary.mjs +1 -14
  226. package/src/runtime/shared/worktrees/test-support.mjs +9 -4
  227. package/src/runtime/terminal-bridge/client.mjs +10 -1
  228. package/src/runtime/visualize-bridge/client.mjs +27 -8
  229. package/src/runtime/visualize-bridge/tool-defs.mjs +22 -8
  230. package/src/runtime/visualize-bridge/tool-meta.d.mts +5 -0
  231. package/src/runtime/visualize-bridge/tool-meta.mjs +22 -0
  232. package/src/runtime/web-search/lib/fetch-pipeline.mjs +2 -2
  233. package/src/session-runtime/agent-home-delivery.mjs +448 -185
  234. package/src/session-runtime/agent-task-sessions.mjs +329 -180
  235. package/src/session-runtime/boot/apis.mjs +9 -0
  236. package/src/session-runtime/boot/infrastructure.mjs +4 -0
  237. package/src/session-runtime/boot/providers.mjs +3 -0
  238. package/src/session-runtime/boot/tools.mjs +6 -1
  239. package/src/session-runtime/config-lifecycle.mjs +6 -0
  240. package/src/session-runtime/connector-merge-migration.mjs +305 -0
  241. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +132 -28
  242. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +2 -0
  243. package/src/session-runtime/lifecycle/inheritance.mjs +16 -3
  244. package/src/session-runtime/lifecycle/session-catalog.mjs +3 -1
  245. package/src/session-runtime/lifecycle/teardown/runtime-stops.mjs +50 -4
  246. package/src/session-runtime/lifecycle/teardown.mjs +23 -5
  247. package/src/session-runtime/mcp-glue/server-catalog.mjs +55 -3
  248. package/src/session-runtime/mcp-oauth-login.mjs +5 -3
  249. package/src/session-runtime/mixy-heartbeat.mjs +153 -33
  250. package/src/session-runtime/notification-bus.mjs +31 -117
  251. package/src/session-runtime/notification-completion-queue.mjs +54 -0
  252. package/src/session-runtime/notification-listeners.mjs +76 -0
  253. package/src/session-runtime/owned-automation-run.mjs +2 -1
  254. package/src/session-runtime/prewarm/channel-start.mjs +3 -7
  255. package/src/session-runtime/resource-mcp-api.mjs +143 -224
  256. package/src/session-runtime/resource-mcp-connector-accounts.mjs +163 -0
  257. package/src/session-runtime/resource-mcp-oauth-api.mjs +217 -0
  258. package/src/session-runtime/resource-mcp-secrets.mjs +82 -0
  259. package/src/session-runtime/resource-mcp-servers.mjs +78 -0
  260. package/src/session-runtime/resource-mcp-store.mjs +49 -0
  261. package/src/session-runtime/routine-owner.mjs +40 -9
  262. package/src/session-runtime/services/agent-tool/close-flow.mjs +3 -0
  263. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +7 -2
  264. package/src/session-runtime/services/agent-tool/tag-registry.mjs +1 -0
  265. package/src/session-runtime/services/agent-tool/worker-index/awaiting-user-sync.mjs +51 -0
  266. package/src/session-runtime/services/agent-tool/worker-index/disposal.mjs +55 -0
  267. package/src/session-runtime/services/agent-tool/worker-index/released-store.mjs +123 -0
  268. package/src/session-runtime/services/agent-tool/worker-index/restart-settlement.mjs +112 -0
  269. package/src/session-runtime/services/agent-tool/worker-index/row-reads.mjs +36 -0
  270. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -0
  271. package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +17 -4
  272. package/src/session-runtime/services/agent-tool/worker-index/row-writes.mjs +58 -0
  273. package/src/session-runtime/services/agent-tool/worker-index.mjs +41 -241
  274. package/src/session-runtime/services/agent-tool/worker-rows.mjs +3 -0
  275. package/src/session-runtime/services/channel-admin.mjs +7 -13
  276. package/src/session-runtime/services/channel-session-router.mjs +4 -2
  277. package/src/session-runtime/services/seeds.mjs +0 -28
  278. package/src/session-runtime/session-compaction-sync.mjs +27 -0
  279. package/src/session-runtime/session-create/session-options.mjs +1 -0
  280. package/src/session-runtime/settings-builtin-tools-api.mjs +11 -0
  281. package/src/session-runtime/settings-compaction-api.mjs +2 -40
  282. package/src/session-runtime/settings-system-api.mjs +13 -1
  283. package/src/session-runtime/setup-tool/agent-settings-actions.mjs +65 -0
  284. package/src/session-runtime/setup-tool/executor.mjs +145 -22
  285. package/src/session-runtime/setup-tool/extended-actions.mjs +8 -3
  286. package/src/session-runtime/setup-tool/session-route-actions.mjs +152 -0
  287. package/src/session-runtime/setup-tool/settings-contract.mjs +4 -3
  288. package/src/session-runtime/setup-tool/tool-defs.mjs +64 -5
  289. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +17 -13
  290. package/src/session-runtime/tool-policy/policy-stage.mjs +16 -2
  291. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  292. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +4 -0
  293. package/src/standalone/channel-transport/client-registry/client-records.mjs +65 -0
  294. package/src/standalone/channel-transport/client-registry/client-registration.mjs +118 -0
  295. package/src/standalone/channel-transport/client-registry.mjs +24 -163
  296. package/src/standalone/channel-transport.mjs +1 -1
  297. package/src/standalone/daemon-channels-loader.mjs +10 -8
  298. package/src/standalone/daemon-session-control/list-page.mjs +72 -0
  299. package/src/standalone/daemon-session-control/request.mjs +51 -0
  300. package/src/standalone/daemon-session-control/room-host.mjs +57 -0
  301. package/src/standalone/daemon-session-control/route-control.mjs +48 -0
  302. package/src/standalone/daemon-session-control/send-turn.mjs +60 -0
  303. package/src/standalone/daemon-session-control/worktree-control.mjs +40 -0
  304. package/src/standalone/daemon-session-control.mjs +124 -188
  305. package/src/standalone/daemon.mjs +26 -3
  306. package/src/standalone/session-client.mjs +7 -3
  307. package/src/standalone/session-protocol.mjs +5 -0
  308. package/src/standalone/session-service/agent-tree/agent-child.mjs +1 -0
  309. package/src/standalone/session-service/quota-resume.mjs +3 -0
  310. package/src/standalone/session-service/retention/busy.mjs +65 -1
  311. package/src/standalone/session-service/session-calls/session-action.mjs +23 -2
  312. package/src/standalone/session-service/session-calls/session-views.mjs +8 -0
  313. package/src/standalone/session-tool-output.mjs +15 -7
  314. package/src/tui/app/model-options.mjs +3 -28
  315. package/src/tui/dist/index.mjs +58 -64
  316. package/src/tui/session/agent-job-feed/model-notification.mjs +3 -0
  317. package/src/tui/session/initial-state.mjs +3 -0
  318. package/src/tui/session/notice-surface.mjs +7 -1
  319. package/src/tui/session/queue-helpers.mjs +3 -42
  320. package/src/tui/session/session-api/routes-agent-definitions.mjs +14 -2
  321. package/src/tui/session/session-api/routes-write.mjs +5 -1
  322. package/src/tui/session/session-api/routes.mjs +1 -0
  323. package/src/tui/session/session-api/settings.mjs +17 -1
  324. package/src/tui/session/session-flow/auto-clear.mjs +4 -33
  325. package/src/tui/session/session-flow/drain-closed-queue.mjs +38 -0
  326. package/src/tui/session/session-flow/drain-enqueue.mjs +86 -0
  327. package/src/tui/session/session-flow/drain-retry.mjs +35 -0
  328. package/src/tui/session/session-flow/drain.mjs +118 -149
  329. package/src/tui/session/session-flow/queue/entry-shape.mjs +5 -0
  330. package/src/tui/session/session-flow/queue/take-entries.mjs +5 -2
  331. package/src/tui/session/tool-approval-delegation.mjs +64 -0
  332. package/src/tui/session/tool-approval-request.mjs +39 -0
  333. package/src/tui/session/tool-approval-settle.mjs +62 -0
  334. package/src/tui/session/tool-approval.mjs +69 -115
  335. package/src/tui/session/tui-steering-persist.mjs +8 -1
  336. package/src/tui/session/turn-stream.mjs +19 -2
  337. package/src/tui/session/turn.mjs +1 -0
  338. package/src/vendor/statusline/bin/statusline-route.mjs +1 -219
  339. package/src/vendor/statusline/src/gateway/route-meta.mjs +35 -13
  340. package/scripts/code-graph-description-contract.mjs +0 -188
  341. package/src/runtime/channels/lib/owned-runtime/ownership-refresh.mjs +0 -36
@@ -2,69 +2,31 @@
2
2
  // the per-server intent (enabled flag) of servers registered plugins declare.
3
3
  // Plugin servers are derived live (mcp-glue/plugin-servers.mjs) and are never
4
4
  // written into `mcpServers`; ordinary edits refuse them.
5
+ // The units it is assembled from: resource-mcp-store.mjs (the `mcpServers`
6
+ // registry), resource-mcp-secrets.mjs (keychain side), resource-mcp-connector-
7
+ // accounts.mjs (multi-account connector accounts), resource-mcp-oauth-api.mjs
8
+ // (sign-in / sign-out) and resource-mcp-servers.mjs (pure map reads).
5
9
  import { clean } from '../runtime/agent/orchestrator/runtime-core/session-text.mjs';
6
- import {
7
- mergeMcpServerConfig,
8
- MCP_TIMEOUT_FIELD_GROUPS,
9
- } from '../runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs';
10
- import { resolveMcpOAuthUrl } from '../runtime/agent/orchestrator/mcp/client-config.mjs';
11
- import { withAdoptedCopies, withPinnedCollisionNames, withPluginServerIntent } from './mcp-glue/plugin-servers.mjs';
10
+ import { mergeMcpServerConfig } from '../runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs';
11
+ import { withAdoptedCopies, withPluginServerIntent } from './mcp-glue/plugin-servers.mjs';
12
12
  import { findRegisteredPlugin } from './resource-plugins-api.mjs';
13
- import { createMcpOAuthLogins } from './mcp-oauth-login.mjs';
14
- import { connectorById, gmailAppPasswordAccount, gmailAppPasswordValue } from '../runtime/shared/connector-catalog.mjs';
13
+ import { migrateMergedConnectorServers } from './connector-merge-migration.mjs';
14
+ import {
15
+ connectorAccountServers,
16
+ connectorAccountsOf,
17
+ connectorById,
18
+ connectorMultiAccount,
19
+ gmailAppPasswordAccount,
20
+ mergeConnectorAccountServers,
21
+ } from '../runtime/shared/connector-catalog.mjs';
15
22
  import { saveMcpOAuthClientSecret } from '../runtime/agent/orchestrator/mcp/oauth-client.mjs';
16
23
  import { forgetGmailAppPassword, saveGmailAppPassword } from '../runtime/connectors/gmail-app-password.mjs';
17
-
18
- /** The URL a server's OAuth credentials are stored under, or null. */
19
- function oauthUrlOf(cfg) {
20
- try {
21
- return resolveMcpOAuthUrl(cfg);
22
- } catch {
23
- // No valid remote URL: such a server never logged in.
24
- return null;
25
- }
26
- }
27
-
28
- /** The `mcpServers` map of a config as a fresh object (never the live one). */
29
- function mcpServersOf(config) {
30
- return config.mcpServers && typeof config.mcpServers === 'object' ? { ...config.mcpServers } : {};
31
- }
32
-
33
- function pluginOwnedError(name, owner) {
34
- return new Error(
35
- `MCP server "${name}" is provided by plugin "${owner.id}" (server "${owner.key}"); ` +
36
- 'enable or disable it with set_mcp_enabled, or manage the plugin instead'
37
- );
38
- }
39
-
40
- const transportType = (type) => (type === 'streamable-http' ? 'http' : type);
41
-
42
- /** UI editors send complete transports; setup may send a partial patch. Merge
43
- * the patch over the existing entry, keeping omitted fields (including
44
- * credentials) unless the transport itself changed. */
45
- function mergedServerInput(input, existing, originalName) {
46
- const transportChanged =
47
- (input.type && transportType(input.type) !== transportType(existing.type || (existing.url ? 'http' : 'stdio'))) ||
48
- (input.command && existing.url) ||
49
- (input.url && !existing.url);
50
- const maps = {};
51
- if (!transportChanged) {
52
- for (const key of ['env', 'headers', 'env_http_headers']) {
53
- if (Object.hasOwn(input, key)) maps[key] = { ...(existing[key] || {}), ...input[key] };
54
- }
55
- }
56
- // Existing timeouts are kept by mergeMcpServerConfig; the normalizer must only
57
- // see (and validate) newly supplied ones, so a stored 0/'off' never blocks
58
- // an unrelated save.
59
- const { ...base } = transportChanged ? {} : existing;
60
- for (const field of MCP_TIMEOUT_FIELD_GROUPS.flat()) delete base[field];
61
- return {
62
- ...base,
63
- ...input,
64
- ...maps,
65
- name: clean(input.name) || originalName,
66
- };
67
- }
24
+ import { extensionScopesFromConfig, withExtensionScope } from '../runtime/shared/extension-scopes.mjs';
25
+ import { accountTarget, mcpServersOf, mergedServerInput, nameTakenError } from './resource-mcp-servers.mjs';
26
+ import { createMcpServerStore } from './resource-mcp-store.mjs';
27
+ import { createMcpServerSecrets } from './resource-mcp-secrets.mjs';
28
+ import { createConnectorAccountEdits, editsServerItself } from './resource-mcp-connector-accounts.mjs';
29
+ import { createMcpOAuthApi } from './resource-mcp-oauth-api.mjs';
68
30
 
69
31
  export function createMcpResourceApi({ deps, sync, decorate }) {
70
32
  const {
@@ -92,121 +54,82 @@ export function createMcpResourceApi({ deps, sync, decorate }) {
92
54
  saveOAuthClientSecret = saveMcpOAuthClientSecret,
93
55
  } = deps;
94
56
 
95
- const reconnectOne = async (name) => {
96
- const status = await connectConfiguredMcp({ only: name, enabled: true });
97
- const row = status?.servers?.find((server) => server.name === name);
98
- return { connected: row?.connected === true, error: row?.error || null };
99
- };
100
- // Explicit OAuth sign-in: the one interactive MCP connect. On success the
101
- // other runtimes reconnect the server non-interactively with the new tokens.
102
- const oauthLogins = createMcpOAuthLogins({
103
- async connect(name, openBrowser) {
104
- const status = await connectConfiguredMcp({ only: name, enabled: true, login: { openBrowser } });
105
- const row = status?.servers?.find((server) => server.name === name);
106
- return { connected: row?.connected === true, error: row?.error || null };
107
- },
108
- cancel: (name) => cancelMcpLogin(name),
109
- completeCallback: completeMcpLogin,
110
- // 계정 바꾸기, transactionally: the working connection stays until the new
111
- // sign-in succeeded and connects; a cancel or failure changes nothing, and
112
- // a new sign-in that cannot connect is rolled back.
113
- async switchAccount(name, launch, attempt) {
114
- const cfg = resolveMcpCatalog().servers[name];
115
- const switching = startMcpServerOAuthSwitch(cfg, launch, name);
116
- attempt.abort = () => switching.cancel();
117
- attempt.answer = (callbackUrl) => switching.answer(callbackUrl);
118
- const signIn = await switching.done;
119
- const rollback = await commitMcpServerOAuthSwitch(cfg, signIn);
120
- const outcome = await reconnectOne(name);
121
- if (outcome.connected) return outcome;
122
- await rollback();
123
- await reconnectOne(name);
124
- return { connected: false, error: outcome.error || 'The new sign-in could not connect' };
125
- },
126
- onComplete: (name) => sync.announce('mcp', { detail: { mcpChanges: [{ name, enabled: true }] } }),
57
+ const oauthApi = createMcpOAuthApi({
58
+ sync,
59
+ mcpStatus,
60
+ connectConfiguredMcp,
61
+ clearMcpServerOAuth,
62
+ reloadFullConfig,
63
+ awaitKeychainPrewarm,
64
+ resolveMcpCatalog,
65
+ cancelMcpLogin,
66
+ completeMcpLogin,
67
+ getMcpServerOAuthState,
68
+ probeMcpServerAuth,
69
+ startMcpServerOAuthSwitch,
70
+ commitMcpServerOAuthSwitch,
71
+ });
72
+ const { adoptedConfig, saveServers, userServers, freeName, refusePluginServer } = createMcpServerStore({
73
+ getConfig,
74
+ saveConfigAndAdopt,
75
+ resolveMcpCatalog,
76
+ pluginMcpServer,
77
+ });
78
+ const { storeOAuthClientSecret, storeGmailAppPassword, releaseOAuth, forgetUnusedGmailAppPassword, releaseAccount } =
79
+ createMcpServerSecrets({
80
+ gmailAppPassword,
81
+ saveOAuthClientSecret,
82
+ clearMcpServerOAuth,
83
+ resolveConfiguredMcpCatalog,
84
+ });
85
+ const { addConnectorAccount, saveConnectorAccount, setConnectorAccountEnabled } = createConnectorAccountEdits({
86
+ normalizeMcpServerInput,
87
+ pluginMcpServer,
88
+ sync,
89
+ saveServers,
90
+ freeName,
91
+ storeOAuthClientSecret,
92
+ storeGmailAppPassword,
93
+ forgetUnusedGmailAppPassword,
94
+ releaseOAuth,
127
95
  });
128
- /** The effective config of a server that signs in with MCP OAuth (HTTP/SSE
129
- * without configured authentication). */
130
- const oauthServerConfig = async (name) => {
131
- const serverName = clean(name);
132
- if (!serverName) throw new Error('MCP server name is required');
133
- await awaitKeychainPrewarm();
134
- reloadFullConfig();
135
- const cfg = resolveMcpCatalog().servers[serverName];
136
- if (!cfg) throw new Error(`MCP server not configured: ${serverName}`);
137
- if (!getMcpServerOAuthState(cfg)) throw new Error(`MCP server "${serverName}" does not use OAuth sign-in`);
138
- return { serverName, cfg };
139
- };
140
-
141
- /** The config with any adoptable plugin server copies already adopted, so
142
- * every write also persists the adoption and never re-adds a copy. */
143
- const adoptedConfig = () => withAdoptedCopies(getConfig(), resolveMcpCatalog().adoption);
144
-
145
- /** Persist a new `mcpServers` map; project overrides never survive a global
146
- * edit. A plugin server pushed off its name by the edit keeps the collision
147
- * name it gets, so it does not flip again later. */
148
- const saveServers = (servers) => {
149
- const nextConfig = { ...adoptedConfig() };
150
- delete nextConfig.mcpProjectOverrides;
151
- saveConfigAndAdopt({ ...nextConfig, mcpServers: servers });
152
- const saved = getConfig();
153
- const pinned = withPinnedCollisionNames(saved, resolveMcpCatalog());
154
- if (pinned !== saved) saveConfigAndAdopt(pinned);
155
- };
156
- const userServers = () => mcpServersOf(adoptedConfig());
157
- const refusePluginServer = (name) => {
158
- const owned = name ? pluginMcpServer(name) : null;
159
- if (owned) throw pluginOwnedError(name, owned.owner);
160
- };
161
- // The user's OAuth client secret (custom connector → 고급 설정 → 비밀값)
162
- // goes to the keychain only; clearing the client id forgets it.
163
- const storeOAuthClientSecret = async (input, cfg, previous = {}) => {
164
- if (!cfg?.url) return;
165
- const secret = typeof input.oauthClientSecret === 'string' ? input.oauthClientSecret.trim() : '';
166
- if (cfg.oauthClientId && secret) await saveOAuthClientSecret(cfg.url, secret);
167
- else if (!cfg.oauthClientId && previous.oauthClientId) await saveOAuthClientSecret(previous.url || cfg.url, '');
168
- };
169
- // Gmail with an app password (connector-catalog.mjs GMAIL_APP_PASSWORD): the
170
- // password is checked with Gmail before anything is saved and goes to the
171
- // keychain only. A new connection needs one; an edit without one keeps it.
172
- const storeGmailAppPassword = async (input, cfg, required) => {
173
- const account = gmailAppPasswordAccount(cfg);
174
- if (!account) return;
175
- const password = gmailAppPasswordValue(input.appPassword);
176
- if (password) await gmailAppPassword.save(account, password);
177
- else if (required || input.appPassword !== undefined) {
178
- throw new Error('GMAIL_APP_PASSWORD_FORMAT: An app password has 16 characters.');
179
- }
180
- };
181
- // A server's OAuth tokens and client registration go with it, unless another
182
- // configured server (plugin or disabled ones included, even with MCP
183
- // disabled for this runtime) still uses them through the same exact URL.
184
- // `signIn` tells the caller what happened to the sign-in: `removed` (local
185
- // secrets deleted, `revoked` when the provider confirmed the revocation
186
- // too), `shared` (another server still uses it), or none (null).
187
- const releaseOAuth = async (cfg) => {
188
- const url = oauthUrlOf(cfg);
189
- if (!url) return null;
190
- if (Object.values(resolveConfiguredMcpCatalog().servers).some((other) => oauthUrlOf(other) === url)) {
191
- return { signIn: 'shared', revoked: false };
192
- }
193
- const { revoked = false } = (await clearMcpServerOAuth(cfg, { revoke: true })) || {};
194
- return { signIn: 'removed', revoked };
195
- };
196
- // An app password is deleted once no configured server uses its account.
197
- const forgetUnusedGmailAppPassword = async (account) => {
198
- if (!account) return false;
199
- const servers = Object.values(resolveConfiguredMcpCatalog().servers);
200
- if (servers.some((cfg) => gmailAppPasswordAccount(cfg) === account)) return false;
201
- await gmailAppPassword.forget(account);
202
- return true;
203
- };
204
96
  // Peers reload config from disk on receipt: land the write before publishing.
205
97
  const persisted = async () => {
206
98
  await flushSkillsSave?.();
207
99
  };
208
100
 
209
101
  return {
102
+ /** One-time, idempotent: merge the one-account servers of a multi-account
103
+ * connector (`gmail`, `gmail-2`…) into one server holding the accounts
104
+ * (connector-catalog.mjs mergeConnectorAccountServers). Every account
105
+ * keeps its address, so its stored sign-in and app password stay as
106
+ * they are. Settings keyed by a merged-away server name go with it:
107
+ * scopes (`extensionScopes.mcp`) are dropped, and the accounts of a
108
+ * server that was off stay off (`enabled: false` on the account);
109
+ * per-agent / per-project connector settings and data-dir hooks move to
110
+ * the surviving server first (connector-merge-migration.mjs); a store
111
+ * that fails stays in a pending record that every start retries.
112
+ * Returns what was merged. */
113
+ migrateConnectorAccounts() {
114
+ const config = getConfig();
115
+ const { servers, merged } = mergeConnectorAccountServers(mcpServersOf(config));
116
+ // Also retries, without a merge, what an earlier start could not migrate.
117
+ const settings = migrateMergedConnectorServers(
118
+ deps.cfgMod?.getPluginData?.() || deps.STANDALONE_DATA_DIR,
119
+ merged
120
+ );
121
+ if (!merged.length) return merged;
122
+ // Neither migrated nor recorded for a retry: merging again next start is the retry.
123
+ if (settings.unrecorded && settings.failed.length) return [];
124
+ let next = { ...config, mcpServers: servers };
125
+ const scoped = extensionScopesFromConfig(config).mcp;
126
+ for (const { name, from } of merged) {
127
+ for (const gone of from)
128
+ if (gone !== name && Object.hasOwn(scoped, gone)) next = withExtensionScope(next, 'mcp', gone, null);
129
+ }
130
+ saveConfigAndAdopt(next);
131
+ return merged;
132
+ },
210
133
  /** One-time, idempotent: move server copies an earlier plugin enable wrote
211
134
  * into `mcpServers` to per-server plugin intent. Returns adopted names. */
212
135
  migratePluginMcpCopies() {
@@ -224,62 +147,14 @@ export function createMcpResourceApi({ deps, sync, decorate }) {
224
147
  async reconnectMcp(name) {
225
148
  await awaitKeychainPrewarm();
226
149
  reloadFullConfig();
227
- const serverName = clean(name);
150
+ // An account reconnects with its server.
151
+ const serverName = accountTarget(resolveMcpCatalog().servers, clean(name))?.serverName || clean(name);
228
152
  if (!serverName) return sync.reconnectMcpAndAnnounce();
229
153
  const { enabled } = getMcpServerConfig(serverName);
230
154
  return sync.reconnectMcpAndAnnounce({ only: serverName, enabled });
231
155
  },
232
- /** `{ flowId, state: 'pending', url }` once the browser login starts
233
- * (`options.openBrowser === false` leaves opening `url` to the caller), or
234
- * `{ flowId, state: 'complete' }` when stored credentials sufficed. */
235
- async beginMcpOAuthLogin(name, options = {}) {
236
- const { serverName, cfg } = await oauthServerConfig(name);
237
- if (cfg.enabled === false) throw new Error(`MCP server is disabled: ${serverName}`);
238
- // `mode`: `connect` (a normal authorization), `reauth` (repair the same
239
- // connection; a refresh is tried first) or `switch` (계정 바꾸기).
240
- if (options?.mode === 'switch' && !getMcpServerOAuthState(cfg)?.signedIn) {
241
- throw new Error(`MCP server "${serverName}" is not signed in`);
242
- }
243
- return oauthLogins.begin(serverName, options);
244
- },
245
- /** `{ flowId, state: 'pending' | 'complete' | 'failed' | 'cancelled',
246
- * step?, error?, reason? }`; with `{ since, waitMs }` it resolves on the
247
- * flow's next change (mcp-oauth-login.mjs). */
248
- getMcpOAuthLoginStatus(flowId, options) {
249
- return oauthLogins.status(flowId, options);
250
- },
251
- cancelMcpOAuthLogin(flowId) {
252
- return oauthLogins.cancel(flowId);
253
- },
254
- /** Remote / headless sign-in: complete the pending flow with the redirect
255
- * URL pasted from the browser. `{ flowId, state: 'complete' | 'failed',
256
- * error? }`; an invalid paste throws a fixed message and stays pending. */
257
- completeMcpOAuthLogin(flowId, callbackUrl) {
258
- return oauthLogins.complete(flowId, callbackUrl);
259
- },
260
- /** Forget the server's stored OAuth credentials (shared by every server
261
- * with the same exact URL) and reconnect without a login, here and on
262
- * peers, so it reports sign-in required. */
263
- async signOutMcpServer(name) {
264
- const { serverName, cfg } = await oauthServerConfig(name);
265
- // A begin issued meanwhile waits until the credentials are gone.
266
- return oauthLogins.cancelServer(serverName, async () => {
267
- const { revoked = false } = (await clearMcpServerOAuth(cfg, { revoke: true })) || {};
268
- const status = await sync.reconnectMcpAndAnnounce({ only: serverName, enabled: cfg.enabled !== false });
269
- return { ...status, revoked };
270
- });
271
- },
272
- /** `{ auth: 'oauth' | 'none' | 'configured' | 'unreachable', error? }` for an
273
- * HTTP/SSE config, without credentials or a browser. */
274
- probeMcpServerAuth(cfg) {
275
- if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) throw new TypeError('MCP server config is required');
276
- const probe = {};
277
- for (const key of ['type', 'url', 'headers', 'bearer_token_env_var', 'env_http_headers']) {
278
- if (cfg[key] !== undefined) probe[key] = cfg[key];
279
- }
280
- if (probe.type !== 'http' && probe.type !== 'sse') throw new TypeError("type must be 'http' or 'sse'");
281
- return probeMcpServerAuth(probe);
282
- },
156
+ // Sign-in / sign-out (resource-mcp-oauth-api.mjs), in this key position.
157
+ ...oauthApi,
283
158
  async addMcpServer(input = {}) {
284
159
  const { name, config: serverConfig } = normalizeMcpServerInput(input);
285
160
  // A catalog connector's server keeps its catalog id (connector-catalog.mjs).
@@ -289,7 +164,14 @@ export function createMcpResourceApi({ deps, sync, decorate }) {
289
164
  serverConfig.connector = connector;
290
165
  }
291
166
  const servers = userServers();
292
- if (Object.hasOwn(servers, name)) throw new Error(`MCP server already exists: ${name}`);
167
+ // A multi-account connector's account (as its own server config) joins
168
+ // that connector's one server.
169
+ if (connector && connectorMultiAccount(connectorById(connector)) && !Array.isArray(input.accounts)) {
170
+ return addConnectorAccount(servers, connector, name, serverConfig, input);
171
+ }
172
+ if (Object.hasOwn(servers, name) || accountTarget(servers, name)) {
173
+ throw nameTakenError(servers, name);
174
+ }
293
175
  const owned = pluginMcpServer(name);
294
176
  if (owned) throw new Error(`MCP server already exists: ${name} (provided by plugin "${owned.owner.id}")`);
295
177
  await storeOAuthClientSecret(input, serverConfig);
@@ -300,6 +182,9 @@ export function createMcpResourceApi({ deps, sync, decorate }) {
300
182
  },
301
183
  async saveMcpServer(input = {}) {
302
184
  const servers = userServers();
185
+ // A name of a multi-account connector's account edits that account.
186
+ const accountOf = accountTarget(servers, clean(input.originalName) || clean(input.name));
187
+ if (accountOf && !editsServerItself(accountOf, input)) return saveConnectorAccount(servers, accountOf, input);
303
188
  // An explicit originalName must exist; a bare name edits a matching
304
189
  // server and otherwise creates a new one. Plugin servers are never
305
190
  // edited, renamed or shadowed here.
@@ -314,8 +199,11 @@ export function createMcpResourceApi({ deps, sync, decorate }) {
314
199
  const existing = originalName ? servers[originalName] : {};
315
200
  const normalizedInput = normalizeMcpServerInput(mergedServerInput(input, existing, originalName));
316
201
  const name = originalName && clean(input.name) === originalName ? originalName : normalizedInput.name;
317
- if (name !== originalName && (Object.hasOwn(servers, name) || pluginMcpServer(name))) {
318
- throw new Error(`MCP server already exists: ${name}`);
202
+ if (
203
+ name !== originalName &&
204
+ (Object.hasOwn(servers, name) || pluginMcpServer(name) || accountTarget(servers, name))
205
+ ) {
206
+ throw nameTakenError(servers, name);
319
207
  }
320
208
  if (originalName && originalName !== name) delete servers[originalName];
321
209
  servers[name] = mergeMcpServerConfig(existing, normalizedInput.config);
@@ -338,6 +226,20 @@ export function createMcpResourceApi({ deps, sync, decorate }) {
338
226
  const serverName = clean(name);
339
227
  if (!serverName) throw new Error('MCP server name is required');
340
228
  const servers = userServers();
229
+ // A name of a multi-account connector's account removes that account;
230
+ // the server goes with its last one.
231
+ const target = accountTarget(servers, serverName);
232
+ if (target) {
233
+ const rest = connectorAccountsOf(target.cfg).filter((entry) => entry.name !== serverName);
234
+ if (rest.length) servers[target.serverName] = { ...target.cfg, accounts: rest };
235
+ else delete servers[target.serverName];
236
+ saveServers(servers);
237
+ const status = rest.length
238
+ ? await sync.reconnectMcpAndAnnounce({ only: target.serverName, enabled: target.cfg.enabled !== false })
239
+ : await sync.reconnectMcpAndAnnounce({ drop: [target.serverName] });
240
+ const released = await releaseAccount(target.accountCfg);
241
+ return released ? { ...status, ...released } : status;
242
+ }
341
243
  if (!Object.hasOwn(servers, serverName)) {
342
244
  refusePluginServer(serverName);
343
245
  throw new Error(`MCP server not configured: ${serverName}`);
@@ -346,6 +248,17 @@ export function createMcpResourceApi({ deps, sync, decorate }) {
346
248
  delete servers[serverName];
347
249
  saveServers(servers);
348
250
  const status = await sync.reconnectMcpAndAnnounce({ drop: [serverName] });
251
+ // The server of a multi-account connector takes every account's sign-in with it.
252
+ const accountCfgs = connectorAccountServers(removed);
253
+ if (accountCfgs.length) {
254
+ const released = (await Promise.all(accountCfgs.map(releaseAccount))).filter(Boolean);
255
+ if (!released.length) return status;
256
+ return {
257
+ ...status,
258
+ signIn: released.every((one) => one.signIn === 'removed') ? 'removed' : 'shared',
259
+ revoked: released.every((one) => one.revoked === true),
260
+ };
261
+ }
349
262
  // A removed server keeps no sign-in another server does not share: an
350
263
  // app password leaves the keychain (it is revoked at Google only).
351
264
  const account = gmailAppPasswordAccount(removed);
@@ -357,7 +270,13 @@ export function createMcpResourceApi({ deps, sync, decorate }) {
357
270
  return released ? { ...status, ...released } : status;
358
271
  },
359
272
  async setMcpServerEnabled(name, enabled) {
360
- const serverName = clean(name);
273
+ // A name a multi-account connector holds as an account switches that
274
+ // account (and, on, its server) rather than the whole server; the
275
+ // account sharing the server's name is switched off with the server.
276
+ const requested = clean(name);
277
+ const servers = userServers();
278
+ const held = accountTarget(servers, requested);
279
+ const serverName = held?.serverName || requested;
361
280
  if (!serverName) throw new Error('MCP server name is required');
362
281
  const want = enabled !== false;
363
282
  const owned = pluginMcpServer(serverName);
@@ -379,11 +298,11 @@ export function createMcpResourceApi({ deps, sync, decorate }) {
379
298
  }
380
299
  const shadowRow = mcpStatus().servers.find((s) => s.name === serverName);
381
300
  if (!shadowRow) throw new Error(`MCP server not configured: ${serverName}`);
382
- const servers = userServers();
383
301
  const current = servers[serverName];
384
302
  if (!current || typeof current !== 'object' || Array.isArray(current)) {
385
303
  throw new Error(`MCP server not configured: ${serverName}`);
386
304
  }
305
+ if (held && (want || serverName !== requested)) return setConnectorAccountEnabled(servers, held, want);
387
306
  servers[serverName] = { ...current, enabled: want };
388
307
  saveServers(servers);
389
308
  const status = await sync.scheduleMcpToggle(serverName, want);
@@ -0,0 +1,163 @@
1
+ // The accounts of a multi-account connector server as the MCP server API
2
+ // edits them: adding one more account (into its server, or a new server
3
+ // holding it) and editing one account's transport, app password or client.
4
+ import { clean } from '../runtime/agent/orchestrator/runtime-core/session-text.mjs';
5
+ import {
6
+ mergeMcpServerConfig,
7
+ MCP_TIMEOUT_FIELD_GROUPS,
8
+ } from '../runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs';
9
+ import {
10
+ connectorAccountEnabled,
11
+ connectorAccountEntry,
12
+ connectorAccountServer,
13
+ connectorAccountServers,
14
+ connectorAccountsOf,
15
+ connectorAccountsUrl,
16
+ connectorById,
17
+ connectorOfServer,
18
+ gmailAppPasswordAccount,
19
+ withConnectorAccountEnabled,
20
+ } from '../runtime/shared/connector-catalog.mjs';
21
+ import { forgetConnectorAccountHealth } from '../runtime/connectors/connector-accounts-mcp.mjs';
22
+ import { accountTarget, mergedServerInput, nameTakenError, oauthUrlOf } from './resource-mcp-servers.mjs';
23
+
24
+ /** The server of multi-account connector `connector` in `servers`, or ''. */
25
+ function connectorServerName(servers, connector) {
26
+ return (
27
+ Object.keys(servers).find(
28
+ (name) => connectorOfServer(servers[name]) === connector && connectorAccountsOf(servers[name]).length > 0
29
+ ) || ''
30
+ );
31
+ }
32
+
33
+ /** Whether `input` edits the server `target` belongs to rather than the
34
+ * account: the account carries the server's name and the input the
35
+ * server's own address (its config editor). */
36
+ export const editsServerItself = (target, input) => {
37
+ if (target.serverName !== target.account.name || !clean(input.url)) return false;
38
+ try {
39
+ return oauthUrlOf({ type: input.type || 'http', url: input.url }) === oauthUrlOf(target.cfg);
40
+ } catch {
41
+ return false;
42
+ }
43
+ };
44
+
45
+ export function createConnectorAccountEdits({
46
+ normalizeMcpServerInput,
47
+ pluginMcpServer,
48
+ sync,
49
+ saveServers,
50
+ freeName,
51
+ storeOAuthClientSecret,
52
+ storeGmailAppPassword,
53
+ forgetUnusedGmailAppPassword,
54
+ releaseOAuth,
55
+ }) {
56
+ /** One more account of multi-account connector `connector` (one-account
57
+ * config `accountCfg` as normalized from `input`): into its server, or a
58
+ * new server named `name` holding it. The same Gmail address again takes
59
+ * the new app password. Resolves `{ name, account, status }`. */
60
+ async function addConnectorAccount(servers, connector, name, accountCfg, input) {
61
+ const serverName = connectorServerName(servers, connector);
62
+ const address = gmailAppPasswordAccount(accountCfg);
63
+ const cfg = serverName ? servers[serverName] : null;
64
+ const same =
65
+ cfg && address
66
+ ? connectorAccountsOf(cfg).find(
67
+ (account) => gmailAppPasswordAccount(connectorAccountServer(cfg, account)) === address
68
+ )
69
+ : null;
70
+ if (same) {
71
+ await storeGmailAppPassword(input, accountCfg, true);
72
+ const status = await sync.reconnectMcpAndAnnounce({ only: serverName, enabled: cfg.enabled !== false });
73
+ return { name: serverName, account: same.name, status };
74
+ }
75
+ if (!serverName && (Object.hasOwn(servers, name) || pluginMcpServer(name) || accountTarget(servers, name))) {
76
+ throw nameTakenError(servers, name);
77
+ }
78
+ const accountName = serverName ? freeName(servers, name) : name;
79
+ const account = connectorAccountEntry(accountName, accountCfg);
80
+ // Sign-in tokens are stored per exact URL: another account needs its own.
81
+ const backendUrl = connectorById(connector)?.backend?.url;
82
+ if (
83
+ !address &&
84
+ cfg &&
85
+ connectorAccountServers(cfg).some((other) => oauthUrlOf(other) === oauthUrlOf({ type: 'http', url: account.url }))
86
+ ) {
87
+ account.url = `${backendUrl}?account=${encodeURIComponent(accountName)}`;
88
+ }
89
+ const storedCfg = connectorAccountServer({ connector }, account);
90
+ await storeOAuthClientSecret(input, storedCfg);
91
+ await storeGmailAppPassword(input, storedCfg, true);
92
+ forgetConnectorAccountHealth(accountName);
93
+ let target = serverName;
94
+ if (cfg) {
95
+ servers[serverName] = { ...cfg, accounts: [...connectorAccountsOf(cfg), account] };
96
+ } else {
97
+ // The server's own settings (timeouts, enabled) ride on the input.
98
+ const { type: _type, url: _url, headers: _headers, oauthClientId: _client, ...rest } = accountCfg;
99
+ target = name;
100
+ servers[name] = { ...rest, type: 'http', url: connectorAccountsUrl(connector), connector, accounts: [account] };
101
+ }
102
+ saveServers(servers);
103
+ const status = await sync.reconnectMcpAndAnnounce({ only: target, enabled: servers[target].enabled !== false });
104
+ return { name: target, account: accountName, status };
105
+ }
106
+
107
+ /** An edit of one account (`target` from accountTarget): its transport,
108
+ * app password or own client, as an edit of its one-account server was. */
109
+ async function saveConnectorAccount(servers, target, input) {
110
+ const { serverName, cfg, account, accountCfg } = target;
111
+ const { config } = normalizeMcpServerInput(
112
+ mergedServerInput({ ...input, name: account.name }, accountCfg, account.name)
113
+ );
114
+ const next = mergeMcpServerConfig(accountCfg, config);
115
+ next.connector = cfg.connector;
116
+ await storeOAuthClientSecret(input, next, accountCfg);
117
+ await storeGmailAppPassword(input, next, false);
118
+ // Settings that are not the account's transport (timeouts) are the server's.
119
+ const { type: _type, url: _url, headers: _headers, oauthClientId: _client, ...serverFields } = config;
120
+ const server = { ...cfg };
121
+ for (const group of MCP_TIMEOUT_FIELD_GROUPS) {
122
+ if (group.some((field) => Object.hasOwn(serverFields, field))) for (const field of group) delete server[field];
123
+ }
124
+ servers[serverName] = {
125
+ ...server,
126
+ ...serverFields,
127
+ accounts: connectorAccountsOf(cfg).map((entry) =>
128
+ entry.name === account.name
129
+ ? {
130
+ ...connectorAccountEntry(account.name, next),
131
+ ...(connectorAccountEnabled(entry) ? {} : { enabled: false }),
132
+ }
133
+ : entry
134
+ ),
135
+ };
136
+ saveServers(servers);
137
+ forgetConnectorAccountHealth(account.name);
138
+ const previousAddress = gmailAppPasswordAccount(accountCfg);
139
+ const address = gmailAppPasswordAccount(next);
140
+ if (previousAddress !== address) await forgetUnusedGmailAppPassword(previousAddress);
141
+ // An account switched from the Google sign-in to an app password keeps no sign-in.
142
+ if (address && !previousAddress) await releaseOAuth(accountCfg);
143
+ const status = await sync.reconnectMcpAndAnnounce({ only: serverName, enabled: cfg.enabled !== false });
144
+ return { name: serverName, account: account.name, source: 'config', status };
145
+ }
146
+
147
+ /** Switch one account (`target` from accountTarget) on or off by itself and
148
+ * reconnect its server. Switching an account on switches its server on;
149
+ * switching its last live account off switches the server off. */
150
+ async function setConnectorAccountEnabled(servers, target, enabled) {
151
+ const { serverName, cfg, account } = target;
152
+ const next = withConnectorAccountEnabled(cfg, account.name, enabled);
153
+ servers[serverName] = next;
154
+ saveServers(servers);
155
+ forgetConnectorAccountHealth(account.name);
156
+ const serverOn = next.enabled !== false;
157
+ const status = await sync.scheduleMcpToggle(serverName, serverOn);
158
+ await sync.publish('mcp', { mcpChanges: [{ name: serverName, enabled: serverOn }] });
159
+ return status;
160
+ }
161
+
162
+ return { addConnectorAccount, saveConnectorAccount, setConnectorAccountEnabled };
163
+ }