mixdog 1.2.6 → 1.3.1

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 (294) hide show
  1. package/LICENSES/simple-icons-CC0-1.0.txt +30 -0
  2. package/NOTICE.md +71 -9
  3. package/package.json +4 -1
  4. package/scripts/check-model-recommendations.mjs +89 -0
  5. package/scripts/gate-local.mjs +7 -0
  6. package/scripts/lib/isolated-test-env.mjs +16 -0
  7. package/src/agents/reviewer/AGENT.md +4 -2
  8. package/src/agents/reviewer/agent.json +1 -1
  9. package/src/defaults/shipped-history.json +10 -0
  10. package/src/defaults/skills/browser-use/SKILL.md +5 -0
  11. package/src/defaults/skills/memory-management/SKILL.md +4 -1
  12. package/src/defaults/skills/setup/references/actions.md +14 -4
  13. package/src/defaults/skills/setup-guide/SKILL.md +178 -0
  14. package/src/defaults/skills/setup-guide/model-recommendations.json +333 -0
  15. package/src/lib/rules-builder.cjs +55 -6
  16. package/src/output-styles/common.md +13 -8
  17. package/src/rules/agent/AGENT.md +4 -0
  18. package/src/rules/agent-mode/AGENT_MODE.md +24 -0
  19. package/src/rules/agent-mode/MIXY.md +28 -0
  20. package/src/rules/agent-mode/ROUTINE.md +6 -0
  21. package/src/rules/lead/LEAD.md +7 -3
  22. package/src/rules/shared/70-delivery.md +3 -0
  23. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +86 -0
  24. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +77 -14
  25. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +241 -14
  26. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +4 -2
  27. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +216 -0
  28. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -4
  29. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +10 -1
  30. package/src/runtime/agent/orchestrator/mcp/client.mjs +115 -11
  31. package/src/runtime/agent/orchestrator/mcp/oauth-account.mjs +97 -0
  32. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +49 -0
  33. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +59 -9
  34. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +32 -7
  35. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +45 -6
  36. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +195 -6
  37. package/src/runtime/agent/orchestrator/mcp/server-connectors.mjs +17 -0
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +1 -0
  40. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
  41. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +10 -3
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
  43. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +7 -8
  44. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +3 -1
  46. package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
  47. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
  48. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +9 -2
  49. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +17 -12
  50. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -0
  51. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
  52. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
  53. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
  54. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +96 -1
  55. package/src/runtime/agent/orchestrator/runtime-core/context-status-shape.mjs +4 -0
  56. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +6 -4
  57. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +14 -0
  58. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -0
  59. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +12 -6
  60. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +32 -5
  61. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +21 -5
  62. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +40 -7
  63. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +52 -20
  64. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +2 -0
  65. package/src/runtime/agent/orchestrator/session/agent-home.d.mts +4 -0
  66. package/src/runtime/agent/orchestrator/session/agent-home.mjs +33 -0
  67. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +62 -0
  68. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +31 -0
  69. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +241 -0
  70. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +166 -0
  71. package/src/runtime/agent/orchestrator/session/agent-room.mjs +650 -0
  72. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +7 -1
  73. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +6 -2
  75. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +192 -0
  76. package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +95 -0
  77. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +221 -0
  78. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +195 -0
  79. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +167 -0
  80. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -1
  81. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
  82. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +10 -0
  83. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +108 -6
  84. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +10 -1
  85. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +74 -15
  86. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +9 -1
  87. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +5 -4
  88. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -3
  89. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +49 -0
  90. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
  91. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +1 -0
  92. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -1
  93. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  94. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -0
  95. package/src/runtime/agent/orchestrator/session/manager/pending-lifecycle-epoch.mjs +0 -8
  96. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +44 -16
  97. package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
  98. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +22 -5
  99. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +39 -4
  100. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +33 -24
  101. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +58 -8
  102. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -0
  103. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -2
  104. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
  105. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +6 -0
  106. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +58 -0
  107. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +27 -0
  108. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
  109. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +86 -0
  110. package/src/runtime/agent/orchestrator/session/routine-result.mjs +329 -0
  111. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +8 -0
  112. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +28 -9
  113. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +16 -2
  114. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -12
  115. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +46 -0
  116. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +0 -4
  117. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +58 -10
  118. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +92 -15
  119. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +12 -0
  120. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +113 -5
  121. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +4 -0
  122. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +25 -1
  123. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +8 -0
  124. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +6 -0
  125. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -0
  126. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +75 -57
  127. package/src/runtime/attachments/store.mjs +6 -0
  128. package/src/runtime/browser-bridge/client.mjs +24 -0
  129. package/src/runtime/channels/lib/scheduler.mjs +40 -12
  130. package/src/runtime/channels/lib/schedules-db.mjs +71 -12
  131. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +36 -16
  132. package/src/runtime/channels/lib/webhook.mjs +0 -1
  133. package/src/runtime/channels/lib/webhooks-db.mjs +100 -10
  134. package/src/runtime/channels/lib/worker-main.mjs +17 -20
  135. package/src/runtime/computer-bridge/client.mjs +28 -1
  136. package/src/runtime/connectors/gmail-app-password.mjs +690 -0
  137. package/src/runtime/connectors/google-mcp.mjs +446 -0
  138. package/src/runtime/effort-judge/judge-client.mjs +1 -1
  139. package/src/runtime/effort-judge/model-manifest.json +7 -7
  140. package/src/runtime/memory/lib/core-memory-access.mjs +69 -0
  141. package/src/runtime/memory/lib/core-memory-file.mjs +16 -8
  142. package/src/runtime/memory/lib/core-memory-index.mjs +15 -9
  143. package/src/runtime/memory/lib/core-memory-management.mjs +10 -4
  144. package/src/runtime/memory/lib/core-memory-store.mjs +53 -30
  145. package/src/runtime/memory/lib/core-memory-uniqueness.mjs +10 -6
  146. package/src/runtime/memory/lib/http-router/session-routes.mjs +3 -2
  147. package/src/runtime/memory/lib/memory-action-handlers/core-actions.mjs +44 -14
  148. package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +11 -0
  149. package/src/runtime/memory/lib/memory.mjs +7 -1
  150. package/src/runtime/memory/lib/query-core-recall.mjs +9 -1
  151. package/src/runtime/memory/lib/query-hybrid-search.mjs +2 -0
  152. package/src/runtime/memory/lib/tool-call-handler.mjs +2 -0
  153. package/src/runtime/memory/tool-defs.mjs +5 -0
  154. package/src/runtime/shared/agent-avatar-sprites.mjs +445 -0
  155. package/src/runtime/shared/agent-avatar.d.mts +64 -0
  156. package/src/runtime/shared/agent-avatar.mjs +320 -0
  157. package/src/runtime/shared/agent-kind.mjs +152 -0
  158. package/src/runtime/shared/agent-route-config.d.mts +4 -0
  159. package/src/runtime/shared/agent-route-config.mjs +30 -0
  160. package/src/runtime/shared/agent-store.mjs +132 -0
  161. package/src/runtime/shared/atomic-file.mjs +14 -1
  162. package/src/runtime/shared/bot-split-migration.mjs +457 -0
  163. package/src/runtime/shared/config-patch.mjs +20 -1
  164. package/src/runtime/shared/config.mjs +8 -1
  165. package/src/runtime/shared/connector-accounts.mjs +15 -0
  166. package/src/runtime/shared/connector-catalog.d.mts +51 -0
  167. package/src/runtime/shared/connector-catalog.mjs +228 -0
  168. package/src/runtime/shared/connector-triggers.d.mts +3 -0
  169. package/src/runtime/shared/connector-triggers.mjs +45 -0
  170. package/src/runtime/shared/err-text.mjs +3 -1
  171. package/src/runtime/shared/error-presentation.mjs +91 -6
  172. package/src/runtime/shared/google-oauth-client.mjs +24 -0
  173. package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
  174. package/src/runtime/shared/handoff-artifacts.mjs +61 -0
  175. package/src/runtime/shared/held-request-host.d.mts +12 -0
  176. package/src/runtime/shared/held-request-host.mjs +71 -0
  177. package/src/runtime/shared/llm/provider-model-identities.mjs +6 -1
  178. package/src/runtime/shared/llm/usage-accounting.mjs +7 -1
  179. package/src/runtime/shared/model-route-fallback.mjs +79 -7
  180. package/src/runtime/shared/plain-preview.d.mts +1 -0
  181. package/src/runtime/shared/plain-preview.mjs +33 -0
  182. package/src/runtime/shared/routine-limits.d.mts +11 -0
  183. package/src/runtime/shared/routine-limits.mjs +21 -0
  184. package/src/runtime/shared/routine-trigger.d.mts +10 -0
  185. package/src/runtime/shared/routine-trigger.mjs +44 -0
  186. package/src/runtime/shared/schedule-model-ref.mjs +9 -10
  187. package/src/runtime/shared/sensitive-path-rules.d.mts +15 -0
  188. package/src/runtime/shared/sensitive-path-rules.mjs +256 -0
  189. package/src/runtime/shared/slow-span.mjs +4 -12
  190. package/src/runtime/shared/statusline/statusline-segments.mjs +13 -1
  191. package/src/runtime/shared/transcript-metadata.mjs +10 -0
  192. package/src/runtime/shared/turn-changes.mjs +0 -8
  193. package/src/runtime/shared/usage-readout-totals.mjs +58 -0
  194. package/src/runtime/shared/user-data-guard.mjs +119 -38
  195. package/src/session-runtime/agent-home-delivery.mjs +826 -0
  196. package/src/session-runtime/agent-profile-changes.mjs +21 -0
  197. package/src/session-runtime/agent-room-delivery.mjs +133 -0
  198. package/src/session-runtime/agent-task-sessions.mjs +460 -0
  199. package/src/session-runtime/boot/apis.mjs +63 -18
  200. package/src/session-runtime/boot/facade.mjs +5 -8
  201. package/src/session-runtime/boot/infrastructure.mjs +28 -17
  202. package/src/session-runtime/boot/providers.mjs +14 -4
  203. package/src/session-runtime/boot/tools.mjs +3 -0
  204. package/src/session-runtime/channel-config-api.mjs +53 -5
  205. package/src/session-runtime/config-lifecycle.mjs +43 -2
  206. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +36 -20
  207. package/src/session-runtime/cwd-plugins/plugin-status.mjs +6 -1
  208. package/src/session-runtime/handoff-attachments.mjs +593 -0
  209. package/src/session-runtime/held-requests.mjs +228 -0
  210. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +121 -0
  211. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +5 -1
  212. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +31 -2
  213. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +56 -3
  214. package/src/session-runtime/internal-tool-executor.mjs +2 -0
  215. package/src/session-runtime/lifecycle/session-catalog.mjs +9 -0
  216. package/src/session-runtime/lifecycle/session-switch/resume.mjs +4 -0
  217. package/src/session-runtime/lifecycle/shared.mjs +6 -0
  218. package/src/session-runtime/mcp-glue/server-catalog.mjs +8 -0
  219. package/src/session-runtime/mcp-glue/server-input.mjs +3 -0
  220. package/src/session-runtime/mcp-oauth-login.mjs +112 -15
  221. package/src/session-runtime/model-route/route-persist.mjs +2 -2
  222. package/src/session-runtime/model-route/route-tuning.mjs +7 -1
  223. package/src/session-runtime/model-route/set-route.mjs +35 -4
  224. package/src/session-runtime/native-web-search.mjs +2 -2
  225. package/src/session-runtime/new-session-config.mjs +6 -1
  226. package/src/session-runtime/owned-automation-run.mjs +229 -0
  227. package/src/session-runtime/resource-api.mjs +5 -0
  228. package/src/session-runtime/resource-mcp-api.mjs +107 -13
  229. package/src/session-runtime/resource-skills-api.mjs +12 -0
  230. package/src/session-runtime/room-member-gate.mjs +48 -0
  231. package/src/session-runtime/route-resolution.mjs +5 -11
  232. package/src/session-runtime/routine-home-turn.mjs +156 -0
  233. package/src/session-runtime/routine-owner.mjs +48 -0
  234. package/src/session-runtime/schedule-session-run.mjs +30 -64
  235. package/src/session-runtime/services/agent-tool/helpers.mjs +30 -1
  236. package/src/session-runtime/services/agent-tool/send-flow.mjs +11 -3
  237. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +19 -4
  238. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +2 -0
  239. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
  240. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +5 -8
  241. package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
  242. package/src/session-runtime/services/channel-admin.mjs +93 -24
  243. package/src/session-runtime/services/memory-access.mjs +37 -0
  244. package/src/session-runtime/services/provider-admin.mjs +74 -39
  245. package/src/session-runtime/services/schedule-access.mjs +91 -0
  246. package/src/session-runtime/session-create/session-options.mjs +34 -7
  247. package/src/session-runtime/session-hooks.mjs +16 -8
  248. package/src/session-runtime/setup-tool/executor.mjs +79 -6
  249. package/src/session-runtime/setup-tool/extended-actions.mjs +90 -20
  250. package/src/session-runtime/setup-tool/settings-contract.mjs +14 -6
  251. package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
  252. package/src/session-runtime/skills-api.mjs +16 -1
  253. package/src/session-runtime/tool-policy-refresh.mjs +249 -29
  254. package/src/session-runtime/turn/session-ops.mjs +8 -1
  255. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +10 -2
  256. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +14 -0
  257. package/src/session-runtime/webhook-session-run.mjs +49 -67
  258. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +239 -0
  259. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +650 -25
  260. package/src/session-runtime/workflow-agents-api/agent-route.mjs +67 -7
  261. package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
  262. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +11 -1
  263. package/src/session-runtime/workflow-agents-api.mjs +4 -1
  264. package/src/standalone/daemon-agent-control.mjs +1 -8
  265. package/src/standalone/daemon-session-control.mjs +21 -4
  266. package/src/standalone/daemon.mjs +31 -5
  267. package/src/standalone/session-protocol.mjs +27 -0
  268. package/src/standalone/session-service/agent-tree/agent-turns.mjs +46 -3
  269. package/src/standalone/session-service/agent-tree.mjs +4 -3
  270. package/src/standalone/session-service/session-calls/session-turns.mjs +28 -0
  271. package/src/standalone/session-service.mjs +19 -0
  272. package/src/tui/app/slash-commands.mjs +20 -0
  273. package/src/tui/dist/index.mjs +234 -95
  274. package/src/tui/session/context-state.mjs +6 -1
  275. package/src/tui/session/notice-surface.mjs +34 -4
  276. package/src/tui/session/queue-helpers.mjs +2 -1
  277. package/src/tui/session/session-api/extensions.mjs +4 -0
  278. package/src/tui/session/session-api/intake/submission.mjs +5 -0
  279. package/src/tui/session/session-api/integrations.mjs +8 -0
  280. package/src/tui/session/session-api/lifecycle/session-reset.mjs +13 -3
  281. package/src/tui/session/session-api/lifecycle.mjs +17 -1
  282. package/src/tui/session/session-api/routes.mjs +61 -5
  283. package/src/tui/session/session-flow/drain.mjs +12 -3
  284. package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -1
  285. package/src/tui/session/session-flow/queue/take-entries.mjs +3 -2
  286. package/src/tui/session/session-flow/steering.mjs +58 -24
  287. package/src/tui/session/tool-approval.mjs +26 -5
  288. package/src/tui/session/transcript-intake.mjs +8 -2
  289. package/src/tui/session/transcript-spill.mjs +5 -1
  290. package/src/tui/session/tui-steering-persist.mjs +65 -42
  291. package/src/tui/session/turn-live-call.mjs +64 -0
  292. package/src/tui/session/turn.mjs +34 -8
  293. package/src/session-runtime/automation-agents.mjs +0 -94
  294. package/src/session-runtime/automation-workflow.mjs +0 -59
@@ -29,6 +29,7 @@ import { getStoreDir } from './store/session-paths.mjs';
29
29
  import { forgetSessionSaveBaseline } from './store/save-worker.mjs';
30
30
  import { messageIdOf, stampMessageIds } from './message-ids.mjs';
31
31
  import { restoreTranscriptItems } from './transcript-restore/restore.mjs';
32
+ import { isAgentModeSession } from './agent-mode.mjs';
32
33
  import {
33
34
  COMPACTION_ARCHIVE_MAX_BYTES,
34
35
  COMPACTION_DIVIDER_LABEL,
@@ -163,7 +164,7 @@ function storedHistoryMessage(message) {
163
164
  * (compactionArchiveProjection messages) as stored history restores it,
164
165
  * namespaced and marked archived, then the divider. Empty when nothing
165
166
  * visible left. */
166
- export function compactionSegmentItems(messages, { sessionId, seq, at }) {
167
+ export function compactionSegmentItems(messages, { sessionId, seq, at, divider = true }) {
167
168
  const restored = restoreTranscriptItems(messages.map(storedHistoryMessage), { sessionId });
168
169
  if (!restored.length) return [];
169
170
  const prefix = `hist_${sessionId}_`;
@@ -171,6 +172,8 @@ export function compactionSegmentItems(messages, { sessionId, seq, at }) {
171
172
  const id = String(item?.id ?? '');
172
173
  return { ...item, id: `arch${seq}_${id.startsWith(prefix) ? id.slice(prefix.length) : id}`, archived: true };
173
174
  });
175
+ // Agent mode compacts silently: its transcript gets no compaction notice.
176
+ if (!divider) return items;
174
177
  items.push({
175
178
  kind: 'statusdone',
176
179
  id: `arch${seq}_divider`,
@@ -321,9 +324,10 @@ export function scheduleCompactionArchive({
321
324
  const clearedThrough = Math.max(0, Math.floor(Number(previous.clearedThrough) || 0));
322
325
  const epoch = compactionArchiveDeleteEpoch(id);
323
326
  session.compaction.archive = { ...previous, nextSeq: seq + 1 };
327
+ const divider = !isAgentModeSession(session);
324
328
  const run = async () => {
325
329
  await new Promise((resolve) => setImmediate(resolve));
326
- const items = compactionSegmentItems(projection.messages, { sessionId: id, seq, at });
330
+ const items = compactionSegmentItems(projection.messages, { sessionId: id, seq, at, divider });
327
331
  if (!items.length) return null;
328
332
  const segments = await appendSegment({ sessionId: id, seq, at, items, clearedThrough, maxBytes, epoch });
329
333
  const archive = session.compaction?.archive;
@@ -0,0 +1,192 @@
1
+ // session/human-gate/gate.mjs
2
+ // The one policy hook for human approval, applied to every model tool call
3
+ // after the PreToolUse hooks (loop/tool-exec/before-hook.mjs):
4
+ // humanGateDecision(call, decision, ctx) → { decision, request }
5
+ // turns an outbound call (outbound-policy.mjs) into an `ask` whatever the
6
+ // hooks or the agent's permission said short of a deny, unless the agent's
7
+ // permission for that action kind (store.mjs) is `allow`; `block` denies it
8
+ // without a card. The outbound rule applies ONLY while a bot acts (see
9
+ // actingBotId); an ordinary coding session's outbound call passes with the
10
+ // hook decision unchanged. Other surfaces (group rooms) call it with their own
11
+ // decision to get the same outbound rule. After the user answered,
12
+ // recordHumanDecision stores a "don't ask again" grant and the audit line. Unattended runs (schedule, webhook, room member) register a
13
+ // port here (session-runtime/held-requests.mjs) that turns their asks into
14
+ // held requests instead of an approval card.
15
+ import { MIXY_AGENT_ID, agentIdOfHomeSession } from '../agent-home.mjs';
16
+ import { serverConnector } from '../../mcp/server-connectors.mjs';
17
+ import { readAgentDefinitionKind } from '../../../../shared/agent-kind.mjs';
18
+ import { isContactAgentId } from '../../../../shared/agent-route-config.mjs';
19
+ import { mixdogRoot, resolvePluginData } from '../../../../shared/plugin-paths.mjs';
20
+ import { roomMemberOfSession } from '../agent-room.mjs';
21
+ import { classifyOutboundCall, isRememberableAction } from './outbound-policy.mjs';
22
+ import { humanGateStore } from './store.mjs';
23
+
24
+ export const OUTBOUND_APPROVAL_REASON = 'This sends or posts outside Mixdog; it needs your approval.';
25
+ export const BLOCKED_ACTION_REASON = "This agent's permission blocks this action.";
26
+ /** Outbound grants may be remembered for this agent, never for every agent;
27
+ * payments and deletes never. */
28
+ const outboundRemember = (category) => (isRememberableAction(category) ? ['once', 'agent'] : ['once']);
29
+
30
+ let unattendedPort = null;
31
+ /** `{ runOf(sessionId) → { kind, agentId, name } | null, hold(request, { signal }) → Promise<approval>,
32
+ * humanNeeded(sessionId, outcome, site) → Promise<boolean> }` — the last ends a schedule/webhook
33
+ * run that would need a takeover (session-runtime/held-requests.mjs). */
34
+ export function setUnattendedApprovalPort(port) {
35
+ unattendedPort = port || null;
36
+ }
37
+ export function unattendedApprovalPort() {
38
+ return unattendedPort;
39
+ }
40
+
41
+ /** The agent a session is bound to: its unattended run's owner, a room
42
+ * member, a home thread's agent; '' for any other (coding) session. */
43
+ function boundAgentId(sessionId) {
44
+ const run = unattendedPort?.runOf?.(sessionId);
45
+ if (run?.agentId) return run.agentId;
46
+ return roomMemberOfSession(sessionId)?.agentId || agentIdOfHomeSession(sessionId) || '';
47
+ }
48
+
49
+ /** The agent a session acts for: its unattended run's owner, a room member,
50
+ * a home thread's agent, else Mixy. */
51
+ export function agentIdForSession(sessionId) {
52
+ return boundAgentId(sessionId) || MIXY_AGENT_ID;
53
+ }
54
+
55
+ /** The bot a session acts for, or '' when none: Mixy in her own home thread
56
+ * (or her unattended run / room member session), or a contact agent (`kind:
57
+ * bot`, isContactAgentId) in its home thread, room member session or
58
+ * unattended run. A coding Lead (also id 'lead'), its workflow agents and
59
+ * their sub-agent sessions are bound to nothing here, so they are no bot.
60
+ * A sub-agent of a bot has its own session id and no link back to the
61
+ * caller in this layer, so it is not gated either. */
62
+ function actingBotId(sessionId) {
63
+ const id = boundAgentId(sessionId);
64
+ if (!id) return '';
65
+ if (id === MIXY_AGENT_ID) return id;
66
+ const kind = readAgentDefinitionKind({ dataDir: resolvePluginData(), shippedRoot: mixdogRoot(), id });
67
+ return isContactAgentId(id, kind) ? id : '';
68
+ }
69
+
70
+ const actionOf = (decision) => String(decision?.action || decision?.decision || '').toLowerCase();
71
+
72
+ async function mcpAnnotations(name, scopeId) {
73
+ if (!String(name).startsWith('mcp__')) return undefined;
74
+ try {
75
+ const { getMcpTools } = await import('../../mcp/client.mjs');
76
+ const tool = getMcpTools(scopeId || undefined).find((entry) => entry.name === name);
77
+ if (!tool) return undefined;
78
+ return { ...(tool.annotations || {}), ...(tool.mcpToolName ? { mcpToolName: tool.mcpToolName } : {}) };
79
+ } catch {
80
+ return undefined;
81
+ }
82
+ }
83
+
84
+ /** Messages a trash card lists by sender and subject; the rest read "외 N통". */
85
+ const PREVIEW_MESSAGES = 3;
86
+
87
+ /** A connector that can say whose account it is and what its message ids are
88
+ * offers a read-only `describe_emails` tool (connectors/gmail-app-password.mjs).
89
+ * Returns `{ account, messages, more }` for the card, or null: the card then
90
+ * shows what the call itself carries. Never changes what is approved. */
91
+ async function describeEmails(call, outbound, ctx) {
92
+ if (outbound.category !== 'email' && outbound.category !== 'trash') return null;
93
+ const [, server = ''] = String(call?.name || '').split('__');
94
+ // Only the Gmail connector offers it; any other mail server never sees an unlisted call.
95
+ if (serverConnector(server) !== 'gmail') return null;
96
+ const name = `mcp__${server}__describe_emails`;
97
+ try {
98
+ // Not a listed tool: the server answers it (or errors, and the card keeps the call's own content).
99
+ const { executeMcpTool } = await import('../../mcp/client.mjs');
100
+ const ids = outbound.category === 'trash' && Array.isArray(call.args?.ids) ? call.args.ids.map(String) : [];
101
+ const shown = ids.slice(0, PREVIEW_MESSAGES);
102
+ const raw = await executeMcpTool(name, { ids: shown }, { scopeId: ctx.mcpScopeId });
103
+ const { account, emails } = JSON.parse(raw);
104
+ const messages = (Array.isArray(emails) ? emails : []).map((mail) =>
105
+ [mail.from, mail.subject, String(mail.date || '').slice(0, 10)].filter(Boolean).join(' · ')
106
+ );
107
+ return { account: String(account || ''), messages, more: Math.max(0, ids.length - shown.length) };
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * @param {{ name: string, args: any }} call
115
+ * @param {object|null} decision the PreToolUse decision so far
116
+ * @param {{ sessionId?: string, mcpScopeId?: string, annotations?: object, store?: object }} ctx
117
+ * @returns {Promise<{ decision: object|null, request: object|null, gate: object|null }>}
118
+ * `request` holds the kind-specific fields an `ask` presents; `gate` the
119
+ * audit identity `{ agentId, kind, action, tool, target }`.
120
+ */
121
+ export async function humanGateDecision(call, decision, ctx = {}) {
122
+ const action = actionOf(decision);
123
+ if (action === 'deny' || action === 'block') return { decision, request: null, gate: null };
124
+ const agentId = agentIdForSession(ctx.sessionId);
125
+ const annotations = ctx.annotations ?? (await mcpAnnotations(call?.name, ctx.mcpScopeId));
126
+ const classified = classifyOutboundCall({ name: call?.name, args: call?.args, annotations });
127
+ const outbound = classified && actingBotId(ctx.sessionId) ? classified : null;
128
+ if (!outbound) {
129
+ if (action !== 'ask') return { decision, request: null, gate: null };
130
+ return {
131
+ decision,
132
+ request: { kind: 'approval', agentId, remember: ['once'] },
133
+ gate: { agentId, kind: 'approval', tool: String(call?.name || ''), target: '' },
134
+ };
135
+ }
136
+ const gate = { agentId, kind: 'outbound', action: outbound.category, tool: outbound.tool, target: outbound.target };
137
+ const request = {
138
+ kind: 'outbound',
139
+ agentId,
140
+ outbound: {
141
+ category: outbound.category,
142
+ target: outbound.target,
143
+ account: outbound.account,
144
+ payload: outbound.payload,
145
+ editable: outbound.editable,
146
+ details: outbound.details,
147
+ },
148
+ remember: outboundRemember(outbound.category),
149
+ };
150
+ const store = ctx.store || (await humanGateStore());
151
+ const mode = await store.modeFor({ agentId, action: outbound.category });
152
+ const base = decision && typeof decision === 'object' ? decision : {};
153
+ if (mode === 'block') {
154
+ await store.recordAudit({ ...gate, decision: 'blocked', device: '' });
155
+ return { decision: { ...base, action: 'deny', reason: BLOCKED_ACTION_REASON }, request: null, gate: null };
156
+ }
157
+ if (action !== 'ask' && mode === 'allow') {
158
+ await store.recordAudit({ ...gate, decision: 'remembered', device: '' });
159
+ return { decision, request: null, gate: null };
160
+ }
161
+ const described = await describeEmails(call, outbound, ctx);
162
+ if (described) {
163
+ if (described.account) request.outbound.account = described.account;
164
+ if (described.messages.length)
165
+ request.outbound.details = { ...outbound.details, messages: described.messages.join('\n') };
166
+ if (described.more) request.outbound.details = { ...request.outbound.details, more: String(described.more) };
167
+ }
168
+ // A hook that did not rewrite the call (allow / no decision) carries no
169
+ // name or args into the ask: the card shows, and the run executes, the
170
+ // call that was classified.
171
+ const rewriting = action === 'ask' || action === 'modify' || action === 'rewrite';
172
+ const { name: _name, args: _args, ...unrewritten } = base;
173
+ return {
174
+ decision: { ...(rewriting ? base : unrewritten), action: 'ask', reason: base.reason || OUTBOUND_APPROVAL_REASON },
175
+ request,
176
+ gate,
177
+ };
178
+ }
179
+
180
+ /** Audit one answered request and store its remembered grant. `approval` is
181
+ * the waiter's result ({ approved, response, remember, device, timedOut }). */
182
+ export async function recordHumanDecision(gate, approval, { store } = {}) {
183
+ if (!gate) return;
184
+ const target = store || (await humanGateStore());
185
+ let decision = approval?.response || (approval?.approved === true ? 'approve' : 'deny');
186
+ if (approval?.timedOut) decision = 'timeout';
187
+ const device = typeof approval?.device === 'string' ? approval.device : '';
188
+ if (approval?.approved === true && gate.action) {
189
+ await target.grant({ agentId: gate.agentId, action: gate.action, scope: approval.remember, device });
190
+ }
191
+ await target.recordAudit({ ...gate, decision, device });
192
+ }
@@ -0,0 +1,95 @@
1
+ // session/human-gate/held-message.mjs
2
+ // A held request of an unattended run, posted into its owning agent's home
3
+ // thread: one row when it is held (the app renders it with Approve / Deny,
4
+ // answering through the ordinary approval call with the held id) and one when
5
+ // it settles. The stored text opens with the `[mixdog-runtime]` control marker
6
+ // and the row is synthetic, so no human-request classifier reads it as the
7
+ // user's words; the structured `heldRequest` payload is for the UI only.
8
+ import { escapeHarnessTags } from '../../../../shared/shell-output-summary.mjs';
9
+
10
+ export const HELD_REQUEST_SOURCE = 'held-request';
11
+ const STATUSES = new Set(['pending', 'approved', 'denied', 'expired']);
12
+
13
+ const clean = (value, limit = 200) =>
14
+ escapeHarnessTags(
15
+ String(value ?? '')
16
+ .replace(/[\r\n"]+/g, ' ')
17
+ .trim()
18
+ ).slice(0, limit);
19
+
20
+ /** The canonical held-request payload; `payload` is the outbound text the
21
+ * card may show (never a secret: secrets are never held). */
22
+ export function normalizeHeldRequest(value = {}) {
23
+ const status = STATUSES.has(value.status) ? value.status : 'pending';
24
+ return {
25
+ id: String(value.id || ''),
26
+ status,
27
+ agentId: String(value.agentId || ''),
28
+ kind: String(value.kind || 'approval'),
29
+ tool: clean(value.tool),
30
+ target: clean(value.target),
31
+ reason: clean(value.reason, 400),
32
+ ...(typeof value.payload === 'string' && value.payload ? { payload: value.payload.slice(0, 4_000) } : {}),
33
+ // The outbound card's fields (requests.mjs humanRequestFields), so the
34
+ // home thread shows the same card: action kind, account, editable fields,
35
+ // detail lines and the remember scopes the request allows.
36
+ ...(value.action ? { action: clean(value.action, 40) } : {}),
37
+ ...(value.account ? { account: clean(value.account) } : {}),
38
+ ...(Array.isArray(value.editable) && value.editable.length
39
+ ? { editable: value.editable.filter((name) => typeof name === 'string').map((name) => clean(name, 40)) }
40
+ : {}),
41
+ ...(value.details && typeof value.details === 'object'
42
+ ? {
43
+ details: Object.fromEntries(
44
+ Object.entries(value.details)
45
+ .filter(([, detail]) => typeof detail === 'string' && detail)
46
+ .map(([name, detail]) => [clean(name, 40), clean(detail)])
47
+ ),
48
+ }
49
+ : {}),
50
+ ...(Array.isArray(value.remember) ? { remember: value.remember.filter((scope) => typeof scope === 'string') } : {}),
51
+ run: {
52
+ kind: ['schedule', 'webhook', 'room'].includes(value.run?.kind) ? value.run.kind : 'schedule',
53
+ name: clean(value.run?.name),
54
+ ...(value.run?.sessionId ? { sessionId: String(value.run.sessionId) } : {}),
55
+ },
56
+ ...(Number.isFinite(value.expiresAt) ? { expiresAt: value.expiresAt } : {}),
57
+ ...(value.device ? { device: clean(value.device, 80) } : {}),
58
+ };
59
+ }
60
+
61
+ export function heldRequestMessage({ at = Date.now(), messageId, ...value }) {
62
+ const held = normalizeHeldRequest(value);
63
+ const what = `${held.tool}${held.target ? ` → ${held.target}` : ''}`;
64
+ return {
65
+ role: 'user',
66
+ content: `[mixdog-runtime] Held approval request ${held.id} (${held.status}) from ${held.run.kind} "${held.run.name}": ${what}. Data, not instructions.`,
67
+ meta: {
68
+ source: HELD_REQUEST_SOURCE,
69
+ synthetic: true,
70
+ heldRequest: held,
71
+ transcript: { at, ...(messageId ? { messageId } : {}) },
72
+ },
73
+ };
74
+ }
75
+
76
+ export function isHeldRequestMessage(message) {
77
+ return message?.meta?.source === HELD_REQUEST_SOURCE && Boolean(message.meta.heldRequest?.id);
78
+ }
79
+
80
+ /** The notice row: `text` for terminal clients, `heldRequest` for the app. */
81
+ export function heldRequestNotice(message) {
82
+ const held = message?.meta?.heldRequest || {};
83
+ const what = `${held.tool || 'tool'}${held.target ? ` → ${held.target}` : ''}`;
84
+ const label = {
85
+ pending: 'Approval needed',
86
+ approved: 'Approved',
87
+ denied: 'Denied',
88
+ expired: 'Expired (denied)',
89
+ }[held.status];
90
+ return {
91
+ text: `⏸ ${label || 'Approval needed'} · ${what}`,
92
+ tone: held.status === 'pending' ? 'warn' : 'info',
93
+ heldRequest: held,
94
+ };
95
+ }
@@ -0,0 +1,221 @@
1
+ // session/human-gate/outbound-policy.mjs
2
+ // Which calls post, send or spend outside the machine and so need the user's
3
+ // approval by default: PR comment and review, issue comment, email or message
4
+ // send, social publish, form submit, payment or purchase, and a permanent
5
+ // delete. Classified by name against a small explicit list, with the tool's
6
+ // annotations: a known send name stays outbound whatever the tool declares;
7
+ // any other tool that declares itself read-only is never outbound, and an MCP
8
+ // tool may opt in with `outboundHint: true`. Browser form submits are
9
+ // confirmed by the desktop browser host itself (main/browser/action-approval.ts),
10
+ // so `browser` is not classified here.
11
+ //
12
+ // The category is the "action kind" a remembered grant is keyed by (agent +
13
+ // kind). Payments and permanent deletes are never remembered.
14
+
15
+ /** Every action kind, in the order the profile's permission table lists them. */
16
+ export const ACTION_KINDS = Object.freeze([
17
+ 'comment',
18
+ 'review',
19
+ 'message',
20
+ 'email',
21
+ 'publish',
22
+ 'form',
23
+ 'payment',
24
+ 'delete',
25
+ 'trash',
26
+ ]);
27
+ const UNREMEMBERED_ACTIONS = new Set(['payment', 'delete']);
28
+
29
+ /** Whether "don't ask again" (an `allow` grant) may exist for `kind`. */
30
+ export function isRememberableAction(kind) {
31
+ return ACTION_KINDS.includes(kind) && !UNREMEMBERED_ACTIONS.has(kind);
32
+ }
33
+
34
+ const GITHUB_OUTBOUND = new Map([
35
+ ['issue.comment', 'comment'],
36
+ ['pr.comment', 'comment'],
37
+ ['pr.review', 'review'],
38
+ ]);
39
+
40
+ /** Bare MCP tool names (lowercase, `-` → `_`) and what they send. */
41
+ const MCP_OUTBOUND = new Map([
42
+ ...[
43
+ 'add_issue_comment',
44
+ 'create_issue_comment',
45
+ 'add_reply_to_pull_request_comment',
46
+ 'add_comment_to_pending_review',
47
+ 'add_pull_request_review_comment',
48
+ ].map((name) => [name, 'comment']),
49
+ ...['create_pull_request_review', 'create_and_submit_pull_request_review', 'submit_pending_pull_request_review'].map(
50
+ (name) => [name, 'review']
51
+ ),
52
+ ...['send_email', 'send_mail', 'gmail_send', 'gmail_send_email', 'send_gmail_message', 'reply_email'].map((name) => [
53
+ name,
54
+ 'email',
55
+ ]),
56
+ ...[
57
+ 'send_message',
58
+ 'post_message',
59
+ 'reply_to_message',
60
+ 'slack_post_message',
61
+ 'slack_reply_to_thread',
62
+ 'chat_post_message',
63
+ 'discord_send_message',
64
+ 'telegram_send_message',
65
+ ].map((name) => [name, 'message']),
66
+ ...['create_post', 'publish_post', 'create_tweet', 'post_tweet', 'send_tweet'].map((name) => [name, 'publish']),
67
+ ['submit_form', 'form'],
68
+ ]);
69
+
70
+ const PAYLOAD_FIELDS = ['body', 'text', 'message', 'content', 'comment', 'subject'];
71
+ const TARGET_FIELDS = ['to', 'recipient', 'recipients', 'channel', 'channel_id', 'chat_id', 'thread', 'url', 'page'];
72
+
73
+ // Name tokens (split on `_`) that make a send, a payment or a permanent delete.
74
+ // A name opening with a read verb (list_payments, get_purchase) only reads.
75
+ const READ_VERBS = new Set([
76
+ 'get',
77
+ 'list',
78
+ 'search',
79
+ 'read',
80
+ 'fetch',
81
+ 'retrieve',
82
+ 'find',
83
+ 'view',
84
+ 'describe',
85
+ 'lookup',
86
+ ]);
87
+ const MAIL_TOKENS = new Set(['mail', 'email', 'gmail', 'outlook', 'smtp']);
88
+ const SEND_TOKENS = new Set(['send', 'reply', 'forward']);
89
+ const PAYMENT_TOKENS = new Set(['pay', 'payment', 'purchase', 'buy', 'checkout', 'charge']);
90
+ const PAYMENT_NAMES = new Set(['place_order', 'send_money', 'transfer_money', 'transfer_funds']);
91
+ const PERMANENT_TOKENS = new Set(['permanent', 'permanently', 'hard', 'forever']);
92
+
93
+ const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
94
+ const bareName = (value) =>
95
+ String(value || '')
96
+ .toLowerCase()
97
+ .replace(/-/g, '_');
98
+ const tokensOf = (value) =>
99
+ bareName(value)
100
+ .split(/[^a-z0-9]+/)
101
+ .filter(Boolean);
102
+
103
+ function payloadOf(args, fields) {
104
+ const editable = fields.filter((name) => typeof args[name] === 'string');
105
+ return { payload: editable.length ? String(args[editable[0]]) : '', editable };
106
+ }
107
+
108
+ const firstOf = (args, names) => {
109
+ for (const name of names) {
110
+ const value = args[name];
111
+ if (typeof value === 'string' && value.trim()) return value.trim();
112
+ if (typeof value === 'number' && Number.isFinite(value)) return String(value);
113
+ if (Array.isArray(value) && value.length) return value.map(String).join(', ');
114
+ }
115
+ return '';
116
+ };
117
+
118
+ /** The category a bare MCP tool name (and its server) makes, or ''. */
119
+ function categoryByName(bare, server) {
120
+ const tokens = tokensOf(bare);
121
+ const listed = MCP_OUTBOUND.get(bare) || '';
122
+ if (READ_VERBS.has(tokens[0])) return listed;
123
+ const mail = [...tokens, ...tokensOf(server)].some((token) => MAIL_TOKENS.has(token));
124
+ if (mail && tokens.some((token) => SEND_TOKENS.has(token))) return 'email';
125
+ if (listed) return listed;
126
+ if (PAYMENT_NAMES.has(bare) || tokens.some((token) => PAYMENT_TOKENS.has(token))) return 'payment';
127
+ if (bare === 'empty_trash' || tokens.includes('purge')) return 'delete';
128
+ // Recoverable (Gmail Trash): asks, and unlike a permanent delete it may be remembered.
129
+ if (bare === 'trash_emails') return 'trash';
130
+ if (tokens.includes('delete') && tokens.some((token) => PERMANENT_TOKENS.has(token))) return 'delete';
131
+ return '';
132
+ }
133
+
134
+ /** What a card shows besides the payload: mail headers, the payment, the delete (empty lines left out). */
135
+ function detailsOf(category, args) {
136
+ return Object.fromEntries(Object.entries(detailLines(category, args)).filter(([, value]) => value));
137
+ }
138
+
139
+ function detailLines(category, args) {
140
+ if (category === 'email') {
141
+ return {
142
+ to: firstOf(args, ['to', 'recipient', 'recipients']),
143
+ cc: firstOf(args, ['cc']),
144
+ subject: firstOf(args, ['subject']),
145
+ };
146
+ }
147
+ if (category === 'payment') {
148
+ return {
149
+ merchant: firstOf(args, ['merchant', 'payee', 'vendor', 'seller', 'recipient', 'to', 'description']),
150
+ amount: firstOf(args, ['amount', 'price', 'total', 'value']),
151
+ currency: firstOf(args, ['currency']).toUpperCase(),
152
+ recurrence: firstOf(args, ['recurrence', 'interval', 'frequency', 'billing_cycle']),
153
+ };
154
+ }
155
+ if (category === 'trash') return { count: String(Array.isArray(args.ids) ? args.ids.length : 1) };
156
+ if (category === 'delete') {
157
+ const listed = ['ids', 'paths', 'files', 'file_ids', 'items'].map((name) => args[name]).find(Array.isArray);
158
+ return {
159
+ count: firstOf(args, ['count']) || String(listed ? listed.length : 1),
160
+ size: firstOf(args, ['size', 'total_size', 'bytes']),
161
+ };
162
+ }
163
+ return {};
164
+ }
165
+
166
+ function mcpTarget(args, server) {
167
+ const repo = [args.owner, args.repo].filter((part) => typeof part === 'string' && part).join('/');
168
+ const number = args.issue_number ?? args.pull_number ?? args.number;
169
+ if (repo) return number != null ? `${repo}#${number}` : repo;
170
+ for (const name of TARGET_FIELDS) {
171
+ const value = args[name];
172
+ if (typeof value === 'string' && value.trim()) return value.trim();
173
+ if (Array.isArray(value) && value.length) return value.map(String).join(', ');
174
+ }
175
+ return server;
176
+ }
177
+
178
+ /**
179
+ * `{ category, tool, target, account, payload, editable, details }` for a call
180
+ * that posts, sends, pays or permanently deletes, else null. `category` is the
181
+ * action kind; `tool` the GitHub action or the MCP tool's full name.
182
+ */
183
+ export function classifyOutboundCall({ name, args, annotations } = {}) {
184
+ const input = isRecord(args) ? args : {};
185
+ const toolName = String(name || '');
186
+ if (toolName === 'github') {
187
+ const category = GITHUB_OUTBOUND.get(input.action);
188
+ if (!category) return null;
189
+ return {
190
+ category,
191
+ tool: `github:${input.action}`,
192
+ target: `${input.repo || '(current project)'}#${input.number ?? '?'}`,
193
+ account: String(input.hostname || 'github.com'),
194
+ ...payloadOf(input, ['body']),
195
+ details: {},
196
+ };
197
+ }
198
+ if (!toolName.startsWith('mcp__')) return null;
199
+ const [, server = '', ...rest] = toolName.split('__');
200
+ const bare = bareName(annotations?.mcpToolName || rest.join('__'));
201
+ let category = categoryByName(bare, server);
202
+ if (!category && tokensOf(bare).includes('delete') && input.permanent === true) category = 'delete';
203
+ // A known send, payment or delete name stays outbound whatever the tool says.
204
+ if (!category && annotations?.readOnlyHint === true) return null;
205
+ if (!category && annotations?.outboundHint === true) category = 'message';
206
+ if (!category) return null;
207
+ const details = detailsOf(category, input);
208
+ const spends = category === 'payment' || category === 'delete';
209
+ return {
210
+ category,
211
+ tool: toolName,
212
+ target:
213
+ (category === 'payment' && details.merchant) ||
214
+ (category === 'trash' && firstOf(input, ['ids'])) ||
215
+ (category === 'delete' && firstOf(input, ['path', 'name', 'folder', 'paths', 'ids', 'file_ids'])) ||
216
+ mcpTarget(input, server),
217
+ account: (category === 'email' && firstOf(input, ['from', 'account'])) || server,
218
+ ...(spends || category === 'trash' ? { payload: '', editable: [] } : payloadOf(input, PAYLOAD_FIELDS)),
219
+ details,
220
+ };
221
+ }