@aiwg/cli 2026.10.1 → 2026.10.4

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 (361) hide show
  1. package/README.md +2 -2
  2. package/agentic/code/providers/capability-matrix.yaml +270 -0
  3. package/agentic/code/providers/model-capabilities.v1.json +3183 -24
  4. package/bin/aiwg.mjs +135 -47
  5. package/dist/src/a2a/codecs.js +9 -0
  6. package/dist/src/agents/grounding/knowledge-base.js +54 -6
  7. package/dist/src/artifacts/browser-export.js +6 -0
  8. package/dist/src/artifacts/checksum-manifest.js +1 -0
  9. package/dist/src/artifacts/citation-parser.js +173 -92
  10. package/dist/src/artifacts/citation-table-schema.js +59 -0
  11. package/dist/src/artifacts/cli.js +57 -18
  12. package/dist/src/artifacts/corpus-tools/snapshot.js +1 -1
  13. package/dist/src/artifacts/corpus-tools/vision-extract.js +107 -15
  14. package/dist/src/artifacts/corpus-views/ref-parser.js +3 -1
  15. package/dist/src/artifacts/corpus-views/renderers.js +3 -3
  16. package/dist/src/artifacts/discovery-eval.js +6 -4
  17. package/dist/src/artifacts/fortemi-core-query-adapter.js +36 -3
  18. package/dist/src/artifacts/fortemi-core-sync.js +166 -40
  19. package/dist/src/artifacts/framework-capabilities.js +171 -0
  20. package/dist/src/artifacts/framework-index-paths.js +73 -0
  21. package/dist/src/artifacts/framework-source-digest.js +75 -0
  22. package/dist/src/artifacts/index-builder.js +285 -131
  23. package/dist/src/artifacts/index-files.js +132 -18
  24. package/dist/src/artifacts/index-preparation.js +65 -0
  25. package/dist/src/artifacts/index-refresh.js +576 -0
  26. package/dist/src/artifacts/index-worker.mjs +9 -0
  27. package/dist/src/artifacts/prebuilt-build-lock.js +47 -4
  28. package/dist/src/artifacts/prebuilt-deployment.js +46 -0
  29. package/dist/src/artifacts/prebuilt-relations.js +80 -0
  30. package/dist/src/artifacts/query-engine.js +410 -88
  31. package/dist/src/artifacts/relations-builder.js +168 -0
  32. package/dist/src/artifacts/relations.js +148 -0
  33. package/dist/src/artifacts/repair.js +9 -4
  34. package/dist/src/artifacts/stats.js +5 -2
  35. package/dist/src/artifacts/types.js +30 -10
  36. package/dist/src/artifacts/watcher.js +1 -1
  37. package/dist/src/auth/credential-store.js +2 -1
  38. package/dist/src/catalog/cli.d.mts +5 -0
  39. package/dist/src/channel/manager.d.mts +20 -0
  40. package/dist/src/channel/manager.mjs +78 -12
  41. package/dist/src/cli/child-process-result.js +40 -0
  42. package/dist/src/cli/command-log.js +2 -3
  43. package/dist/src/cli/env.js +38 -16
  44. package/dist/src/cli/flag-parsing.js +236 -0
  45. package/dist/src/cli/handlers/artifacts.js +3 -0
  46. package/dist/src/cli/handlers/auth.js +3 -0
  47. package/dist/src/cli/handlers/decision.js +218 -5
  48. package/dist/src/cli/handlers/diagnose.js +13 -5
  49. package/dist/src/cli/handlers/effect.js +4 -2
  50. package/dist/src/cli/handlers/evidence.js +3 -0
  51. package/dist/src/cli/handlers/feedback.js +14 -5
  52. package/dist/src/cli/handlers/help.js +1 -0
  53. package/dist/src/cli/handlers/index.js +3 -1
  54. package/dist/src/cli/handlers/init.js +248 -30
  55. package/dist/src/cli/handlers/lint.js +5 -0
  56. package/dist/src/cli/handlers/mc.js +23 -0
  57. package/dist/src/cli/handlers/models.js +169 -10
  58. package/dist/src/cli/handlers/ralph-launcher.js +2 -0
  59. package/dist/src/cli/handlers/ralph.js +3 -0
  60. package/dist/src/cli/handlers/refresh.js +28 -0
  61. package/dist/src/cli/handlers/regenerate.js +14 -0
  62. package/dist/src/cli/handlers/resource-versions.js +3 -0
  63. package/dist/src/cli/handlers/rules.js +195 -0
  64. package/dist/src/cli/handlers/run.js +4 -0
  65. package/dist/src/cli/handlers/runtime-info.js +25 -11
  66. package/dist/src/cli/handlers/script-runner.js +19 -8
  67. package/dist/src/cli/handlers/session.js +406 -62
  68. package/dist/src/cli/handlers/sessions.js +243 -76
  69. package/dist/src/cli/handlers/setup-manifest.js +474 -79
  70. package/dist/src/cli/handlers/setup.js +180 -39
  71. package/dist/src/cli/handlers/steward.js +11 -0
  72. package/dist/src/cli/handlers/subcommands.js +60 -3
  73. package/dist/src/cli/handlers/team.js +18 -8
  74. package/dist/src/cli/handlers/uhp.js +3 -0
  75. package/dist/src/cli/handlers/use.js +307 -55
  76. package/dist/src/cli/handlers/utilities.js +125 -26
  77. package/dist/src/cli/handlers/version.js +8 -0
  78. package/dist/src/cli/prompt-utils.js +72 -0
  79. package/dist/src/cli/provider-resolution.js +11 -0
  80. package/dist/src/cli/router.js +15 -1
  81. package/dist/src/cli/services/deployment-verification.js +19 -9
  82. package/dist/src/cli/services/provider-prepare-coordinator.js +24 -0
  83. package/dist/src/cli/skill-usage.js +2 -3
  84. package/dist/src/config/aiwg-config.js +349 -35
  85. package/dist/src/config/cli.js +256 -23
  86. package/dist/src/config/decision-config.js +106 -0
  87. package/dist/src/config/dispatch-thresholds-runtime.d.mts +8 -0
  88. package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
  89. package/dist/src/config/gitignore.js +50 -0
  90. package/dist/src/config/index-graph-overrides.js +36 -0
  91. package/dist/src/config/project-artifacts-health.d.mts +37 -0
  92. package/dist/src/config/project-artifacts-health.mjs +14 -3
  93. package/dist/src/config/project-artifacts-runtime.d.mts +16 -0
  94. package/dist/src/config/user-config-dir.d.mts +9 -0
  95. package/dist/src/config/user-config.js +38 -11
  96. package/dist/src/config/user-registry.js +23 -15
  97. package/dist/src/config/user-state.js +7 -0
  98. package/dist/src/decision/adapters/jev.js +75 -27
  99. package/dist/src/decision/ask.js +203 -43
  100. package/dist/src/decision/context-live-qualification.js +2 -2
  101. package/dist/src/decision/driver.js +14 -7
  102. package/dist/src/decision/ensemble/aggregate.js +23 -10
  103. package/dist/src/decision/ensemble-study/multifact-study.js +6 -2
  104. package/dist/src/decision/entry.js +25 -6
  105. package/dist/src/decision/graph-live-qualification.js +1 -1
  106. package/dist/src/decision/graph-run.js +9 -0
  107. package/dist/src/decision/heldout/collector.js +15 -14
  108. package/dist/src/decision/heldout/contract.js +30 -7
  109. package/dist/src/decision/index.js +1 -0
  110. package/dist/src/decision/interop.js +60 -8
  111. package/dist/src/decision/jev-credentials.js +15 -1
  112. package/dist/src/decision/ledger.js +942 -0
  113. package/dist/src/decision/offload-activation.js +137 -0
  114. package/dist/src/decision/offload.js +550 -0
  115. package/dist/src/decision/qualification/load.js +4 -6
  116. package/dist/src/decision/savings.js +28 -0
  117. package/dist/src/decision/validate.js +6 -3
  118. package/dist/src/effects/dispatch-verifier.js +252 -0
  119. package/dist/src/effects/index.js +1 -0
  120. package/dist/src/effects/types.js +5 -1
  121. package/dist/src/effects/verifiers/git.js +2 -1
  122. package/dist/src/effects/verifiers/tracker.js +26 -14
  123. package/dist/src/extensions/commands/definitions.js +19 -0
  124. package/dist/src/extensions/project-local-remove.js +36 -1
  125. package/dist/src/features/installer.js +2 -2
  126. package/dist/src/flow/graph-metadata.js +5 -0
  127. package/dist/src/gates/providers/loader.js +64 -18
  128. package/dist/src/installation/manager-command.d.mts +18 -0
  129. package/dist/src/installation/manager-command.mjs +15 -3
  130. package/dist/src/installation/manager.d.mts +49 -0
  131. package/dist/src/installation/manager.mjs +95 -5
  132. package/dist/src/issues/cli.js +4 -4
  133. package/dist/src/issues/local.js +24 -5
  134. package/dist/src/jobs/runner.js +1 -1
  135. package/dist/src/lint/cli.js +27 -1
  136. package/dist/src/lint/loader.js +2 -1
  137. package/dist/src/lint/reporters.js +25 -1
  138. package/dist/src/lint/rule-activation.js +43 -0
  139. package/dist/src/lint/runner.js +137 -62
  140. package/dist/src/mcp/adapters/codex-runtime.js +366 -56
  141. package/dist/src/mcp/claude-install.d.mts +4 -0
  142. package/dist/src/mcp/claude-install.mjs +41 -0
  143. package/dist/src/mcp/cli.d.mts +1 -0
  144. package/dist/src/mcp/cli.mjs +620 -85
  145. package/dist/src/mcp/config-file.d.mts +28 -0
  146. package/dist/src/mcp/config-file.mjs +279 -0
  147. package/dist/src/mcp/config-layers.d.mts +36 -0
  148. package/dist/src/mcp/config-layers.mjs +348 -0
  149. package/dist/src/mcp/credentials.d.mts +51 -0
  150. package/dist/src/mcp/credentials.mjs +484 -0
  151. package/dist/src/mcp/grok-build-config.d.mts +6 -0
  152. package/dist/src/mcp/grok-build-config.mjs +17 -27
  153. package/dist/src/mcp/helpers.mjs +109 -4
  154. package/dist/src/mcp/muse-native-config.d.mts +13 -0
  155. package/dist/src/mcp/muse-native-config.mjs +1 -0
  156. package/dist/src/mcp/omp-config.d.mts +4 -0
  157. package/dist/src/mcp/omp-config.mjs +50 -29
  158. package/dist/src/mcp/profiles.js +97 -23
  159. package/dist/src/mcp/profiles.mjs +99 -27
  160. package/dist/src/mcp/registry.js +248 -131
  161. package/dist/src/mcp/registry.mjs +169 -100
  162. package/dist/src/mcp/server-config.d.mts +3 -0
  163. package/dist/src/mcp/server-config.mjs +89 -0
  164. package/dist/src/mcp/server.mjs +127 -82
  165. package/dist/src/mcp/test-simple.mjs +2 -1
  166. package/dist/src/mcp/toml-editor.d.mts +4 -0
  167. package/dist/src/mcp/toml-string.d.mts +1 -0
  168. package/dist/src/mcp/toml-string.mjs +9 -0
  169. package/dist/src/mcp/toml-strip-mcp.d.mts +1 -0
  170. package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
  171. package/dist/src/mcp/tool-filters.d.mts +46 -0
  172. package/dist/src/mcp/tool-filters.mjs +357 -0
  173. package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
  174. package/dist/src/mcp/tools/decision.mjs +22 -3
  175. package/dist/src/mcp/tools/discovery.mjs +62 -55
  176. package/dist/src/mcp/tools/orchestration.mjs +50 -2
  177. package/dist/src/mcp/tools/subsystems.mjs +39 -8
  178. package/dist/src/mcp/tools/worker-invoke.mjs +2856 -0
  179. package/dist/src/mcp/tools/worker-shell-control.mjs +289 -0
  180. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
  181. package/dist/src/memory/project-registry.js +2 -2
  182. package/dist/src/mission-protocol/codecs.js +647 -1
  183. package/dist/src/mission-protocol/types.js +110 -0
  184. package/dist/src/mission-protocol/worker-status.js +2 -0
  185. package/dist/src/models/config-loader.js +76 -14
  186. package/dist/src/models/grok-build-models.js +93 -41
  187. package/dist/src/models/model-capabilities.v1.json +3183 -24
  188. package/dist/src/models/model-discovery.js +931 -67
  189. package/dist/src/models/provider-policy.js +719 -11
  190. package/dist/src/models/qualification-digest.d.mts +2 -0
  191. package/dist/src/models/qualification-digest.mjs +17 -0
  192. package/dist/src/models/resolver.js +261 -0
  193. package/dist/src/models/worker-direct-readiness.js +550 -0
  194. package/dist/src/models/worker-dispatch-offer.js +169 -0
  195. package/dist/src/models/worker-health-readiness.js +144 -0
  196. package/dist/src/models/worker-installation.d.mts +39 -0
  197. package/dist/src/models/worker-installation.mjs +986 -0
  198. package/dist/src/models/wrapper-route.js +18 -0
  199. package/dist/src/ops/registry.js +9 -14
  200. package/dist/src/plugins/standalone-packager.js +4 -3
  201. package/dist/src/process/bounded-child-process.d.mts +84 -0
  202. package/dist/src/process/bounded-child-process.mjs +430 -0
  203. package/dist/src/process/cli-command.d.mts +16 -0
  204. package/dist/src/process/cli-command.mjs +75 -0
  205. package/dist/src/process/signal-child.d.mts +6 -0
  206. package/dist/src/process/signal-child.mjs +7 -0
  207. package/dist/src/providers/capability-matrix.js +8 -0
  208. package/dist/src/providers/capability-matrix.yaml +270 -0
  209. package/dist/src/providers/deployed-skill-roots.js +20 -0
  210. package/dist/src/providers/grok-build-acp.js +3 -2
  211. package/dist/src/providers/grok-build-headless.js +53 -23
  212. package/dist/src/providers/grok-build-inspect.js +1 -0
  213. package/dist/src/providers/omp-agent.d.mts +8 -0
  214. package/dist/src/providers/omp-diagnostics.d.mts +1 -0
  215. package/dist/src/providers/omp-paths.d.mts +13 -0
  216. package/dist/src/providers/provider-definitions.js +9 -1
  217. package/dist/src/providers/provider-definitions.mjs +9 -2
  218. package/dist/src/providers/skill-ownership.d.mts +9 -0
  219. package/dist/src/providers/skill-ownership.mjs +88 -0
  220. package/dist/src/rules/activation.d.mts +21 -0
  221. package/dist/src/rules/activation.mjs +62 -0
  222. package/dist/src/rules/inline-budget.d.mts +26 -0
  223. package/dist/src/rules/inline-budget.mjs +66 -0
  224. package/dist/src/rules/inventory.js +129 -0
  225. package/dist/src/rules/pin-state.js +73 -0
  226. package/dist/src/rules/pins.d.mts +14 -0
  227. package/dist/src/rules/pins.mjs +112 -0
  228. package/dist/src/rules/rule-frontmatter.schema.json +197 -0
  229. package/dist/src/serve/executor-registry.js +16 -24
  230. package/dist/src/serve/fleet-mission-conductor.js +9 -0
  231. package/dist/src/serve/mission-conductor.js +47 -12
  232. package/dist/src/serve/shared-host-scheduler.js +2 -0
  233. package/dist/src/serve/worker-invocation.js +558 -0
  234. package/dist/src/serve/worker-stdin-frame.d.mts +10 -0
  235. package/dist/src/serve/worker-stdin-frame.mjs +107 -0
  236. package/dist/src/sessions/adapter-registry.js +49 -0
  237. package/dist/src/sessions/adapters/claude.js +39 -54
  238. package/dist/src/sessions/adapters/codex.js +82 -67
  239. package/dist/src/sessions/adapters/copilot.js +4 -4
  240. package/dist/src/sessions/adapters/cursor.js +40 -57
  241. package/dist/src/sessions/adapters/deepseek-harness.js +15 -12
  242. package/dist/src/sessions/adapters/factory.js +31 -27
  243. package/dist/src/sessions/adapters/generic.js +43 -9
  244. package/dist/src/sessions/adapters/grok-build.js +11 -11
  245. package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
  246. package/dist/src/sessions/adapters/grokbot.js +434 -24
  247. package/dist/src/sessions/adapters/hermes.js +27 -29
  248. package/dist/src/sessions/adapters/muse-workspace-scan.js +128 -0
  249. package/dist/src/sessions/adapters/muse.js +200 -104
  250. package/dist/src/sessions/adapters/omp.js +28 -14
  251. package/dist/src/sessions/adapters/openclaw.js +89 -79
  252. package/dist/src/sessions/adapters/opencode.js +27 -24
  253. package/dist/src/sessions/adapters/openhuman.js +18 -60
  254. package/dist/src/sessions/adapters/pi.js +30 -14
  255. package/dist/src/sessions/adapters/warp.js +3 -3
  256. package/dist/src/sessions/adapters/windsurf.js +19 -48
  257. package/dist/src/sessions/batch-contracts.js +25 -5
  258. package/dist/src/sessions/batch-import.js +72 -51
  259. package/dist/src/sessions/candidates.js +47 -11
  260. package/dist/src/sessions/contracts.js +75 -2
  261. package/dist/src/sessions/coverage-preflight.js +134 -0
  262. package/dist/src/sessions/discovery.js +3 -0
  263. package/dist/src/sessions/export-plan.js +4 -3
  264. package/dist/src/sessions/grok-build-cli.js +67 -5
  265. package/dist/src/sessions/import-exclusions.js +72 -0
  266. package/dist/src/sessions/import-lease.js +34 -18
  267. package/dist/src/sessions/import-limits.js +37 -0
  268. package/dist/src/sessions/importer.js +84 -46
  269. package/dist/src/sessions/index.js +2 -0
  270. package/dist/src/sessions/lifecycle.js +45 -0
  271. package/dist/src/sessions/policy.js +133 -25
  272. package/dist/src/sessions/readers.js +106 -53
  273. package/dist/src/sessions/refresh.js +106 -0
  274. package/dist/src/sessions/repository.js +199 -63
  275. package/dist/src/sessions/workspace-discovery.js +315 -88
  276. package/dist/src/sessions/workspace-matching.js +107 -0
  277. package/dist/src/skills/adapters/clawhub.js +10 -8
  278. package/dist/src/skills/adapters/openclaw.js +10 -7
  279. package/dist/src/skills/importer.js +1 -0
  280. package/dist/src/smiths/context-pipeline/aiwg-md.js +11 -0
  281. package/dist/src/smiths/context-pipeline/claude-hook.js +3 -26
  282. package/dist/src/smiths/context-pipeline/finalization.js +6 -5
  283. package/dist/src/smiths/context-pipeline/generator.js +21 -4
  284. package/dist/src/smiths/context-pipeline/parallelism-section.js +195 -14
  285. package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
  286. package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
  287. package/dist/src/smiths/mcpsmith/example.js +1 -1
  288. package/dist/src/smiths/mcpsmith/generator.js +1 -1
  289. package/dist/src/smiths/toolsmith/runtime-discovery.d.mts +10 -0
  290. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +85 -31
  291. package/dist/src/storage/backends/fs.js +18 -76
  292. package/dist/src/storage/backends/logseq.js +68 -7
  293. package/dist/src/storage/backends/obsidian.js +1 -0
  294. package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
  295. package/dist/src/testing/generators/test-code-generator.js +57 -30
  296. package/dist/src/tracker/forge-http.js +2 -1
  297. package/dist/src/uhp/client.js +11 -1
  298. package/dist/src/uhp/mission.js +5 -0
  299. package/dist/src/update/checker.d.mts +2 -0
  300. package/dist/src/update/checker.mjs +5 -5
  301. package/dist/src/update/notifier.mjs +54 -2
  302. package/dist/src/update/service.d.mts +22 -0
  303. package/dist/src/utils/contained-filesystem.d.mts +37 -0
  304. package/dist/src/utils/contained-filesystem.mjs +683 -0
  305. package/dist/src/utils/temporary-directory.js +36 -0
  306. package/dist/src/writing/example-generator.js +1 -1
  307. package/dist/src/writing/fidelity.js +18 -1
  308. package/package.json +32 -2
  309. package/schemas/mission-v1.schema.json +2429 -0
  310. package/schemas/models/model-catalog.v1.schema.json +301 -0
  311. package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
  312. package/schemas/models/model-policy.v1.schema.json +14 -0
  313. package/schemas/models/provider-model-capabilities.v1.schema.json +1072 -0
  314. package/tools/_resolve-impl.mjs +39 -2
  315. package/tools/agents/deploy-agents.mjs +64 -23
  316. package/tools/agents/providers/base.mjs +153 -166
  317. package/tools/agents/providers/claude.mjs +8 -24
  318. package/tools/agents/providers/codex.mjs +18 -11
  319. package/tools/agents/providers/cursor.mjs +19 -18
  320. package/tools/agents/providers/grok-build.mjs +3 -1
  321. package/tools/agents/providers/grokbot.mjs +11 -12
  322. package/tools/agents/providers/hermes.mjs +62 -49
  323. package/tools/agents/providers/omp.mjs +5 -2
  324. package/tools/agents/providers/openclaw-translator.mjs +19 -5
  325. package/tools/agents/providers/openclaw.mjs +9 -4
  326. package/tools/agents/providers/pi.mjs +5 -0
  327. package/tools/agents/providers/provider-prepare-channel.mjs +5 -0
  328. package/tools/agents/providers/provider-prepare-coordinator.mjs +247 -0
  329. package/tools/agents/providers/provider-prepare-worker.mjs +84 -0
  330. package/tools/agents/providers/rule-delivery.mjs +1270 -0
  331. package/tools/agents/providers/warp.mjs +20 -26
  332. package/tools/cli/decision-offload-report.mjs +40 -0
  333. package/tools/cli/workspace-status.mjs +862 -0
  334. package/tools/plugin/package-plugins.mjs +186 -8
  335. package/tools/plugin/plugin-links.mjs +114 -0
  336. package/tools/providers/antigravity-transport.mjs +3 -1
  337. package/tools/providers/deepseek-harness-transport.mjs +360 -0
  338. package/tools/providers/omp-transport.mjs +194 -0
  339. package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
  340. package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
  341. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +1748 -0
  342. package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
  343. package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
  344. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +55 -0
  345. package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
  346. package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
  347. package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
  348. package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
  349. package/tools/ralph-external/lib/pi-adapter.mjs +329 -0
  350. package/tools/ralph-external/lib/provider-adapter.mjs +748 -0
  351. package/tools/ralph-external/lib/recovery-probe.mjs +72 -0
  352. package/tools/ralph-external/lib/session-environment.mjs +28 -0
  353. package/tools/ralph-external/lib/systemd-client.mjs +49 -0
  354. package/tools/ralph-external/lib/worker-health.mjs +439 -0
  355. package/tools/ralph-external/lib/worker-isolation.mjs +717 -0
  356. package/tools/ralph-external/lib/worker-overflow.mjs +50 -0
  357. package/tools/ralph-external/process-monitor.mjs +1017 -0
  358. package/tools/ralph-external/session-launcher.mjs +5057 -0
  359. package/tools/scripts/resource-preflight.mjs +541 -0
  360. package/tools/skills/deploy-skills-codex.mjs +2 -1
  361. package/vscode-extension/schemas/aiwg.config.v1.json +2916 -0
@@ -5,11 +5,113 @@
5
5
  * @issues #1311 (scope split), #1312 (command-run), #1313 (discover + pairs)
6
6
  */
7
7
 
8
+ import { signalChild } from '../process/signal-child.mjs';
8
9
  import { spawn } from 'node:child_process';
9
10
  import { StringDecoder } from 'node:string_decoder';
10
11
  import fs from 'node:fs/promises';
11
12
  import path from 'node:path';
12
13
 
14
+ /** Existing MCP owners have not qualified worker contracts; fail before handoff. */
15
+ const MISSION_WORKER_SCHEMA_ID = 'https://aiwg.io/schemas/mission.aiwg.io/v1';
16
+ const MISSION_WORKER_DEFINITIONS = new Set([
17
+ 'workerArtifact',
18
+ 'workerEffort',
19
+ 'workerTuple',
20
+ 'workerLimits',
21
+ 'workerSelectionIntent',
22
+ 'workerBrief',
23
+ 'workerClaims',
24
+ 'workerUsage',
25
+ 'workerExecution',
26
+ 'workerVerification',
27
+ 'workerResult',
28
+ 'workerDispatch',
29
+ 'workerDirectIntent',
30
+ 'workerDirectBrief',
31
+ 'workerResolvedInvocation',
32
+ 'workerDirectVerification',
33
+ 'workerDirectResult',
34
+ 'workerHealthSummary',
35
+ 'workerEffectiveHealth',
36
+ ]);
37
+
38
+ // Resolve against the owning schema, never infer ownership from a $defs name.
39
+ function workerContractReference(reference, base = 'https://aiwg.invalid/local-schema') {
40
+ if (typeof reference !== 'string') return false;
41
+ try {
42
+ const resolved = new URL(reference, base);
43
+ const fragment = decodeURIComponent(resolved.hash).split('/');
44
+ resolved.hash = '';
45
+ return resolved.href === MISSION_WORKER_SCHEMA_ID
46
+ && fragment[0] === '#' && fragment[1] === '$defs'
47
+ && MISSION_WORKER_DEFINITIONS.has(fragment[2]);
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ export function assertNoWorkerBinding(value, consumer) {
54
+ const seenExtensions = new Set();
55
+ const seenRecords = new Set();
56
+ const seenSchemas = new Map();
57
+ function unsupported() {
58
+ const error = new Error(`OG_CONTRACT_UNSUPPORTED: ${consumer} does not advertise worker contract support.`);
59
+ error.code = 'OG_CONTRACT_UNSUPPORTED';
60
+ throw error;
61
+ }
62
+ function extensions(item) {
63
+ if (!item || typeof item !== 'object' || Array.isArray(item) || seenExtensions.has(item)) return;
64
+ seenExtensions.add(item);
65
+ if (Object.hasOwn(item, 'aiwg.worker-dispatch')) unsupported();
66
+ for (const [key, child] of Object.entries(item)) if (key.startsWith('aiwg.source.')) {
67
+ extensions(child);
68
+ record(child);
69
+ }
70
+ }
71
+ function schema(item, inheritedBase = 'https://aiwg.invalid/local-schema') {
72
+ if (!item || typeof item !== 'object' || Array.isArray(item)) return;
73
+ let base = inheritedBase;
74
+ if (typeof item.$id === 'string') {
75
+ try { base = new URL(item.$id, inheritedBase).href; } catch { /* invalid schemas belong to validation */ }
76
+ }
77
+ const bases = seenSchemas.get(item) ?? new Set();
78
+ if (bases.has(base)) return;
79
+ bases.add(base);
80
+ seenSchemas.set(item, bases);
81
+ if (workerContractReference(item.$ref, base) || workerContractReference(item.schemaRef, base)) unsupported();
82
+ // Only JSON Schema subschemas inherit an owner. const/default/examples
83
+ // and properties' names are data, not active references or extensions.
84
+ for (const key of ['$defs', 'definitions', 'properties', 'patternProperties', 'dependentSchemas', 'dependencies']) {
85
+ for (const child of Object.values(item[key] ?? {})) schema(child, base);
86
+ }
87
+ for (const key of ['allOf', 'anyOf', 'oneOf', 'prefixItems']) {
88
+ if (Array.isArray(item[key])) for (const child of item[key]) schema(child, base);
89
+ }
90
+ for (const key of ['items', 'additionalItems', 'additionalProperties', 'unevaluatedItems', 'unevaluatedProperties', 'contains', 'propertyNames', 'not', 'if', 'then', 'else']) {
91
+ if (Array.isArray(item[key])) for (const child of item[key]) schema(child, base);
92
+ else schema(item[key], base);
93
+ }
94
+ }
95
+ function record(item) {
96
+ if (!item || typeof item !== 'object' || Array.isArray(item) || seenRecords.has(item)) return;
97
+ seenRecords.add(item);
98
+ // All mission-v1 carriers: /extensions and /status/artifacts/*/extensions.
99
+ extensions(item.extensions);
100
+ if (Array.isArray(item.status?.artifacts)) item.status.artifacts.forEach(record);
101
+ // Native protocols also use metadata and top-level artifacts.
102
+ extensions(item.metadata);
103
+ if (Array.isArray(item.artifacts)) item.artifacts.forEach(record);
104
+ if (typeof item.$ref === 'string' || typeof item.schemaRef === 'string'
105
+ || typeof item.$id === 'string' || typeof item.$schema === 'string') schema(item);
106
+ schema(item.schema);
107
+ if (workerContractReference(item.output?.schemaRef)) unsupported();
108
+ // Existing sandbox response envelopes own these records explicitly.
109
+ record(item.workload);
110
+ if (item.document_type === 'inventory' && Array.isArray(item.records)) item.records.forEach(record);
111
+ }
112
+ record(value);
113
+ }
114
+
13
115
  /**
14
116
  * Resolve AIWG_ROOT, with safe fallback to known install locations.
15
117
  */
@@ -49,11 +151,14 @@ function parseProjectArtifactLocation(contents) {
49
151
  return null;
50
152
  }
51
153
 
52
- export async function resolveProjectAiwgDir(projectDir) {
154
+ // MCP artifact I/O resolves pointers and environment configuration when each
155
+ // project is first trusted, then pins the resulting store.
156
+ export async function resolveProjectAiwgDir(projectDir, { allowLocationPointer = true, env = process.env } = {}) {
53
157
  for (const key of ARTIFACT_PATH_ENV_ALIASES) {
54
- const value = process.env[key];
158
+ const value = env[key];
55
159
  if (typeof value === 'string' && value.trim()) return expandProjectArtifactPath(value, projectDir);
56
160
  }
161
+ if (!allowLocationPointer) return path.resolve(projectDir, '.aiwg');
57
162
  try {
58
163
  const pointer = await fs.readFile(path.join(projectDir, PROJECT_AIWG_LOCATION_FILE), 'utf-8');
59
164
  const configured = parseProjectArtifactLocation(pointer);
@@ -183,11 +288,11 @@ export function runAiwgCli(args, { cwd, env, timeoutMs = 120_000, input } = {})
183
288
  // owned child one second to exit gracefully, then escalate cleanup.
184
289
  reject(err);
185
290
  killTimer = setTimeout(() => {
186
- try { proc.kill('SIGKILL'); } catch { /* child may already have exited */ }
291
+ try { signalChild(proc, 'SIGKILL'); } catch { /* child may already have exited */ }
187
292
  }, 1000);
188
293
  killTimer.unref?.();
189
294
  // Install cleanup first: kill() can synchronously trigger close in an adapter.
190
- try { proc.kill('SIGTERM'); } catch { /* retain the original failure */ }
295
+ try { signalChild(proc, 'SIGTERM'); } catch { /* retain the original failure */ }
191
296
  };
192
297
  const timer = setTimeout(() => {
193
298
  rejectAndTerminate(new Error(`aiwg ${args[0] || ''} timed out after ${timeoutMs}ms`));
@@ -0,0 +1,13 @@
1
+ export type MuseNativeInspection = {
2
+ cli: string;
3
+ hooks: 'absent' | 'managed' | 'operator-only';
4
+ mcp: 'absent' | 'configured' | 'unreadable';
5
+ };
6
+
7
+ export function inspectMuseNative(options?: {
8
+ projectDir?: string;
9
+ env?: Record<string, string | undefined>;
10
+ userHome?: string;
11
+ }): Promise<MuseNativeInspection>;
12
+
13
+ export function isMuseDeployedProject(projectDir?: string): Promise<boolean>;
@@ -50,6 +50,7 @@ function inspectMuseCli(env) {
50
50
  env,
51
51
  encoding: 'utf8',
52
52
  timeout: MUSE_VERSION_TIMEOUT_MS,
53
+ killSignal: 'SIGKILL',
53
54
  });
54
55
  if (result.error?.code === 'ENOENT') return 'absent';
55
56
  if (result.error?.code === 'ETIMEDOUT') return 'present (version timeout)';
@@ -0,0 +1,4 @@
1
+ export function ompServerConfig(server: Record<string, any>): Record<string, any>;
2
+ export function manageOmpMcp(configPath: string, servers: Array<{name: string; [key: string]: any}>, options?: {dryRun?: boolean; remove?: string[]; userScope?: boolean; projectRoot?: string}): Promise<{
3
+ configPath: string; serversInjected: string[]; alreadyPresent: string[]; removed: string[];
4
+ }>;
@@ -1,22 +1,37 @@
1
- import { readFile, writeFile, mkdir, rename, lstat, unlink, open } from 'node:fs/promises';
2
- import { dirname, resolve } from 'node:path';
3
- import { createHash, randomUUID } from 'node:crypto';
1
+ import { assertConfigDestination, assertNoSymlinkParents, writeConfigAtomic } from './config-file.mjs';
2
+ import { readFile, mkdir, lstat, unlink, open } from 'node:fs/promises';
3
+ import { dirname, parse, resolve } from 'node:path';
4
+ import { createHash } from 'node:crypto';
4
5
 
5
6
  const hash = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
6
7
  const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
7
- async function rejectSymlinkPath(file) {
8
- let current = resolve(file);
9
- for (;;) {
10
- try {
11
- if ((await lstat(current)).isSymbolicLink()) throw new Error('OMP MCP configuration path cannot traverse a symbolic link');
12
- } catch (error) { if (error.code !== 'ENOENT') throw error; }
13
- const parent = dirname(current);
14
- if (parent === current) return;
15
- current = parent;
8
+ /**
9
+ * Reject a symlinked ancestor, regardless of whether the caller passed a
10
+ * projectRoot: a symlinked parent can redirect the write outside the intended
11
+ * directory either way.
12
+ *
13
+ * - With a projectRoot, the walk stops there (matching
14
+ * assertConfigDestination) so a deliberately symlinked project root
15
+ * itself - a dotfile-manager alias - is not rejected as its own ancestor.
16
+ * - Without a projectRoot, an explicit `userScope: true` means the caller
17
+ * knows this is a private home-scope destination (symlinked dotfile-
18
+ * manager parents there are tolerated, same as every other provider's
19
+ * home-scope target); the walk is skipped.
20
+ * - Without a projectRoot and without an explicit userScope, there is no
21
+ * known safe boundary, so every ancestor up to the filesystem root is
22
+ * checked - the conservative default for a caller that supplied neither.
23
+ */
24
+ async function rejectSymlinkAncestors(file, projectRoot, userScope) {
25
+ if (projectRoot) {
26
+ await assertNoSymlinkParents(file, resolve(projectRoot));
27
+ return;
16
28
  }
29
+ if (userScope === true) return;
30
+ await assertNoSymlinkParents(file, parse(resolve(file)).root);
17
31
  }
18
- async function readObject(file) {
19
- await rejectSymlinkPath(file);
32
+ async function readObject(file, projectRoot, userScope) {
33
+ await rejectSymlinkAncestors(file, projectRoot, userScope);
34
+ await assertConfigDestination(file, projectRoot);
20
35
  try {
21
36
  if ((await lstat(file)).isSymbolicLink()) throw new Error('OMP MCP configuration cannot be a symbolic link');
22
37
  const data = JSON.parse(await readFile(file, 'utf8'));
@@ -28,13 +43,10 @@ async function readObject(file) {
28
43
  throw error;
29
44
  }
30
45
  }
31
- async function atomic(file, data) {
32
- await mkdir(dirname(file), { recursive: true });
33
- const temporary = `${file}.${randomUUID()}.tmp`;
34
- try {
35
- await writeFile(temporary, JSON.stringify(data, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
36
- await rename(temporary, file);
37
- } finally { await unlink(temporary).catch(() => {}); }
46
+ async function atomic(file, data, options) {
47
+ // OMP files can carry auth/oauth secrets, so they are always owner-only even
48
+ // for project-scope destinations. Containment still follows projectRoot.
49
+ await writeConfigAtomic(file, JSON.stringify(data, null, 2) + '\n', { ...options, userScope: true });
38
50
  }
39
51
 
40
52
  export function ompServerConfig(server) {
@@ -74,14 +86,21 @@ export function ompServerConfig(server) {
74
86
  config.headers[header] = '${' + variable + '}';
75
87
  }
76
88
  }
89
+ if (server.envFrom) {
90
+ config.env = { ...config.env };
91
+ for (const [key, variable] of Object.entries(server.envFrom)) {
92
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(variable)) throw new Error('Invalid MCP env-from environment reference');
93
+ config.env[key] = '${' + variable + '}';
94
+ }
95
+ }
77
96
  return config;
78
97
  }
79
98
 
80
99
  /** Hash-only receipt: never saves an extra copy of operator configuration. */
81
- async function manageLockedOmpMcp(configPath, servers, { dryRun = false, remove = [] } = {}) {
100
+ async function manageLockedOmpMcp(configPath, servers, { dryRun = false, remove = [], userScope, projectRoot } = {}) {
82
101
  const receiptPath = `${configPath}.aiwg-ownership.json`;
83
- const existing = await readObject(configPath);
84
- const receipt = await readObject(receiptPath);
102
+ const existing = await readObject(configPath, projectRoot, userScope);
103
+ const receipt = await readObject(receiptPath, projectRoot, userScope);
85
104
  if (receipt.servers !== undefined && (!record(receipt.servers) || receipt.schema !== 'aiwg.omp-mcp-ownership.v1')) throw new Error('Invalid OMP MCP ownership receipt');
86
105
  if (existing.mcpServers !== undefined && !record(existing.mcpServers)) throw new Error('Invalid OMP mcpServers object');
87
106
  const next = { ...(existing.mcpServers || {}) };
@@ -105,10 +124,10 @@ async function manageLockedOmpMcp(configPath, servers, { dryRun = false, remove
105
124
  if (owned[name]) { delete next[name]; delete owned[name]; result.removed.push(name); }
106
125
  }
107
126
  if (!dryRun) {
108
- if (hash(await readObject(configPath)) !== hash(existing) || hash(await readObject(receiptPath)) !== hash(receipt)) throw new Error('OMP MCP configuration changed during injection; retry after reviewing the operator edit');
127
+ if (hash(await readObject(configPath, projectRoot, userScope)) !== hash(existing) || hash(await readObject(receiptPath, projectRoot, userScope)) !== hash(receipt)) throw new Error('OMP MCP configuration changed during injection; retry after reviewing the operator edit');
109
128
  // If interrupted between writes, a retry fails closed on the hash mismatch.
110
- await atomic(configPath, { ...existing, mcpServers: next });
111
- await atomic(receiptPath, { schema: 'aiwg.omp-mcp-ownership.v1', servers: owned });
129
+ await atomic(configPath, { ...existing, mcpServers: next }, { userScope, projectRoot });
130
+ await atomic(receiptPath, { schema: 'aiwg.omp-mcp-ownership.v1', servers: owned }, { userScope: true, projectRoot });
112
131
  }
113
132
  return result;
114
133
  }
@@ -116,9 +135,11 @@ async function manageLockedOmpMcp(configPath, servers, { dryRun = false, remove
116
135
  /** Serialize AIWG writers; operator edits detected before committing a replacement. */
117
136
  export async function manageOmpMcp(configPath, servers, options = {}) {
118
137
  if (options.dryRun) return manageLockedOmpMcp(configPath, servers, options);
119
- await rejectSymlinkPath(configPath);
138
+ await rejectSymlinkAncestors(configPath, options.projectRoot, options.userScope);
139
+ await assertConfigDestination(configPath, options.projectRoot);
120
140
  const lockPath = `${configPath}.aiwg-lock`;
121
- await rejectSymlinkPath(lockPath);
141
+ await rejectSymlinkAncestors(lockPath, options.projectRoot, options.userScope);
142
+ await assertConfigDestination(lockPath, options.projectRoot);
122
143
  await mkdir(dirname(configPath), { recursive: true });
123
144
  let lock;
124
145
  try { lock = await open(lockPath, 'wx', 0o600); }
@@ -9,7 +9,9 @@
9
9
  *
10
10
  * @implements #889
11
11
  */
12
- import { readFile, writeFile, mkdir } from 'fs/promises';
12
+ import { resolveCredentialPolicy } from './credentials.mjs';
13
+ import { isLowerLayerEntry, loadLayered, resolveProfileExtends, resolveConfigLayers, saveLayerData } from './config-layers.mjs';
14
+ import { readFile, writeFile } from 'fs/promises';
13
15
  import { resolve } from 'path';
14
16
  import { resolveConfigDir } from '../config/user-config.js';
15
17
  // ─────────────────────────────────────────────
@@ -66,8 +68,12 @@ export const PRESET_PROFILES = {
66
68
  export class McpProfileRegistry {
67
69
  configDir;
68
70
  cache = null;
71
+ /** Configuration layers, lowest precedence first; null for a single directory */
72
+ layers;
73
+ layering = null;
69
74
  constructor(configDirOverride) {
70
- this.configDir = resolveConfigDir(configDirOverride);
75
+ this.layers = resolveConfigLayers(configDirOverride);
76
+ this.configDir = this.layers ? this.layers[this.layers.length - 1] : resolveConfigDir(configDirOverride);
71
77
  }
72
78
  getPath() {
73
79
  return resolve(this.configDir, PROFILES_FILENAME);
@@ -75,6 +81,12 @@ export class McpProfileRegistry {
75
81
  async load() {
76
82
  if (this.cache)
77
83
  return this.cache;
84
+ if (this.layers) {
85
+ const layered = await loadLayered(this.layers, PROFILES_FILENAME, 'profiles', DEFAULT_DATA);
86
+ this.cache = layered.data;
87
+ this.layering = layered.layering;
88
+ return this.cache;
89
+ }
78
90
  const filePath = this.getPath();
79
91
  try {
80
92
  const content = await readFile(filePath, 'utf-8');
@@ -88,13 +100,24 @@ export class McpProfileRegistry {
88
100
  catch {
89
101
  this.cache = { ...DEFAULT_DATA, profiles: {} };
90
102
  }
103
+ try {
104
+ resolveCredentialPolicy({ registryPolicy: this.cache.credentialPolicy, env: {} });
105
+ }
106
+ catch (error) {
107
+ this.cache = null;
108
+ throw new Error(`Invalid MCP credential policy in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
109
+ }
91
110
  return this.cache;
92
111
  }
93
- async save() {
94
- if (!this.cache)
112
+ async save(data = this.cache) {
113
+ if (!data)
95
114
  return;
96
- await mkdir(this.configDir, { recursive: true });
97
- await writeFile(this.getPath(), JSON.stringify(this.cache, null, 2) + '\n', 'utf-8');
115
+ await saveLayerData(this.getPath(), data, 'profiles', this.layers, this.layering);
116
+ this.cache = data;
117
+ if (this.layers) {
118
+ this.clearCache();
119
+ await this.load();
120
+ }
98
121
  }
99
122
  validateName(name) {
100
123
  if (!NAME_RE.test(name)) {
@@ -122,11 +145,13 @@ export class McpProfileRegistry {
122
145
  }
123
146
  async add(profile, serverRegistry) {
124
147
  this.validateName(profile.name);
125
- const data = await this.load();
148
+ const data = structuredClone(await this.load());
126
149
  if (data.profiles[profile.name]) {
127
150
  throw new Error(`Profile "${profile.name}" already exists. Use "aiwg mcp profile edit" to modify it.`);
128
151
  }
129
152
  await this.validateServers(profile.servers ?? [], serverRegistry);
153
+ if (profile.extends?.length)
154
+ resolveProfileExtends(profile.name, { ...data.profiles, [profile.name]: profile });
130
155
  data.profiles[profile.name] = {
131
156
  ...profile,
132
157
  servers: profile.servers ?? [],
@@ -134,18 +159,28 @@ export class McpProfileRegistry {
134
159
  createdAt: new Date().toISOString(),
135
160
  updatedAt: new Date().toISOString(),
136
161
  };
137
- await this.save();
162
+ await this.save(data);
138
163
  }
139
164
  async get(name) {
140
165
  const data = await this.load();
141
166
  return data.profiles[name];
142
167
  }
168
+ /**
169
+ * Return a profile with its `extends` chain applied across all configuration
170
+ * layers. Use get() for the profile exactly as stored.
171
+ */
172
+ async resolve(name) {
173
+ const data = await this.load();
174
+ if (!data.profiles[name])
175
+ return undefined;
176
+ return resolveProfileExtends(name, data.profiles);
177
+ }
143
178
  async list() {
144
179
  const data = await this.load();
145
180
  return Object.values(data.profiles);
146
181
  }
147
182
  async edit(name, changes, serverRegistry) {
148
- const data = await this.load();
183
+ const data = structuredClone(await this.load());
149
184
  const existing = data.profiles[name];
150
185
  if (!existing)
151
186
  throw new Error(`Profile "${name}" not found.`);
@@ -162,20 +197,45 @@ export class McpProfileRegistry {
162
197
  if (changes.removeServers && changes.removeServers.length > 0) {
163
198
  current.servers = current.servers.filter((s) => !(changes.removeServers ?? []).includes(s));
164
199
  }
200
+ if (changes.clearToolFilters) {
201
+ const overrides = { ...(current.providerOverrides ?? {}) };
202
+ delete overrides[changes.clearToolFilters];
203
+ current.providerOverrides = overrides;
204
+ }
205
+ for (const [provider, override] of Object.entries(changes.providerOverrides ?? {})) {
206
+ const overrides = { ...(current.providerOverrides ?? {}) };
207
+ const previous = overrides[provider] ?? {};
208
+ const merge = (before, added) => (added ? [...new Set([...(before ?? []), ...added])] : before);
209
+ overrides[provider] = {
210
+ ...previous,
211
+ ...(override.toolDeny ? { toolDeny: merge(previous.toolDeny, override.toolDeny) } : {}),
212
+ ...(override.toolAllow ? { toolAllow: merge(previous.toolAllow, override.toolAllow) } : {}),
213
+ };
214
+ current.providerOverrides = overrides;
215
+ }
165
216
  current.updatedAt = new Date().toISOString();
166
217
  data.profiles[name] = current;
167
- await this.save();
218
+ await this.save(data);
168
219
  return data.profiles[name];
169
220
  }
170
221
  async remove(name) {
171
- const data = await this.load();
222
+ const data = structuredClone(await this.load());
172
223
  if (!data.profiles[name])
173
224
  throw new Error(`Profile "${name}" not found.`);
225
+ if (this.layering && isLowerLayerEntry(this.layering, name)) {
226
+ throw new Error(`Profile "${name}" is defined in a lower configuration layer; remove it there.`);
227
+ }
228
+ const dependents = Object.entries(data.profiles)
229
+ .filter(([other, profile]) => other !== name && profile.extends?.includes(name))
230
+ .map(([other]) => other);
231
+ if (dependents.length) {
232
+ throw new Error(`Cannot remove profile "${name}": extended by ${dependents.join(', ')}.`);
233
+ }
174
234
  delete data.profiles[name];
175
- await this.save();
235
+ await this.save(data);
176
236
  }
177
237
  async resolveServers(name, serverRegistry) {
178
- const profile = await this.get(name);
238
+ const profile = await this.resolve(name);
179
239
  if (!profile)
180
240
  throw new Error(`Profile "${name}" not found.`);
181
241
  if (profile.servers.includes('__all__') && serverRegistry) {
@@ -194,20 +254,27 @@ export class McpProfileRegistry {
194
254
  async importFrom(filePath) {
195
255
  const content = await readFile(filePath, 'utf-8');
196
256
  const imported = JSON.parse(content);
197
- const data = await this.load();
257
+ const data = structuredClone(await this.load());
198
258
  let added = 0;
199
259
  let updated = 0;
200
- const profiles = imported.profiles ?? {};
260
+ const invalidNames = [];
261
+ const profiles = imported.profiles ?? (typeof imported === 'object' && !Array.isArray(imported)
262
+ ? imported
263
+ : {});
264
+ const candidate = { ...data.profiles };
201
265
  for (const [name, profile] of Object.entries(profiles)) {
202
266
  try {
203
267
  this.validateName(name);
204
268
  }
205
- catch {
269
+ catch (error) {
270
+ // Collected and refused below with the complete graph-validation pass,
271
+ // instead of silently dropping named profiles from the import.
272
+ invalidNames.push(`${name}: ${error instanceof Error ? error.message : String(error)}`);
206
273
  continue;
207
274
  }
208
- if (data.profiles[name]) {
209
- data.profiles[name] = {
210
- ...data.profiles[name],
275
+ if (candidate[name]) {
276
+ candidate[name] = {
277
+ ...candidate[name],
211
278
  ...profile,
212
279
  name,
213
280
  updatedAt: new Date().toISOString(),
@@ -215,9 +282,10 @@ export class McpProfileRegistry {
215
282
  updated++;
216
283
  }
217
284
  else {
218
- data.profiles[name] = {
285
+ candidate[name] = {
219
286
  name,
220
287
  description: profile.description,
288
+ ...(profile.extends ? { extends: profile.extends } : {}),
221
289
  servers: profile.servers ?? [],
222
290
  providerOverrides: profile.providerOverrides ?? {},
223
291
  createdAt: new Date().toISOString(),
@@ -226,7 +294,13 @@ export class McpProfileRegistry {
226
294
  added++;
227
295
  }
228
296
  }
229
- await this.save();
297
+ if (invalidNames.length) {
298
+ throw new Error(`Refusing to import profile(s) with invalid names: ${invalidNames.join('; ')}`);
299
+ }
300
+ for (const name of Object.keys(candidate))
301
+ resolveProfileExtends(name, candidate);
302
+ data.profiles = candidate;
303
+ await this.save(data);
230
304
  return { added, updated };
231
305
  }
232
306
  async exportTo(filePath, profileName) {
@@ -240,7 +314,7 @@ export class McpProfileRegistry {
240
314
  await writeFile(filePath, JSON.stringify({ profiles: toExport }, null, 2) + '\n', 'utf-8');
241
315
  }
242
316
  async initPresets() {
243
- const data = await this.load();
317
+ const data = structuredClone(await this.load());
244
318
  let added = 0;
245
319
  for (const [name, preset] of Object.entries(PRESET_PROFILES)) {
246
320
  if (!data.profiles[name]) {
@@ -253,7 +327,7 @@ export class McpProfileRegistry {
253
327
  added++;
254
328
  }
255
329
  }
256
- await this.save();
330
+ await this.save(data);
257
331
  return { added, total: Object.keys(PRESET_PROFILES).length };
258
332
  }
259
333
  clearCache() {