mixdog 1.2.0 → 1.2.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 (201) hide show
  1. package/README.md +7 -5
  2. package/package.json +1 -1
  3. package/src/defaults/shipped-history.json +4 -0
  4. package/src/defaults/skills/pptx/SKILL.md +6 -6
  5. package/src/defaults/skills/pptx/references/review.md +6 -4
  6. package/src/defaults/skills/pptx/scripts/source-extract.mjs +1 -1
  7. package/src/defaults/skills/setup/references/actions.md +13 -3
  8. package/src/defaults/skills/visualize/SKILL.md +12 -9
  9. package/src/rules/shared/60-verification.md +3 -1
  10. package/src/rules/shared/70-delivery.md +4 -2
  11. package/src/runtime/agent/orchestrator/cache-mtime.mjs +26 -5
  12. package/src/runtime/agent/orchestrator/config.mjs +10 -4
  13. package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
  14. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +11 -5
  15. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +74 -21
  16. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +86 -8
  17. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +5 -1
  18. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +65 -7
  19. package/src/runtime/agent/orchestrator/mcp/client.mjs +128 -44
  20. package/src/runtime/agent/orchestrator/mcp/lazy-output-validator.mjs +25 -0
  21. package/src/runtime/agent/orchestrator/mcp/plugin-placeholders.mjs +54 -0
  22. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +108 -17
  23. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  24. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +19 -11
  25. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +9 -0
  26. package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +337 -0
  27. package/src/runtime/agent/orchestrator/providers/command-code-oauth-tokens.mjs +159 -0
  28. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +444 -0
  29. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  30. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -4
  31. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +9 -2
  32. package/src/runtime/agent/orchestrator/providers/gemini.mjs +41 -28
  33. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +7 -1
  34. package/src/runtime/agent/orchestrator/providers/model-proxy-bridge.mjs +1080 -0
  35. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +15 -0
  36. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +35 -0
  37. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -0
  38. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +6 -5
  39. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +6 -2
  41. package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +11 -0
  42. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +2 -1
  43. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +9 -3
  44. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +4 -4
  45. package/src/runtime/agent/orchestrator/providers/registry.mjs +35 -9
  46. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +3 -0
  47. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +15 -55
  48. package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +15 -2
  49. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +267 -84
  50. package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +4 -1
  51. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +21 -0
  52. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +54 -3
  53. package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +284 -0
  54. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +268 -0
  55. package/src/runtime/agent/orchestrator/session/catalog-db/sqlite-gate.mjs +27 -0
  56. package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +628 -0
  57. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +6 -2
  58. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +184 -0
  59. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +136 -0
  60. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +128 -0
  61. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +127 -0
  62. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +101 -0
  63. package/src/runtime/agent/orchestrator/session/explicit-skills.mjs +83 -5
  64. package/src/runtime/agent/orchestrator/session/imported-message.mjs +11 -0
  65. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +2 -1
  66. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -1
  67. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +3 -2
  68. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +6 -4
  69. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +4 -3
  70. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -0
  71. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -3
  72. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +3 -1
  73. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +20 -12
  74. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +9 -2
  75. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +4 -0
  76. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +17 -4
  77. package/src/runtime/agent/orchestrator/session/manager.mjs +0 -1
  78. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +4 -3
  79. package/src/runtime/agent/orchestrator/session/store/listing.mjs +6 -2
  80. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +6 -0
  81. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +817 -0
  82. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +7 -0
  83. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +19 -12
  84. package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +158 -0
  85. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +183 -374
  86. package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +3 -1
  87. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +21 -2
  89. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +38 -17
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +17 -1
  91. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -0
  92. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +2 -2
  93. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +6 -6
  94. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +173 -22
  95. package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +8 -4
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +5 -4
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +15 -2
  98. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +13 -10
  99. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +1 -1
  100. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +17 -12
  101. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +2 -2
  102. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +4 -4
  104. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/route.mjs +2 -2
  105. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
  106. package/src/runtime/agent/orchestrator/tools/patch/read-redirects.mjs +34 -30
  107. package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +7 -7
  108. package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +1 -1
  109. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -0
  110. package/src/runtime/attachments/store.d.mts +3 -0
  111. package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +1 -1
  112. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +16 -4
  113. package/src/runtime/channels/lib/worker-main.mjs +17 -14
  114. package/src/runtime/computer-bridge/linux-readiness.mjs +0 -2
  115. package/src/runtime/effort-judge/judge-client.mjs +56 -18
  116. package/src/runtime/memory/lib/memory.mjs +1 -1
  117. package/src/runtime/memory/lib/session-ingest.mjs +2 -2
  118. package/src/runtime/shared/atomic-file.mjs +25 -4
  119. package/src/runtime/shared/extension-scopes.mjs +5 -3
  120. package/src/runtime/shared/file-lock.mjs +37 -11
  121. package/src/runtime/shared/llm/anthropic-effort.mjs +14 -0
  122. package/src/runtime/shared/llm/model-catalog.mjs +0 -12
  123. package/src/runtime/shared/llm/provider-catalog-registry.mjs +6 -0
  124. package/src/runtime/shared/plugin-manifest.mjs +18 -9
  125. package/src/runtime/shared/pristine-execution-contract.json +9 -2
  126. package/src/runtime/shared/pristine-execution.mjs +28 -4
  127. package/src/runtime/shared/provider-accounts.mjs +124 -20
  128. package/src/runtime/shared/provider-api-key.mjs +12 -0
  129. package/src/runtime/shared/shell-display.mjs +9 -3
  130. package/src/runtime/shared/skill-mentions.d.mts +10 -0
  131. package/src/runtime/shared/skill-mentions.mjs +133 -0
  132. package/src/runtime/shared/skill-script-runtime.mjs +264 -0
  133. package/src/runtime/shared/tool-primitives.mjs +35 -2
  134. package/src/runtime/shared/turn-worktree-snapshot.mjs +25 -5
  135. package/src/runtime/shared/worker-requests.mjs +15 -2
  136. package/src/session-runtime/automation-agents.mjs +8 -2
  137. package/src/session-runtime/boot/apis.mjs +55 -0
  138. package/src/session-runtime/boot/config.mjs +5 -2
  139. package/src/session-runtime/boot/facade.mjs +1 -0
  140. package/src/session-runtime/boot/infrastructure.mjs +29 -18
  141. package/src/session-runtime/conversation-import/discovery.mjs +698 -0
  142. package/src/session-runtime/conversation-import/service.mjs +369 -0
  143. package/src/session-runtime/cwd-plugins/cwd-apply.mjs +10 -5
  144. package/src/session-runtime/cwd-plugins/plugin-status.mjs +47 -42
  145. package/src/session-runtime/mcp-glue/connect.mjs +57 -10
  146. package/src/session-runtime/mcp-glue/plugin-servers.mjs +499 -0
  147. package/src/session-runtime/mcp-glue/server-catalog.mjs +115 -30
  148. package/src/session-runtime/mcp-glue.mjs +22 -2
  149. package/src/session-runtime/new-session-config.mjs +3 -2
  150. package/src/session-runtime/notification-bus.mjs +9 -2
  151. package/src/session-runtime/provider-auth/accounts.mjs +33 -15
  152. package/src/session-runtime/provider-models/web-search-catalog.mjs +5 -1
  153. package/src/session-runtime/provider-readiness.mjs +16 -10
  154. package/src/session-runtime/resource-extension-sync.mjs +20 -5
  155. package/src/session-runtime/resource-mcp-api.mjs +101 -77
  156. package/src/session-runtime/resource-plugins-api.mjs +29 -41
  157. package/src/session-runtime/resource-scope-decorate.mjs +6 -8
  158. package/src/session-runtime/resource-skills-api.mjs +23 -2
  159. package/src/session-runtime/schedule-session-run.mjs +1 -1
  160. package/src/session-runtime/services/channel-session-router.mjs +4 -1
  161. package/src/session-runtime/services/hook-bus/command-handler.mjs +4 -1
  162. package/src/session-runtime/services/hook-bus/handlers.mjs +7 -3
  163. package/src/session-runtime/services/provider-admin.mjs +64 -18
  164. package/src/session-runtime/services/usage-dashboard-model.mjs +3 -0
  165. package/src/session-runtime/setup-tool/executor.mjs +12 -1
  166. package/src/session-runtime/setup-tool/extended-actions.mjs +1 -0
  167. package/src/session-runtime/setup-tool/settings-contract.mjs +1 -0
  168. package/src/session-runtime/setup-tool/tool-defs.mjs +7 -1
  169. package/src/session-runtime/skills-api.mjs +7 -3
  170. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  171. package/src/session-runtime/webhook-session-run.mjs +16 -4
  172. package/src/skill-script-entry.mjs +36 -0
  173. package/src/standalone/daemon-stored-session-views.mjs +11 -3
  174. package/src/standalone/daemon.mjs +41 -4
  175. package/src/standalone/session-protocol.mjs +5 -0
  176. package/src/standalone/session-service/session-calls/session-action.mjs +6 -1
  177. package/src/tui/App.jsx +9 -1
  178. package/src/tui/app/app-view/floating-panel.jsx +9 -1
  179. package/src/tui/app/app-view.jsx +6 -0
  180. package/src/tui/app/extension-pickers/plugins-pickers.mjs +35 -16
  181. package/src/tui/app/model-options.mjs +2 -0
  182. package/src/tui/app/skill-mention-palette.mjs +88 -0
  183. package/src/tui/app/slash-palette.mjs +23 -7
  184. package/src/tui/app/transcript-row-estimate.mjs +11 -3
  185. package/src/tui/app/use-prompt-draft-flow.mjs +7 -2
  186. package/src/tui/app/use-prompt-state.mjs +5 -0
  187. package/src/tui/components/Message.jsx +22 -1
  188. package/src/tui/components/PromptInput.jsx +21 -3
  189. package/src/tui/components/SlashCommandPalette.jsx +12 -3
  190. package/src/tui/components/TranscriptItem.jsx +9 -1
  191. package/src/tui/components/prompt-input/key-handler.mjs +21 -8
  192. package/src/tui/components/user-message-skills.mjs +38 -0
  193. package/src/tui/dist/index.mjs +507 -171
  194. package/src/tui/session/queue-helpers.mjs +7 -2
  195. package/src/tui/session/session-api/extensions.mjs +4 -2
  196. package/src/tui/session/session-api/settings.mjs +7 -0
  197. package/src/tui/session/session-flow/drain.mjs +14 -4
  198. package/src/tui/session/tui-steering-persist.mjs +25 -9
  199. package/src/tui/session/turn.mjs +17 -0
  200. package/src/runtime/effort-judge/previous-human-request.mjs +0 -162
  201. package/src/runtime/effort-judge/verified-context-input.mjs +0 -10
@@ -1,51 +1,25 @@
1
- // MCP server configuration: add/save/remove/toggle plus the plugin-declared
2
- // servers a plugin's manifest or script contributes.
3
- import { join } from 'node:path';
4
- import { existsSync } from 'node:fs';
1
+ // MCP server configuration: add/save/remove/toggle of the user's servers, and
2
+ // the per-server intent (enabled flag) of servers registered plugins declare.
3
+ // Plugin servers are derived live (mcp-glue/plugin-servers.mjs) and are never
4
+ // written into `mcpServers`; ordinary edits refuse them.
5
5
  import { clean } from '../runtime/agent/orchestrator/runtime-core/session-text.mjs';
6
6
  import {
7
- normalizePluginMcpServerConfig,
8
- pluginMcpServerName,
9
- pluginRawMcpServers,
10
- pluginMcpEnableScript,
11
- pluginServerMatcher,
12
- resolveContainedPluginPath,
13
7
  mergeMcpServerConfig,
14
8
  MCP_TIMEOUT_FIELD_GROUPS,
15
9
  } from '../runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs';
10
+ import { withAdoptedCopies, withPinnedCollisionNames, withPluginServerIntent } from './mcp-glue/plugin-servers.mjs';
11
+ import { findRegisteredPlugin } from './resource-plugins-api.mjs';
16
12
 
17
13
  /** The `mcpServers` map of a config as a fresh object (never the live one). */
18
14
  function mcpServersOf(config) {
19
15
  return config.mcpServers && typeof config.mcpServers === 'object' ? { ...config.mcpServers } : {};
20
16
  }
21
17
 
22
- function pluginMcpEnv(pluginDataDir, plugin, serverName) {
23
- return {
24
- MIXDOG_PLUGIN_ROOT: clean(plugin.root),
25
- MIXDOG_PLUGIN_DATA: join(pluginDataDir, 'plugins', 'data', clean(plugin.id || plugin.name || serverName)),
26
- };
27
- }
28
-
29
- // Manifest-declared servers replace every entry the plugin owned before; a
30
- // single server keeps the bare name.
31
- function manifestPluginMcpServers(existing, { rawServers, mcpRoot }, { plugin, serverName, env }) {
32
- const keys = Object.keys(rawServers).filter((k) => {
33
- const v = rawServers[k];
34
- return v !== null && typeof v === 'object' && !Array.isArray(v);
35
- });
36
- const owned = pluginServerMatcher(serverName);
37
- const nextServers = {};
38
- for (const [k, v] of Object.entries(existing || {})) {
39
- if (owned(k)) continue;
40
- nextServers[k] = v;
41
- }
42
- for (const serverKey of keys) {
43
- const cfg = normalizePluginMcpServerConfig(rawServers[serverKey], mcpRoot);
44
- cfg.env = { ...(cfg.env || {}), ...env };
45
- if (plugin.enabled === false) cfg._mixdogPluginDisabled = true;
46
- nextServers[keys.length === 1 ? serverName : `${serverName}--${serverKey}`] = cfg;
47
- }
48
- return nextServers;
18
+ function pluginOwnedError(name, owner) {
19
+ return new Error(
20
+ `MCP server "${name}" is provided by plugin "${owner.id}" (server "${owner.key}"); ` +
21
+ 'enable or disable it with set_mcp_enabled, or manage the plugin instead'
22
+ );
49
23
  }
50
24
 
51
25
  const transportType = (type) => (type === 'streamable-http' ? 'http' : type);
@@ -80,24 +54,53 @@ function mergedServerInput(input, existing, originalName) {
80
54
  export function createMcpResourceApi({ deps, sync, decorate }) {
81
55
  const {
82
56
  getConfig,
83
- cfgMod,
84
- STANDALONE_DATA_DIR,
85
57
  saveConfigAndAdopt,
86
58
  normalizeMcpServerInput,
87
59
  mcpStatus,
88
60
  getMcpServerConfig,
61
+ connectConfiguredMcp,
89
62
  reloadFullConfig,
90
63
  awaitKeychainPrewarm,
64
+ pluginsStatus,
65
+ resolveMcpCatalog = () => ({ adoption: null, plugins: {}, entries: {} }),
66
+ pluginMcpServer = () => null,
67
+ flushSkillsSave,
91
68
  } = deps;
92
69
 
93
- /** Persist a new `mcpServers` map; project overrides never survive a global edit. */
70
+ /** The config with any adoptable plugin server copies already adopted, so
71
+ * every write also persists the adoption and never re-adds a copy. */
72
+ const adoptedConfig = () => withAdoptedCopies(getConfig(), resolveMcpCatalog().adoption);
73
+
74
+ /** Persist a new `mcpServers` map; project overrides never survive a global
75
+ * edit. A plugin server pushed off its name by the edit keeps the collision
76
+ * name it gets, so it does not flip again later. */
94
77
  const saveServers = (servers) => {
95
- const nextConfig = { ...getConfig() };
78
+ const nextConfig = { ...adoptedConfig() };
96
79
  delete nextConfig.mcpProjectOverrides;
97
80
  saveConfigAndAdopt({ ...nextConfig, mcpServers: servers });
81
+ const saved = getConfig();
82
+ const pinned = withPinnedCollisionNames(saved, resolveMcpCatalog());
83
+ if (pinned !== saved) saveConfigAndAdopt(pinned);
84
+ };
85
+ const userServers = () => mcpServersOf(adoptedConfig());
86
+ const refusePluginServer = (name) => {
87
+ const owned = name ? pluginMcpServer(name) : null;
88
+ if (owned) throw pluginOwnedError(name, owned.owner);
89
+ };
90
+ // Peers reload config from disk on receipt: land the write before publishing.
91
+ const persisted = async () => {
92
+ await flushSkillsSave?.();
98
93
  };
99
94
 
100
95
  return {
96
+ /** One-time, idempotent: move server copies an earlier plugin enable wrote
97
+ * into `mcpServers` to per-server plugin intent. Returns adopted names. */
98
+ migratePluginMcpCopies() {
99
+ const { adoption } = resolveMcpCatalog();
100
+ if (!adoption) return [];
101
+ saveConfigAndAdopt(withAdoptedCopies(getConfig(), adoption));
102
+ return adoption.removed;
103
+ },
101
104
  mcpStatus() {
102
105
  return decorate.mcp(mcpStatus());
103
106
  },
@@ -114,26 +117,31 @@ export function createMcpResourceApi({ deps, sync, decorate }) {
114
117
  },
115
118
  async addMcpServer(input = {}) {
116
119
  const { name, config: serverConfig } = normalizeMcpServerInput(input);
117
- const servers = mcpServersOf(getConfig());
120
+ const servers = userServers();
118
121
  if (Object.hasOwn(servers, name)) throw new Error(`MCP server already exists: ${name}`);
122
+ const owned = pluginMcpServer(name);
123
+ if (owned) throw new Error(`MCP server already exists: ${name} (provided by plugin "${owned.owner.id}")`);
119
124
  saveServers({ ...servers, [name]: serverConfig });
120
125
  const status = await sync.reconnectMcpAndAnnounce({ only: name, enabled: serverConfig.enabled !== false });
121
126
  return { name, status };
122
127
  },
123
128
  async saveMcpServer(input = {}) {
124
- const servers = mcpServersOf(getConfig());
129
+ const servers = userServers();
125
130
  // An explicit originalName must exist; a bare name edits a matching
126
- // server and otherwise creates a new one.
131
+ // server and otherwise creates a new one. Plugin servers are never
132
+ // edited, renamed or shadowed here.
127
133
  const explicitOriginal = clean(input.originalName);
128
134
  if (explicitOriginal && !Object.hasOwn(servers, explicitOriginal)) {
135
+ refusePluginServer(explicitOriginal);
129
136
  throw new Error(`MCP server not configured: ${explicitOriginal}`);
130
137
  }
131
138
  const bareName = clean(input.name);
139
+ if (!explicitOriginal && !Object.hasOwn(servers, bareName)) refusePluginServer(bareName);
132
140
  const originalName = explicitOriginal || (Object.hasOwn(servers, bareName) ? bareName : '');
133
141
  const existing = originalName ? servers[originalName] : {};
134
142
  const normalizedInput = normalizeMcpServerInput(mergedServerInput(input, existing, originalName));
135
143
  const name = originalName && clean(input.name) === originalName ? originalName : normalizedInput.name;
136
- if (name !== originalName && Object.hasOwn(servers, name)) {
144
+ if (name !== originalName && (Object.hasOwn(servers, name) || pluginMcpServer(name))) {
137
145
  throw new Error(`MCP server already exists: ${name}`);
138
146
  }
139
147
  if (originalName && originalName !== name) delete servers[originalName];
@@ -149,8 +157,9 @@ export function createMcpResourceApi({ deps, sync, decorate }) {
149
157
  async removeMcpServer(name) {
150
158
  const serverName = clean(name);
151
159
  if (!serverName) throw new Error('MCP server name is required');
152
- const servers = mcpServersOf(getConfig());
160
+ const servers = userServers();
153
161
  if (!Object.hasOwn(servers, serverName)) {
162
+ refusePluginServer(serverName);
154
163
  throw new Error(`MCP server not configured: ${serverName}`);
155
164
  }
156
165
  delete servers[serverName];
@@ -161,9 +170,26 @@ export function createMcpResourceApi({ deps, sync, decorate }) {
161
170
  const serverName = clean(name);
162
171
  if (!serverName) throw new Error('MCP server name is required');
163
172
  const want = enabled !== false;
173
+ const owned = pluginMcpServer(serverName);
174
+ if (owned) {
175
+ // Server intent only: the effective state still requires the plugin.
176
+ // Only a choice that differs from the declared default is stored.
177
+ const { owner, pluginEnabled, declaredEnabled } = owned;
178
+ saveConfigAndAdopt(
179
+ withPluginServerIntent(adoptedConfig(), owner.id, owner.key, (intent) => ({
180
+ ...intent,
181
+ enabled: want === (declaredEnabled !== false) ? undefined : want,
182
+ }))
183
+ );
184
+ await persisted();
185
+ const effective = want && pluginEnabled;
186
+ const status = await sync.scheduleMcpToggle(serverName, effective);
187
+ await sync.publish('mcp', { mcpChanges: [{ name: serverName, enabled: effective }] });
188
+ return status;
189
+ }
164
190
  const shadowRow = mcpStatus().servers.find((s) => s.name === serverName);
165
191
  if (!shadowRow) throw new Error(`MCP server not configured: ${serverName}`);
166
- const servers = mcpServersOf(getConfig());
192
+ const servers = userServers();
167
193
  const current = servers[serverName];
168
194
  if (!current || typeof current !== 'object' || Array.isArray(current)) {
169
195
  throw new Error(`MCP server not configured: ${serverName}`);
@@ -174,38 +200,36 @@ export function createMcpResourceApi({ deps, sync, decorate }) {
174
200
  await sync.publish('mcp', { mcpChanges: [{ name: serverName, enabled: want }] });
175
201
  return status;
176
202
  },
203
+ /** Kept for older callers: plugin servers appear on their own once the
204
+ * plugin is registered and enabled. This clears any per-server "off"
205
+ * intent of the plugin's servers and reconnects them (here and on peers);
206
+ * a disabled plugin stays disabled. */
177
207
  async enablePluginMcp(plugin = {}) {
178
- const root = clean(plugin.root);
179
- const script = pluginMcpEnableScript(root, plugin);
180
- if (!root || !script) throw new Error('plugin has no MCP script');
181
- const serverName = pluginMcpServerName(plugin);
182
- const nextConfig = { ...getConfig() };
183
- const manifestMcp = pluginRawMcpServers(root, script);
184
- const env = pluginMcpEnv(cfgMod.getPluginData?.() || STANDALONE_DATA_DIR, plugin, serverName);
185
- if (manifestMcp) {
186
- nextConfig.mcpServers = manifestPluginMcpServers(nextConfig.mcpServers, manifestMcp, {
187
- plugin,
188
- serverName,
189
- env,
190
- });
191
- } else {
192
- const scriptPath = resolveContainedPluginPath(root, script);
193
- if (!scriptPath || !existsSync(scriptPath))
194
- throw new Error(`plugin MCP script not found: ${join(root, script)}`);
195
- nextConfig.mcpServers = {
196
- ...(nextConfig.mcpServers || {}),
197
- [serverName]: {
198
- command: 'node',
199
- args: [scriptPath],
200
- cwd: root,
201
- env,
202
- ...(plugin.enabled === false ? { _mixdogPluginDisabled: true } : {}),
203
- },
204
- };
208
+ const registered = findRegisteredPlugin(pluginsStatus, plugin) || plugin;
209
+ const id = clean(registered.id || registered.name);
210
+ const catalog = resolveMcpCatalog();
211
+ const row = catalog.plugins[id];
212
+ if (!row) throw new Error(`plugin not registered: ${clean(plugin.id || plugin.name || plugin) || id}`);
213
+ if (!row.servers.length) {
214
+ const detail = row.errors.map((error) => error.message).join('; ');
215
+ throw new Error(`plugin declares no MCP servers${detail ? `: ${detail}` : ''}`);
216
+ }
217
+ let next = withAdoptedCopies(getConfig(), catalog.adoption);
218
+ for (const name of row.servers) {
219
+ const { owner, declaredEnabled } = catalog.entries[name];
220
+ next = withPluginServerIntent(next, id, owner.key, (intent) => ({
221
+ ...intent,
222
+ enabled: declaredEnabled === false ? true : undefined,
223
+ }));
205
224
  }
206
- saveConfigAndAdopt(nextConfig);
207
- const status = await sync.reconnectMcpAndAnnounce();
208
- return { serverName, status };
225
+ saveConfigAndAdopt(next);
226
+ await persisted();
227
+ const mcpChanges = row.servers.map((name) => ({ name, enabled: row.enabled }));
228
+ let status;
229
+ for (const change of mcpChanges)
230
+ status = await connectConfiguredMcp({ only: change.name, enabled: change.enabled });
231
+ await sync.announce('mcp', { detail: { mcpChanges } });
232
+ return { serverName: row.servers[0], serverNames: row.servers, status };
209
233
  },
210
234
  };
211
235
  }
@@ -1,4 +1,6 @@
1
- // Plugin registry edits and the MCP entries a plugin owns.
1
+ // Plugin registry edits. A plugin's MCP servers are derived from the registry
2
+ // (mcp-glue/plugin-servers.mjs), so each edit reconciles this runtime's MCP
3
+ // connections and then lets peer runtimes reconcile theirs.
2
4
  import { clean } from '../runtime/agent/orchestrator/runtime-core/session-text.mjs';
3
5
  import {
4
6
  addPlugin as registryAddPlugin,
@@ -6,7 +8,7 @@ import {
6
8
  setPluginEnabled as registrySetPluginEnabled,
7
9
  updatePlugin as registryUpdatePlugin,
8
10
  } from './services/plugin-admin.mjs';
9
- import { pluginMcpServerName, pluginServerMatcher } from '../runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs';
11
+ import { withAdoptedCopies, withoutPluginIntents, withPinnedCollisionNames } from './mcp-glue/plugin-servers.mjs';
10
12
 
11
13
  const pluginKey = (plugin) => clean(plugin.id || plugin.name || plugin);
12
14
 
@@ -24,22 +26,22 @@ export function createPluginsResourceApi({ deps, sync, decorate }) {
24
26
  getConfig,
25
27
  cfgMod,
26
28
  saveConfigAndAdopt,
27
- connectConfiguredMcp,
28
- invalidatePreSessionToolSurface,
29
29
  pluginsStatus,
30
+ resolveMcpCatalog = () => ({ adoption: null }),
31
+ flushSkillsSave,
30
32
  } = deps;
31
33
  const pluginData = () => cfgMod.getPluginData?.();
32
34
 
33
- /** Rewrite the plugin's MCP entries through `rewrite`; when any existed,
34
- * persist and reconnect so the connection registry matches the config. */
35
- async function rewritePluginServers(plugin, rewrite) {
36
- const nextConfig = { ...getConfig() };
37
- const owned = pluginServerMatcher(pluginMcpServerName(plugin));
38
- const current = nextConfig.mcpServers || {};
39
- if (!Object.keys(current).some(owned)) return;
40
- saveConfigAndAdopt({ ...nextConfig, mcpServers: rewrite(current, owned) });
41
- await connectConfiguredMcp({ reset: true });
42
- invalidatePreSessionToolSurface();
35
+ /** Persist any adoptable copies of plugin servers while the registry still
36
+ * knows their plugin (a removal must not leave them stranded), and keep any
37
+ * collision name a plugin server was just given. */
38
+ async function adoptPluginServerCopies(transform = (config) => config) {
39
+ const current = getConfig();
40
+ const catalog = resolveMcpCatalog();
41
+ const next = transform(withPinnedCollisionNames(withAdoptedCopies(current, catalog.adoption), catalog));
42
+ if (next === current) return;
43
+ saveConfigAndAdopt(next);
44
+ await flushSkillsSave?.();
43
45
  }
44
46
 
45
47
  return {
@@ -47,49 +49,35 @@ export function createPluginsResourceApi({ deps, sync, decorate }) {
47
49
  return decorate.plugins(pluginsStatus());
48
50
  },
49
51
  async reloadPlugins() {
50
- await sync.announce('plugins');
52
+ await sync.reconcilePluginsAndAnnounce();
51
53
  return pluginsStatus();
52
54
  },
53
55
  async addPlugin(source) {
54
56
  const plugin = registryAddPlugin(source, { dataDir: pluginData() });
55
- await sync.announce('plugins');
57
+ await adoptPluginServerCopies();
58
+ await sync.reconcilePluginsAndAnnounce();
56
59
  return { plugin, status: pluginsStatus() };
57
60
  },
58
61
  async updatePlugin(plugin = {}) {
59
62
  const updated = registryUpdatePlugin(pluginKey(plugin), { dataDir: pluginData() });
60
- await sync.announce('plugins');
63
+ await adoptPluginServerCopies();
64
+ await sync.reconcilePluginsAndAnnounce();
61
65
  return { plugin: updated, status: pluginsStatus() };
62
66
  },
63
67
  async setPluginEnabled(plugin = {}, enabled = true) {
64
- const owner = findRegisteredPlugin(pluginsStatus, plugin) || plugin;
68
+ await adoptPluginServerCopies();
65
69
  const updated = registrySetPluginEnabled(pluginKey(plugin), enabled, { dataDir: pluginData() });
66
- await rewritePluginServers(owner, (servers, owned) => {
67
- const next = {};
68
- for (const [name, value] of Object.entries(servers)) {
69
- const config = value && typeof value === 'object' && !Array.isArray(value) ? { ...value } : value;
70
- if (owned(name) && config && typeof config === 'object' && !Array.isArray(config)) {
71
- if (enabled === false) config._mixdogPluginDisabled = true;
72
- else delete config._mixdogPluginDisabled;
73
- }
74
- next[name] = config;
75
- }
76
- return next;
77
- });
78
- await sync.announce('plugins');
70
+ await sync.reconcilePluginsAndAnnounce();
79
71
  return { plugin: updated, status: pluginsStatus() };
80
72
  },
81
73
  async removePlugin(plugin = {}) {
82
- // Resolve before the registry forgets the plugin and its manifest name.
83
- const owner = findRegisteredPlugin(pluginsStatus, plugin) || plugin;
74
+ const owner = findRegisteredPlugin(pluginsStatus, plugin);
75
+ const ownerId = clean(owner?.id);
76
+ // Adopt first (the registry still knows the plugin), then drop the
77
+ // plugin's per-server intent together with the plugin.
78
+ await adoptPluginServerCopies((config) => (ownerId ? withoutPluginIntents(config, ownerId) : config));
84
79
  const removed = registryRemovePlugin(pluginKey(plugin), { dataDir: pluginData() });
85
- await rewritePluginServers(owner, (servers, owned) => {
86
- const next = { ...servers };
87
- for (const name of Object.keys(next)) {
88
- if (owned(name)) delete next[name];
89
- }
90
- return next;
91
- });
92
- await sync.announce('plugins');
80
+ await sync.reconcilePluginsAndAnnounce();
93
81
  return { plugin: removed, status: pluginsStatus() };
94
82
  },
95
83
  };
@@ -7,19 +7,17 @@ import {
7
7
  cwdWithinProjects,
8
8
  extensionScopeProjects,
9
9
  extensionScopesFromConfig,
10
- pluginIdForMcpServer,
11
10
  } from '../runtime/agent/orchestrator/runtime-core/extension-scopes.mjs';
12
11
 
13
12
  function listOf(status, key) {
14
13
  return Array.isArray(status?.[key]) ? status[key] : [];
15
14
  }
16
15
 
17
- export function createScopeDecorators({ getConfig, getCurrentCwd, pluginsStatus }) {
18
- function scopeInfo(kind, name, pluginId = '') {
16
+ export function createScopeDecorators({ getConfig, getCurrentCwd }) {
17
+ function scopeInfo(kind, name, pluginId = '', cwd = getCurrentCwd()) {
19
18
  const scopes = extensionScopesFromConfig(getConfig());
20
19
  const scope = extensionScopeProjects(scopes, kind, name);
21
20
  const inheritedScope = pluginId ? extensionScopeProjects(scopes, 'plugins', pluginId) : null;
22
- const cwd = getCurrentCwd();
23
21
  const activeHere =
24
22
  (scope ? cwdWithinProjects(cwd, scope) : true) &&
25
23
  (inheritedScope ? cwdWithinProjects(cwd, inheritedScope) : true);
@@ -27,21 +25,21 @@ export function createScopeDecorators({ getConfig, getCurrentCwd, pluginsStatus
27
25
  }
28
26
  return {
29
27
  mcp(status) {
30
- const plugins = pluginsStatus()?.plugins || [];
31
28
  return {
32
29
  ...status,
33
30
  servers: listOf(status, 'servers').map((server) => ({
34
31
  ...server,
35
- ...scopeInfo('mcp', server.name, pluginIdForMcpServer(server.name, plugins)),
32
+ ...scopeInfo('mcp', server.name, server?.owner?.kind === 'plugin' ? clean(server.owner.id) : ''),
36
33
  })),
37
34
  };
38
35
  },
39
- skills(status) {
36
+ /** `cwd`: decide `activeHere` for that folder instead of the current one. */
37
+ skills(status, cwd = getCurrentCwd()) {
40
38
  return {
41
39
  ...status,
42
40
  skills: listOf(status, 'skills').map((skill) => ({
43
41
  ...skill,
44
- ...scopeInfo('skills', skill.name, skill?.owner?.kind === 'plugin' ? clean(skill.owner.id) : ''),
42
+ ...scopeInfo('skills', skill.name, skill?.owner?.kind === 'plugin' ? clean(skill.owner.id) : '', cwd),
45
43
  })),
46
44
  };
47
45
  },
@@ -1,4 +1,23 @@
1
1
  // Global Skill documents and the disabled-skill list.
2
+ import { statSync } from 'node:fs';
3
+ import { isAbsolute, resolve } from 'node:path';
4
+
5
+ /** `skillsStatus({ cwd })`: the folder to list and scope skills for, in place
6
+ * of the session's own. Only an existing absolute directory; it reads the
7
+ * same skill roots a session opened there would, nothing wider. */
8
+ function statusCwd(options) {
9
+ if (options == null) return '';
10
+ const cwd = options?.cwd;
11
+ if (typeof cwd !== 'string' || !cwd || cwd.includes('\0') || !isAbsolute(cwd)) {
12
+ throw new TypeError('skillsStatus cwd must be an absolute path.');
13
+ }
14
+ const folder = resolve(cwd);
15
+ if (!statSync(folder, { throwIfNoEntry: false })?.isDirectory()) {
16
+ throw new TypeError('skillsStatus cwd is not a directory.');
17
+ }
18
+ return folder;
19
+ }
20
+
2
21
  export function createSkillsResourceApi({ deps, sync, decorate }) {
3
22
  const {
4
23
  skillsStatus,
@@ -11,8 +30,10 @@ export function createSkillsResourceApi({ deps, sync, decorate }) {
11
30
  flushSkillsSave,
12
31
  } = deps;
13
32
  return {
14
- skillsStatus() {
15
- return decorate.skills(skillsStatus());
33
+ skillsStatus(options) {
34
+ const cwd = statusCwd(options);
35
+ if (!cwd) return decorate.skills(skillsStatus());
36
+ return decorate.skills(skillsStatus({ cwd }), cwd);
16
37
  },
17
38
  skillContent(name) {
18
39
  return skillContent(name);
@@ -65,7 +65,7 @@ export async function runScheduleSession(schedule, { config = null, prompt: prom
65
65
  const result = await runAutomationTurns(
66
66
  session.id,
67
67
  () => askSession(session.id, content, null, null, cwd || undefined),
68
- (completions) => askSession(session.id, completions, null, null, cwd || undefined)
68
+ (completions, askOpts) => askSession(session.id, completions, null, null, cwd || undefined, undefined, askOpts)
69
69
  );
70
70
  return { sessionId: session.id, result: String(result?.content || '') };
71
71
  }
@@ -19,7 +19,10 @@ export function createChannelSessionRouter({ getSessionService, getSessionId, lo
19
19
  service.submitSession({
20
20
  sessionId,
21
21
  prompt: content,
22
- options: { source: 'channel' },
22
+ // A channel notification (webhook event rules carry fenced external
23
+ // values) is queued as a task notification, never as typed input, so
24
+ // nothing inside it selects a skill.
25
+ options: { source: 'channel', mode: 'task-notification' },
23
26
  })
24
27
  ).catch((error) => {
25
28
  log(`channel session delivery failed session=${sessionId}: ${error?.message || error}`);
@@ -3,6 +3,7 @@ import { spawn, spawnSync } from 'node:child_process';
3
3
  import { MAX_BUFFER_BYTES } from './constants.mjs';
4
4
  import { handlerTimeoutS } from './handler-timeout.mjs';
5
5
  import { abortReason, throwIfAborted } from '../../../runtime/shared/abort-race.mjs';
6
+ import { scrubRuntimeRootVars } from '../../../runtime/agent/orchestrator/tools/env-scrub.mjs';
6
7
 
7
8
  function resolvePlaceholders(str, projectDir, pluginData, pluginRoot = null) {
8
9
  if (typeof str !== 'string') return str;
@@ -88,8 +89,10 @@ function hookEnv(projectDir, pluginData, payload, pluginRoot = null) {
88
89
  const resolvedProject = projectDir || process.cwd();
89
90
  const resolvedPluginRoot = pluginRoot || resolvedProject;
90
91
  const resolvedPluginData = pluginData || resolvedProject;
92
+ // Hooks are user commands like model shells: the host's runtime root,
93
+ // identity and Electron node-mode must not reach them.
91
94
  const env = {
92
- ...process.env,
95
+ ...scrubRuntimeRootVars({ ...process.env }),
93
96
  MIXDOG_PROJECT_DIR: resolvedProject,
94
97
  MIXDOG_PLUGIN_ROOT: resolvedPluginRoot,
95
98
  MIXDOG_PLUGIN_DATA: resolvedPluginData,
@@ -1,6 +1,7 @@
1
1
  import { readResponseBuffer } from '../../../runtime/shared/bounded-download.mjs';
2
2
  import { runAbortable, throwIfAborted } from '../../../runtime/shared/abort-race.mjs';
3
3
  import { assertPublicUrl, pinnedFetch } from '../../../runtime/web-search/lib/ssrf-guard.mjs';
4
+ import { providerSafeMcpToolName } from '../../../runtime/agent/orchestrator/mcp/client-config.mjs';
4
5
  import { handlerTimeoutS } from './handler-timeout.mjs';
5
6
  export { handlerTimeoutS } from './handler-timeout.mjs';
6
7
  export { defaultShellKind, runCommandHandler } from './command-handler.mjs';
@@ -216,8 +217,9 @@ export async function runMcpToolHandler(handler, payload, eventName, mcpToolRunn
216
217
  throwIfAborted(signal);
217
218
  const timeoutMs = Math.round(handlerTimeoutS(handler, eventName) * 1000);
218
219
  let name = String(handler.tool || '').trim();
219
- if (handler.server && !name.startsWith('mcp__')) {
220
- name = `mcp__${String(handler.server).trim()}__${name}`;
220
+ if (handler.server && name && !name.startsWith('mcp__')) {
221
+ // The same provider-safe name the server's tool was registered under.
222
+ name = providerSafeMcpToolName(String(handler.server).trim(), name);
221
223
  }
222
224
  if (!name) {
223
225
  return {
@@ -241,7 +243,9 @@ export async function runMcpToolHandler(handler, payload, eventName, mcpToolRunn
241
243
  onTimeout: () => {
242
244
  try {
243
245
  controller.abort(new Error(`mcp_tool hook timed out: ${name}`));
244
- } catch {}
246
+ } catch {
247
+ // Abort is best-effort; the timeout error below is what callers see.
248
+ }
245
249
  return new Error(`mcp_tool hook timed out: ${name}`);
246
250
  },
247
251
  }