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
@@ -1,5 +1,7 @@
1
1
  // Deferred-pool and MCP-instruction manifests injected into BP2.
2
- import { parseMcpToolName } from '../../../shared/tool-primitives.mjs';
2
+ import { accountOfInstructions } from '../../../shared/connector-accounts.mjs';
3
+ import { connectorTrigger } from '../../../shared/connector-triggers.mjs';
4
+ import { parseMcpToolName, titleCaseMcpServer } from '../../../shared/tool-primitives.mjs';
3
5
  import { isToolBridgeEnabled, TOOL_BRIDGE_GUIDANCE } from '../runtime-core/tool-exposure.mjs';
4
6
 
5
7
  export function compactPromptManifestText(value, max = 250) {
@@ -100,11 +102,8 @@ function sanitizeMcpInstructionText(text, max = MCP_INSTRUCTION_MAX_CHARS) {
100
102
  /**
101
103
  * Per-server MCP initialize instructions for deferred-pool tools only.
102
104
  * Empty when no instructions or no matching deferred MCP tools → omit block.
103
- * Emits ONLY the server heading + instruction body: the per-server tool names
104
- * are deliberately NOT repeated here — every pool tool is already listed once
105
- * (with its description) in <available-deferred-tools>, and re-listing ~30
106
- * names per server doubled the MCP share of the BP2 prefix (2026-08-05 audit).
107
- * Server membership stays evident from the mcp__<server>__ name prefix.
105
+ * Emits ONLY the server heading + instruction body: tool names are never
106
+ * listed — <available-deferred-tools> names each server once (mcp__<server>).
108
107
  */
109
108
  function buildMcpInstructionsManifest(mcpServerInstructions, poolNames, catalog = []) {
110
109
  const map = mcpServerInstructions && typeof mcpServerInstructions === 'object' ? mcpServerInstructions : {};
@@ -163,18 +162,82 @@ function rebuildDeferredToolManifestBlock(text, manifest) {
163
162
  return out;
164
163
  }
165
164
 
166
- function deferredManifestEntries(session, pool) {
167
- const descByName = new Map();
165
+ function catalogByName(session) {
166
+ const byName = new Map();
168
167
  for (const tool of Array.isArray(session?.deferredToolCatalog) ? session.deferredToolCatalog : []) {
169
168
  const name = String(tool?.name || '').trim();
170
- // Skill-style selection trigger when the tool declares one; else its description.
171
- if (name && !descByName.has(name)) descByName.set(name, String(tool?.trigger || tool?.description || ''));
169
+ if (name && !byName.has(name)) byName.set(name, tool);
172
170
  }
171
+ return byName;
172
+ }
173
+
174
+ // MCP tools are listed once per server: load_tool loads every tool of
175
+ // `mcp__<server>`. Tool definitions carry their server (shortened names
176
+ // cannot be parsed).
177
+ const mcpServerOf = (byName, name) => parseMcpToolName(byName.get(String(name).trim()) || name)?.server || '';
178
+ const mcpServerLineName = (server) => `mcp__${server}`;
179
+ const firstSentence = (text) => {
180
+ const flat = String(text || '')
181
+ .replace(/\s+/g, ' ')
182
+ .trim();
183
+ return (flat.split(/(?<=[.!?])\s/, 1)[0] || flat).replace(/[.!?]+$/, '');
184
+ };
185
+
186
+ // A catalog connector's curated trigger; else the server's MCP instructions;
187
+ // else its tools' leading descriptions (name order); else its name. Derived
188
+ // lines are compacted to at most 100 characters, ellipsis included.
189
+ function mcpServerTrigger(server, tools, instructions) {
190
+ // A multi-account connector's server names its account (connector-accounts.mjs).
191
+ const account = accountOfInstructions(instructions?.[server]);
192
+ const curated = connectorTrigger(server);
193
+ if (curated) return account ? `${curated} Account: ${account}` : curated;
194
+ const instruction = String(instructions?.[server] || '').replace(/[<>]/g, '');
195
+ const described = [...tools]
196
+ .sort((a, b) => String(a?.name || '').localeCompare(String(b?.name || '')))
197
+ .map((tool) => firstSentence(String(tool?.description || '').replace(/[<>]/g, '')))
198
+ .filter(Boolean)
199
+ .join('; ');
200
+ const derived = instruction.trim() ? instruction : described;
201
+ return derived ? compactPromptManifestText(derived, 98) : `${titleCaseMcpServer(server)} MCP server tools.`;
202
+ }
203
+
204
+ function deferredManifestEntries(session, pool) {
205
+ const byName = catalogByName(session);
173
206
  const skillRoutedNames = skillRoutedToolNames(session.messages);
174
- return pool.map((name) => ({
175
- name,
176
- description: skillRoutedNames.has(String(name).trim()) ? '' : descByName.get(String(name).trim()) || '',
177
- }));
207
+ const entries = [];
208
+ const servers = new Map();
209
+ for (const name of pool) {
210
+ const key = String(name).trim();
211
+ const tool = byName.get(key);
212
+ const server = mcpServerOf(byName, name);
213
+ if (server) {
214
+ if (!servers.has(server)) servers.set(server, []);
215
+ servers.get(server).push(tool || { name: key });
216
+ continue;
217
+ }
218
+ // Skill-style selection trigger when the tool declares one; else its description.
219
+ entries.push({
220
+ name,
221
+ description: skillRoutedNames.has(key) ? '' : String(tool?.trigger || tool?.description || ''),
222
+ });
223
+ }
224
+ for (const [server, tools] of servers) {
225
+ entries.push({
226
+ name: mcpServerLineName(server),
227
+ description: mcpServerTrigger(server, tools, session.mcpServerInstructions),
228
+ });
229
+ }
230
+ return entries;
231
+ }
232
+
233
+ /** The pool names `rendered` lists: by their own line, or their MCP server's. */
234
+ export function listedDeferredToolNames(session, poolNames, rendered) {
235
+ const byName = catalogByName(session);
236
+ const text = String(rendered || '');
237
+ return (Array.isArray(poolNames) ? poolNames : []).filter((name) => {
238
+ const server = mcpServerOf(byName, name);
239
+ return text.includes(server ? `- ${mcpServerLineName(server)}: ` : name);
240
+ });
178
241
  }
179
242
 
180
243
  function composeDeferredBp2Manifest(session, pool) {
@@ -1,12 +1,15 @@
1
1
  // Agent-scoped role markdown and 4-BP system prompt composition.
2
2
 
3
+ import { createHash } from 'node:crypto';
3
4
  import { existsSync, readdirSync } from 'node:fs';
4
- import { join } from 'node:path';
5
+ import { basename, dirname, join } from 'node:path';
5
6
  import { compareCodePoints } from '../../../shared/code-point-order.mjs';
7
+ import { BOTS_DIR, MIXY_BOT_ID, botStoreActive, isStoreBot, mixyIdentityDir } from '../../../shared/agent-store.mjs';
6
8
  import { mixdogRoot } from '../../../shared/plugin-paths.mjs';
7
9
  import { readMarkdownDocument } from '../../../shared/markdown-frontmatter.mjs';
8
10
  import { listHiddenAgentsByKind, getAgentCatalogShareAgents } from '../internal-agents.mjs';
9
11
  import { mixdogGlobalDir, mtimeWithTtl, readSafe } from './skill-catalog.mjs';
12
+ import { mixyDisplayName } from '../session/mixy-name.mjs';
10
13
 
11
14
  // --- Agent-scoped instruction loader ---
12
15
  // Emits a BP2 agent/system block scoped to the calling agent:
@@ -45,6 +48,12 @@ const _scopedRoleInstructionsCache = new Map();
45
48
  const _scopedRoleInstructionsMtimeCache = new Map();
46
49
  const _ROLE_INSTRUCTIONS_MTIME_TTL_MS = 2000;
47
50
 
51
+ /** A saved AGENT.md applies to the next composition, even inside the mtime TTL. */
52
+ export function invalidateRoleInstructionsCache() {
53
+ _scopedRoleInstructionsCache.clear();
54
+ _scopedRoleInstructionsMtimeCache.clear();
55
+ }
56
+
48
57
  function loadHiddenAgentSnippets(pluginRoot) {
49
58
  try {
50
59
  const agentRulesDir = join(pluginRoot, 'rules', 'agent');
@@ -81,9 +90,35 @@ function agentSectionDirs(pluginRoot) {
81
90
  } catch {
82
91
  /* unresolvable data dir — built-in roles still load */
83
92
  }
93
+ // Bots of the store (agent-store.mjs), last: a bot's own folder wins over
94
+ // the agents/ original the split left behind.
95
+ const root = userDataRoot();
96
+ if (root && botStoreActive(root)) dirs.push(join(root, BOTS_DIR));
84
97
  return dirs;
85
98
  }
86
99
 
100
+ // The user data dir (parent of agents/), or '' when it cannot be resolved.
101
+ function userDataRoot() {
102
+ try {
103
+ const userDir = mixdogGlobalDir('agents');
104
+ return userDir ? dirname(userDir) : '';
105
+ } catch {
106
+ return '';
107
+ }
108
+ }
109
+
110
+ // A store bot's own folder, or ''.
111
+ function storeBotDir(agent) {
112
+ const root = userDataRoot();
113
+ return root && isStoreBot(root, agent) ? join(root, BOTS_DIR, agent) : '';
114
+ }
115
+
116
+ // Mixy's identity folder: bots/mixy with the store, agents/lead before.
117
+ function mixyDir() {
118
+ const root = userDataRoot();
119
+ return root ? mixyIdentityDir(root) : '';
120
+ }
121
+
87
122
  function loadAgentSections(pluginRoot) {
88
123
  // agents/ accepts both the compatibility flat layout and the current
89
124
  // nested agents/<agent>/AGENT.md layout.
@@ -106,7 +141,10 @@ function loadAgentSections(pluginRoot) {
106
141
  name = entry.name.replace(/\.md$/, '');
107
142
  raw = readSafe(join(agentsDir, entry.name));
108
143
  }
109
- if (!name || !raw) continue;
144
+ // Mixy's own AGENT.md (agents/lead/) rides only Mixy's agent-mode
145
+ // prompt (loadMixyRoleSection), never another role catalog.
146
+ if (!name || !raw || name === 'lead') continue;
147
+ if (name === MIXY_BOT_ID && basename(agentsDir) === BOTS_DIR) continue;
110
148
  const { body } = readMarkdownDocument(raw);
111
149
  if (!body) continue;
112
150
  byName.set(name, roleSection(name, body));
@@ -225,6 +263,174 @@ export function loadScopedRoleInstructions(agent, provider = null) {
225
263
  }
226
264
  }
227
265
 
266
+ // --- Agent persona (agents/<id>/PERSONA.md) ---
267
+ // Optional tone/addressing/personality guide, rendered right after the
268
+ // agent's role section. Read from the same directory that supplies the
269
+ // agent's AGENT.md (user data dir first, then shipped); the Lead (`lead`,
270
+ // Mixy) has no AGENT.md, so its persona is the first PERSONA.md found.
271
+ // The text depends only on the file, so the BP3 prefix is stable until the
272
+ // file changes; it is re-read per composition, so an edit applies next turn.
273
+ export const PERSONA_MAX_CHARS = 4000;
274
+ export const PERSONA_TRUNCATION_MARKER = '\n\n[persona truncated]';
275
+ const PERSONA_AGENT_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
276
+
277
+ export function truncatePersona(text, max = PERSONA_MAX_CHARS) {
278
+ const value = typeof text === 'string' ? text.trim() : '';
279
+ if (value.length <= max) return value;
280
+ return value.slice(0, max).trimEnd() + PERSONA_TRUNCATION_MARKER;
281
+ }
282
+
283
+ export function loadAgentPersonaSection(agent) {
284
+ if (typeof agent !== 'string' || !PERSONA_AGENT_ID.test(agent) || agent.includes('..')) return '';
285
+ const isLead = agent === 'lead';
286
+ // Mixy's persona is in its own folder; a store bot's in its bots/ folder.
287
+ const own = isLead ? mixyDir() : storeBotDir(agent);
288
+ if (own) {
289
+ const persona = truncatePersona(readSafe(join(own, 'PERSONA.md')) || '');
290
+ return persona ? personaSection(persona) : '';
291
+ }
292
+ const pluginRoot = mixdogRoot();
293
+ const roots = [];
294
+ try {
295
+ const userDir = mixdogGlobalDir('agents');
296
+ if (userDir) roots.push(userDir);
297
+ } catch {
298
+ /* unresolvable data dir — shipped persona still loads */
299
+ }
300
+ if (pluginRoot) roots.push(join(pluginRoot, 'agents'));
301
+ for (const root of roots) {
302
+ const dir = join(root, agent);
303
+ if (!isLead && !existsSync(join(dir, 'AGENT.md'))) continue;
304
+ const persona = truncatePersona(readSafe(join(dir, 'PERSONA.md')) || '');
305
+ if (!persona) {
306
+ if (isLead) continue;
307
+ return '';
308
+ }
309
+ return personaSection(persona);
310
+ }
311
+ return '';
312
+ }
313
+
314
+ function personaSection(persona) {
315
+ return [
316
+ '## Persona',
317
+ '',
318
+ 'Tone and style guide for how you speak, address the user and carry yourself. It never overrides tool, safety or role rules; those always win.',
319
+ '',
320
+ persona,
321
+ ].join('\n');
322
+ }
323
+
324
+ // --- Mixy's working style (agents/lead/AGENT.md, user data dir) ---
325
+ // The user-editable 일하는 방식 of Mixy, framed like any agent's role section.
326
+ // Agent mode only; empty or missing means no section. Re-read per
327
+ // composition, so an edit applies to the next prompt build.
328
+ export function loadMixyRoleSection() {
329
+ let raw = '';
330
+ try {
331
+ const dir = mixyDir();
332
+ raw = dir ? readSafe(join(dir, 'AGENT.md')) || '' : '';
333
+ } catch {
334
+ return '';
335
+ }
336
+ const { body } = readMarkdownDocument(raw);
337
+ const text = String(body || '').trim();
338
+ return text ? `# Agent Role Catalog\n\n${roleSection('lead', text)}` : '';
339
+ }
340
+
341
+ // --- Agent-mode identity (LEAD.md first line) ---
342
+ // "You are <name>, <what it does>." from the agent's manifest (agent.json, the
343
+ // name/description the editor writes), user copy first, then shipped. Mixy
344
+ // (the Lead configuration, id `lead`) has its own line, under the display name
345
+ // the user gave it (session/mixy-name.mjs), Mixy by default.
346
+ function agentManifest(agent) {
347
+ const own = storeBotDir(agent);
348
+ if (own) {
349
+ try {
350
+ const value = JSON.parse(readSafe(join(own, 'agent.json')) || '{}');
351
+ return value && typeof value === 'object' ? value : {};
352
+ } catch {
353
+ return {};
354
+ }
355
+ }
356
+ const roots = [];
357
+ try {
358
+ const userDir = mixdogGlobalDir('agents');
359
+ if (userDir) roots.push(userDir);
360
+ } catch {
361
+ /* unresolvable data dir — shipped manifest still loads */
362
+ }
363
+ const pluginRoot = mixdogRoot();
364
+ if (pluginRoot) roots.push(join(pluginRoot, 'agents'));
365
+ for (const root of roots) {
366
+ const dir = join(root, agent);
367
+ if (!existsSync(join(dir, 'AGENT.md'))) continue;
368
+ try {
369
+ const value = JSON.parse(readSafe(join(dir, 'agent.json')) || '{}');
370
+ return value && typeof value === 'object' ? value : {};
371
+ } catch {
372
+ return {};
373
+ }
374
+ }
375
+ return {};
376
+ }
377
+
378
+ export function agentModeIdentityLine(agent) {
379
+ if (agent === 'lead') return `You are ${mixyDisplayName()}, the user's assistant in Mixdog.`;
380
+ if (typeof agent !== 'string' || !PERSONA_AGENT_ID.test(agent) || agent.includes('..')) return '';
381
+ const oneLine = (value) => (typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '');
382
+ const manifest = agentManifest(agent);
383
+ const name = oneLine(manifest.name) || agent;
384
+ const role = oneLine(manifest.description).replace(/[.。]+$/, '');
385
+ return role ? `You are ${name}. Your role: ${role}.` : `You are ${name}.`;
386
+ }
387
+
388
+ // --- Agent profile revision ---
389
+ // The files an agent's prompt sections (identity line, role, persona) are
390
+ // read from — its agent.json, AGENT.md and PERSONA.md in the user data dir and
391
+ // the shipped tree — as one hash. A session stamps it when its prompt is
392
+ // composed; another value means the profile was edited since
393
+ // (session-runtime/tool-policy-refresh.mjs refreshAgentProfile). '' for an
394
+ // invalid id. A coding Lead (`lead` outside Mixy's own thread) embeds no
395
+ // profile file at all, so its stamp is a constant: editing Mixy never
396
+ // rebuilds a coding session's prompt.
397
+ export const CODING_LEAD_PROFILE_REVISION = 'coding-lead';
398
+ export function agentProfileRevision(agent, { mixy = true } = {}) {
399
+ if (typeof agent !== 'string' || !PERSONA_AGENT_ID.test(agent) || agent.includes('..')) return '';
400
+ if (agent === 'lead' && !mixy) return CODING_LEAD_PROFILE_REVISION;
401
+ const roots = [mixdogGlobalDir('agents')];
402
+ const pluginRoot = mixdogRoot();
403
+ if (pluginRoot) roots.push(join(pluginRoot, 'agents'));
404
+ const hash = createHash('sha256');
405
+ for (const root of roots) {
406
+ for (const file of ['agent.json', 'AGENT.md', 'PERSONA.md']) {
407
+ hash.update(`${root}\0${file}\0${readSafe(join(root, agent, file)) ?? '\u0001'}\0`);
408
+ }
409
+ }
410
+ // Mixy's or a store bot's own folder, when it has one.
411
+ const own = agent === 'lead' ? mixyDir() : storeBotDir(agent);
412
+ if (own && own !== join(roots[0] || '', agent)) {
413
+ for (const file of ['agent.json', 'AGENT.md', 'PERSONA.md']) {
414
+ hash.update(`${own}\0${file}\0${readSafe(join(own, file)) ?? '\u0001'}\0`);
415
+ }
416
+ }
417
+ return hash.digest('hex').slice(0, 24);
418
+ }
419
+
420
+ // --- Lead roster revision ---
421
+ // The bot roster a Lead's prompt lists (workflow/context.mjs leadPromptParts:
422
+ // the agent catalog and Mixy's contact catalog — never the orchestration
423
+ // mode, a setting that applies to new sessions), as one hash, stamped beside
424
+ // agentProfileRevision. A bot created, renamed, archived or restored changes
425
+ // it for every Lead whose prompt lists that bot.
426
+ export function leadRosterRevision(parts) {
427
+ const hash = createHash('sha256');
428
+ for (const slice of [parts?.agentCatalogContext, parts?.contactCatalogContext]) {
429
+ hash.update(`${slice || ''}\0`);
430
+ }
431
+ return hash.digest('hex').slice(0, 24);
432
+ }
433
+
228
434
  // --- Compose system prompt — 4-BP cache layout ---
229
435
  // Returns the three stable system blocks and the BP3 core used for refreshes.
230
436
  // They map directly to the breakpoint plan:
@@ -262,32 +468,53 @@ export function composeSystemPrompt(opts) {
262
468
  ]);
263
469
 
264
470
  // ── BP3: workflow/role + session environment layer ─────────────────
265
- const roleInstructionContext = opts.skipRoleCatalog
266
- ? ''
267
- : loadScopedRoleInstructions(opts.agent || null, opts.provider || null);
268
- // Keep the active workflow first within BP3 so it leads the role and
269
- // environment material that varies with the session.
471
+ let roleInstructionContext = '';
472
+ if (!opts.skipRoleCatalog) {
473
+ roleInstructionContext =
474
+ opts.agent === 'lead'
475
+ ? loadMixyRoleSection()
476
+ : loadScopedRoleInstructions(opts.agent || null, opts.provider || null);
477
+ }
478
+ // Persona follows the role; Mixy's own thread carries Mixy's persona
479
+ // (bots/mixy/PERSONA.md) — a coding Lead's prompt never does (skipPersona).
480
+ const personaContext = opts.agent && !opts.skipPersona ? loadAgentPersonaSection(opts.agent) : '';
270
481
  const coreMemory = skip.memory ? '' : trimmedPromptSlice(opts.coreMemoryContext);
271
482
  // User AGENTS.md / CLAUDE.md block (context/instruction-files.mjs), already
272
- // headed `# Instruction Files`; rides right after core memory.
483
+ // headed `# Instruction Files`; rides right before core memory.
273
484
  const instructionFiles = skip.claudemd ? '' : trimmedPromptSlice(opts.instructionFilesContext);
485
+ // Ordered for the longest shared prefix (session-prompt-composition.mjs):
486
+ // 1. shared by every Lead — the workflow pack and LEAD.md's common lines;
487
+ // 2. the mode block — coding: orchestration mode, LEAD.md's two passages,
488
+ // the full agent catalog; agent mode: the delegation block, their
489
+ // variants, AGENT_MODE.md, the sub-agent catalog (the same for every
490
+ // agent other than Mixy);
491
+ // 3. Mixy only — MIXY.md and its contact-agent catalog;
492
+ // 4. per session or agent, last — routine line, identity, caller prompt,
493
+ // role, persona, instruction files and core memory (changes most).
274
494
  const sessionMarkerCore = joinPromptSlices([
275
495
  trimmedPromptSlice(opts.workflowContext),
276
496
  opts.roleRules || '',
497
+ trimmedPromptSlice(opts.orchestrationContext),
498
+ opts.leadModeRules || '',
499
+ opts.agentModeRules || '',
500
+ trimmedPromptSlice(opts.agentCatalogContext),
501
+ opts.mixyRules || '',
502
+ trimmedPromptSlice(opts.contactCatalogContext),
503
+ opts.routineRules || '',
504
+ opts.identityContext || '',
277
505
  opts.userPrompt || '',
278
506
  roleInstructionContext,
279
- coreMemory ? `# Core Memory\n${coreMemory}` : '',
507
+ personaContext,
280
508
  instructionFiles,
509
+ coreMemory ? `# Core Memory\n${coreMemory}` : '',
281
510
  ]);
282
511
  // Response language is the LAST system text before the conversation. It
283
512
  // rides the environment block (not the tier3 core) because the `# Session`
284
513
  // / shell / git lines are English and would otherwise trail it, pulling the
285
514
  // pre-tool preamble toward English.
286
- const environmentParts = [
287
- opts.sessionStartContext,
288
- opts.environmentContext,
289
- opts.languageContext,
290
- ].map(trimmedPromptSlice);
515
+ const environmentParts = [opts.sessionStartContext, opts.environmentContext, opts.languageContext].map(
516
+ trimmedPromptSlice
517
+ );
291
518
  // Volatile session/project environment. Kept OUT of sessionMarkerCore so
292
519
  // Anthropic providers can leave it as an UNMARKED system block (covered by
293
520
  // the messages-tail breakpoint) while prefix-order providers still see it
@@ -174,8 +174,10 @@ export function collectSkills(cwd) {
174
174
  } catch {
175
175
  continue;
176
176
  }
177
- // Agent Skills requires the manifest name to match its folder.
178
- if (basename(dirname(filePath)) !== skill.name) continue;
177
+ // Agent Skills requires the manifest name to match its folder. Plugins
178
+ // are exempt: they ship nested layouts (skills/<plugin>/<topic>/) and
179
+ // load by their declared name; `seen` still settles collisions.
180
+ if (source !== 'plugin' && basename(dirname(filePath)) !== skill.name) continue;
179
181
  if (seen.has(skill.name)) continue;
180
182
  seen.add(skill.name);
181
183
  skills.push({
@@ -0,0 +1,216 @@
1
+ // Connector policy: which globally installed MCP servers (connectors) an agent
2
+ // or a project may use. Every installed connector is on by default for every
3
+ // agent and project, existing and future ones; a policy only switches some off:
4
+ // agents/<id>/agent.json connectors: { off: [serverId…], tools?: { [serverId]: { [tool]: permission } } }
5
+ // <data>/project-connectors.json { projects: { [projectRoot]: { off: [serverId…] } } }
6
+ // `tools` keeps per-tool overrides (auto | needs_approval | disabled); a tool
7
+ // without one is auto, except a catalog connector's high-risk tools, which
8
+ // ask (connectorToolDefault). A legacy agent list (an array: only its enabled
9
+ // entries were mounted) is enforced as written until the host converts it
10
+ // (migrateLegacyConnectorPolicy). A policy that cannot be read, or has neither
11
+ // shape, mounts nothing. Enforced at dispatch only: every session lists every
12
+ // connected tool, so the schema and the BP2 manifest stay shared.
13
+ import { readFileSync, statSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ import { cwdWithinProjects } from '../../../shared/extension-scopes.mjs';
16
+ import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
17
+ import { userDefinitionDir } from '../../../shared/agent-store.mjs';
18
+ import { isSelfMcpToolName, parseMcpToolName } from '../../../shared/tool-primitives.mjs';
19
+ import { connectorToolDefault } from '../../../shared/connector-catalog.mjs';
20
+ import { serverConnector } from './server-connectors.mjs';
21
+
22
+ export const CONNECTOR_PERMISSIONS = Object.freeze(['auto', 'needs_approval', 'disabled']);
23
+ export const PROJECT_CONNECTORS_FILE = 'project-connectors.json';
24
+
25
+ /** What an unreadable or malformed policy means: no connector is on. */
26
+ export const DENY_ALL_CONNECTORS = Object.freeze({
27
+ allow: Object.freeze([]),
28
+ off: Object.freeze([]),
29
+ tools: Object.freeze({}),
30
+ });
31
+
32
+ const AGENT_DIR_ID = /^[a-z0-9][a-z0-9._-]*$/i;
33
+ const POLICY_KEYS = new Set(['off', 'tools']);
34
+
35
+ const isPlainObject = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
36
+ const clean = (value) => (typeof value === 'string' ? value.trim() : '');
37
+ const uniqueIds = (list) => [...new Set((Array.isArray(list) ? list : []).map(clean).filter(Boolean))].sort();
38
+
39
+ function toolOverrides(value) {
40
+ const out = {};
41
+ for (const [server, tools] of Object.entries(isPlainObject(value) ? value : {})) {
42
+ const own = {};
43
+ for (const [tool, permission] of Object.entries(isPlainObject(tools) ? tools : {})) {
44
+ if (clean(tool) && CONNECTOR_PERMISSIONS.includes(permission)) own[clean(tool)] = permission;
45
+ }
46
+ if (clean(server) && Object.keys(own).length) out[clean(server)] = own;
47
+ }
48
+ return out;
49
+ }
50
+
51
+ /** A stored `connectors` field → `{ off, tools }`, or `{ allow, off, tools }`
52
+ * for a legacy list; null when absent. Throws on any other shape. */
53
+ export function parseConnectorPolicy(value) {
54
+ if (value == null) return null;
55
+ if (Array.isArray(value)) {
56
+ const allow = [];
57
+ const tools = {};
58
+ for (const item of value) {
59
+ const id = clean(item?.id);
60
+ if (!id) continue;
61
+ if (item.enabled === true) allow.push(id);
62
+ const own = toolOverrides({ [id]: item.tools })[id];
63
+ if (own) tools[id] = { ...tools[id], ...own };
64
+ }
65
+ return { allow: uniqueIds(allow), off: [], tools };
66
+ }
67
+ if (!isPlainObject(value) || Object.keys(value).some((key) => !POLICY_KEYS.has(key))) {
68
+ throw new Error('connectors must be { off: [connector id…], tools? }');
69
+ }
70
+ if (value.off != null && !Array.isArray(value.off)) throw new Error('connectors.off must be a list');
71
+ if (value.tools != null && !isPlainObject(value.tools)) throw new Error('connectors.tools must be an object');
72
+ return { off: uniqueIds(value.off), tools: toolOverrides(value.tools) };
73
+ }
74
+
75
+ /** The form a deny-list policy is saved in; null when it switches nothing off
76
+ * (the same as no policy). A legacy list must be migrated first. */
77
+ export function storedConnectorPolicy(policy) {
78
+ if (!policy) return null;
79
+ if (policy.allow) throw new Error('a legacy connector list must be migrated before it is saved');
80
+ const off = uniqueIds(policy.off);
81
+ const tools = toolOverrides(policy.tools);
82
+ if (!off.length && !Object.keys(tools).length) return null;
83
+ return { off, ...(Object.keys(tools).length ? { tools } : {}) };
84
+ }
85
+
86
+ /** A legacy list (or an unreadable policy) as the equivalent deny list over
87
+ * the installed connectors `installedIds`: each one it did not enable is off.
88
+ * A deny-list policy (or none) passes through. */
89
+ export function migrateLegacyConnectorPolicy(policy, installedIds) {
90
+ if (!policy?.allow) return policy;
91
+ const allow = new Set(policy.allow);
92
+ const missing = uniqueIds(installedIds).filter((id) => !allow.has(id));
93
+ return { off: uniqueIds([...policy.off, ...missing]), tools: toolOverrides(policy.tools) };
94
+ }
95
+
96
+ /** Whether connector `serverId` is on under `policy` (null: every one is). */
97
+ export function connectorEnabled(policy, serverId) {
98
+ if (!policy) return true;
99
+ if (policy.allow && !policy.allow.includes(serverId)) return false;
100
+ return !policy.off.includes(serverId);
101
+ }
102
+
103
+ // Policy files are stat-checked on every read (tiny files), so a save from
104
+ // any process applies to the next dispatch without an explicit flush.
105
+ const fileCache = new Map();
106
+
107
+ function cachedRead(path, read) {
108
+ let stat;
109
+ try {
110
+ stat = statSync(path);
111
+ } catch (error) {
112
+ // No file is the "no policy" state; any other fault means it cannot be read.
113
+ return error?.code === 'ENOENT' || error?.code === 'ENOTDIR' ? null : unreadable(path, error);
114
+ }
115
+ const cached = fileCache.get(path);
116
+ if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) return cached.value;
117
+ const value = read(path);
118
+ fileCache.set(path, { mtimeMs: stat.mtimeMs, size: stat.size, value });
119
+ return value;
120
+ }
121
+
122
+ function readJson(path, parse) {
123
+ let text;
124
+ try {
125
+ text = readFileSync(path, 'utf8');
126
+ } catch (error) {
127
+ return error?.code === 'ENOENT' || error?.code === 'ENOTDIR' ? null : unreadable(path, error);
128
+ }
129
+ try {
130
+ return parse(JSON.parse(text));
131
+ } catch (error) {
132
+ return unreadable(path, error);
133
+ }
134
+ }
135
+
136
+ // A policy that exists but cannot be trusted mounts nothing.
137
+ function unreadable(path, error) {
138
+ process.stderr.write(`[connectors] ${path} is unreadable (${error?.message || error}); denying all connectors\n`);
139
+ return DENY_ALL_CONNECTORS;
140
+ }
141
+
142
+ /** Uncached: the policy in the agent.json at `path` (see parseConnectorPolicy);
143
+ * null without a file or field, DENY_ALL_CONNECTORS when it cannot be
144
+ * trusted. The agent editor reads the policy through this too. */
145
+ export function readConnectorsFile(path) {
146
+ return readJson(path, (manifest) => parseConnectorPolicy(manifest?.connectors));
147
+ }
148
+
149
+ /** The agent's saved policy under `dataDir`, or null when it has none. */
150
+ export function agentConnectors(agentId, dataDir = resolvePluginData()) {
151
+ const id = clean(agentId);
152
+ if (!id || !AGENT_DIR_ID.test(id)) return null;
153
+ return cachedRead(join(userDefinitionDir(dataDir, id), 'agent.json'), readConnectorsFile);
154
+ }
155
+
156
+ /** Uncached: the project policy file → `[{ root, off }]`, null without one,
157
+ * DENY_ALL_CONNECTORS when it cannot be trusted. */
158
+ export function readProjectConnectorsFile(path) {
159
+ return readJson(path, (raw) => {
160
+ if (!isPlainObject(raw) || (raw.projects != null && !isPlainObject(raw.projects))) {
161
+ throw new Error('projects must be an object');
162
+ }
163
+ return Object.entries(raw.projects || {}).flatMap(([root, entry]) => {
164
+ if (!isPlainObject(entry) || (entry.off != null && !Array.isArray(entry.off))) {
165
+ throw new Error(`project "${root}" must be { off: [connector id…] }`);
166
+ }
167
+ const off = uniqueIds(entry.off);
168
+ return clean(root) && off.length ? [{ root: clean(root), off }] : [];
169
+ });
170
+ });
171
+ }
172
+
173
+ export function projectConnectorPolicies(dataDir = resolvePluginData()) {
174
+ return cachedRead(join(dataDir, PROJECT_CONNECTORS_FILE), readProjectConnectorsFile);
175
+ }
176
+
177
+ /** 'auto' | 'needs_approval' | 'disabled' for an MCP tool (definition or name)
178
+ * under an agent's policy. Non-MCP tools and Mixdog's own self server are
179
+ * not connectors: always 'auto'. */
180
+ export function connectorToolPermission(policy, toolOrName) {
181
+ const mcp = parseMcpToolName(toolOrName);
182
+ if (!mcp || isSelfMcpToolName(toolOrName)) return 'auto';
183
+ if (policy && !connectorEnabled(policy, mcp.server)) return 'disabled';
184
+ // An explicit per-tool entry decides; otherwise a catalog connector's
185
+ // high-risk tools ask (connectorToolDefault) and any other tool is auto.
186
+ return policy?.tools?.[mcp.server]?.[mcp.tool] || connectorToolDefault(serverConnector(mcp.server), mcp.tool);
187
+ }
188
+
189
+ /** The permission `agentId` has for the MCP tool `toolOrName`. */
190
+ export function agentMcpToolPermission(agentId, toolOrName) {
191
+ if (!parseMcpToolName(toolOrName)) return 'auto';
192
+ return connectorToolPermission(agentConnectors(agentId), toolOrName);
193
+ }
194
+
195
+ /** Where a session runs: its registered Project (a worktree Lead's too), and
196
+ * its cwd (a delegated worker inside the project). */
197
+ function sessionProjectPaths(session) {
198
+ return [session?.desktopSession?.projectPath, session?.workspace?.projectPath, session?.cwd]
199
+ .map(clean)
200
+ .filter(Boolean);
201
+ }
202
+
203
+ /** True when the project `session` runs in switched the tool's connector off
204
+ * (or its policy file cannot be read). */
205
+ export function projectMcpToolDisabled(session, toolOrName, dataDir = resolvePluginData()) {
206
+ const mcp = parseMcpToolName(toolOrName);
207
+ if (!mcp || isSelfMcpToolName(toolOrName)) return false;
208
+ const paths = sessionProjectPaths(session);
209
+ if (!paths.length) return false;
210
+ const policies = projectConnectorPolicies(dataDir);
211
+ if (!policies) return false;
212
+ if (policies === DENY_ALL_CONNECTORS) return true;
213
+ return policies.some(
214
+ ({ root, off }) => off.includes(mcp.server) && paths.some((path) => cwdWithinProjects(path, [root]))
215
+ );
216
+ }