@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
@@ -7,20 +7,146 @@
7
7
  */
8
8
 
9
9
  import { startServer, createServer } from './server.mjs';
10
+ import { assertConfigDestination, assertConfigObject, assertProjectCredentials, isPathInsideDir, classifyMcpDestination, projectCredentialRemedy, writeConfigAtomic, writeConfigTransaction } from './config-file.mjs';
10
11
  import fs from 'fs/promises';
11
12
  import path from 'path';
12
- import { fileURLToPath } from 'url';
13
+ import os from 'os';
14
+ import { existsSync as nodeExistsSync, readFileSync as nodeReadFileSync } from 'fs';
15
+ import { fileURLToPath, pathToFileURL } from 'url';
13
16
  import {
14
17
  McpServerRegistry,
18
+ buildServerConfig,
15
19
  injectServers,
16
20
  SUPPORTED_PROVIDERS,
17
21
  getProviderConfigPath,
18
22
  } from './registry.mjs';
19
23
  import { McpProfileRegistry } from './profiles.mjs';
24
+ import {
25
+ applyJsonToolFilterPlan,
26
+ hasToolFilters,
27
+ planToolFilters,
28
+ prepareClaudePermissions,
29
+ resolveToolFilters,
30
+ } from './tool-filters.mjs';
31
+ import { assertCredentialPolicy, CREDENTIAL_POLICIES, installPolicyCheckedServer, redactCredentialArgs, redactCredentialText, redactCredentialRecord, resolveCredentialPolicy } from './credentials.mjs';
20
32
  import { getMcpInjectionDefinition } from '../providers/provider-definitions.mjs';
21
33
  import { manageOmpMcp } from './omp-config.mjs';
34
+ import { claudeDesktopConfigPath, resolveClaudeInstallTarget, legacyClaudeMigration } from './claude-install.mjs';
22
35
  import { unmanageGrokBuildMcp } from './grok-build-config.mjs';
23
36
 
37
+ /**
38
+ * Thrown when a recognized flag is present but has no usable value
39
+ * following it (#3062 review item 11). Mirrors `src/cli/flag-parsing.ts`'s
40
+ * `MissingFlagValueError`/`requireFlagValue`, duplicated in plain JS rather
41
+ * than imported: this file is exercised both compiled (`dist/src/mcp/
42
+ * cli.mjs`, where a `../cli/flag-parsing.js` sibling import would resolve)
43
+ * and directly from source in tests (`src/mcp/cli.mjs`, where it would
44
+ * not — there is no compiled `.js` sibling under `src/` itself).
45
+ */
46
+ class MissingFlagValueError extends Error {
47
+ constructor(flagName) {
48
+ super(`${flagName} requires a value`);
49
+ this.name = 'MissingFlagValueError';
50
+ this.flagName = flagName;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Single-pass, every-occurrence-validated flag lookup (#3062 second
56
+ * review, finding 4 — mirrors `src/cli/flag-parsing.ts`'s
57
+ * `scanFlagOccurrences`/`requireFlagValue`, duplicated here for the same
58
+ * source/dist reason as the class above): stops at a bare `--`
59
+ * terminator; accepts `--flag value` and `--flag=value`; a space-
60
+ * separated value starting with `--` is treated as missing (use `=` for a
61
+ * value that must start with `--`); every occurrence is checked, not just
62
+ * the first; `options.rejectEmpty` additionally rejects an empty-string
63
+ * value. `options.precedence` picks which occurrence wins when the flag
64
+ * repeats — `'first'` (default) here matches this file's own pre-existing
65
+ * `parseFlag()` convention (first match wins).
66
+ */
67
+ function requireFlagValue(args, options, ...names) {
68
+ const occurrences = [];
69
+ for (let i = 0; i < args.length; i += 1) {
70
+ const token = args[i];
71
+ if (token === undefined) continue;
72
+ if (token === '--') break;
73
+
74
+ if (token.startsWith('--')) {
75
+ const eq = token.indexOf('=');
76
+ if (eq !== -1) {
77
+ const flag = token.slice(0, eq);
78
+ const value = token.slice(eq + 1);
79
+ if (names.includes(flag)) {
80
+ if (options.rejectEmpty && value === '') throw new MissingFlagValueError(flag);
81
+ occurrences.push(value);
82
+ continue;
83
+ }
84
+ }
85
+ }
86
+
87
+ if (names.includes(token)) {
88
+ const next = args[i + 1];
89
+ if (next === undefined || next.startsWith('--')) throw new MissingFlagValueError(token);
90
+ if (options.rejectEmpty && next === '') throw new MissingFlagValueError(token);
91
+ occurrences.push(next);
92
+ i += 1;
93
+ }
94
+ }
95
+ if (occurrences.length === 0) return undefined;
96
+ return options.precedence === 'last' ? occurrences[occurrences.length - 1] : occurrences[0];
97
+ }
98
+
99
+ /**
100
+ * Find the AIWG package root by walking up from THIS file's own location
101
+ * (never from `AIWG_ROOT`/`process.cwd()`, which name the *served project*,
102
+ * not necessarily where this module's own package lives). Depth-independent
103
+ * on purpose: this file ships at `src/mcp/cli.mjs` in a dev checkout and at
104
+ * `dist/src/mcp/cli.mjs` in the published package. Mirrors
105
+ * `src/mcp/tools/worker-invoke.mjs`'s own `findOwnPackageRoot` (and
106
+ * `tools/_resolve-impl.mjs`'s `findPackageRoot`).
107
+ */
108
+ function findOwnPackageRoot() {
109
+ let dir = path.dirname(fileURLToPath(import.meta.url));
110
+ for (let i = 0; i < 12; i += 1) {
111
+ const pkgPath = path.join(dir, 'package.json');
112
+ if (nodeExistsSync(pkgPath)) {
113
+ try {
114
+ const pkg = JSON.parse(nodeReadFileSync(pkgPath, 'utf8'));
115
+ if (pkg.name === 'aiwg' || pkg.name === '@aiwg/cli') return dir;
116
+ } catch { /* keep walking */ }
117
+ }
118
+ const parent = path.dirname(dir);
119
+ if (parent === dir) break;
120
+ dir = parent;
121
+ }
122
+ throw new Error('Could not locate the AIWG package root from mcp/cli.mjs');
123
+ }
124
+
125
+ /**
126
+ * Load the compiled `loginInProfile()` adapter (#3062 second review,
127
+ * finding 3). The first round routed this through `tools/_resolve-impl.mjs`,
128
+ * which falls back to importing the TypeScript *source* when no compiled
129
+ * `dist/src/` exists — but that source module's own internal imports
130
+ * (e.g. `../../cli/env.js`) have no `.js` sibling under `src/` either, so
131
+ * the fallback itself fails deep inside the adapter with an unrelated-
132
+ * looking `ERR_MODULE_NOT_FOUND`, on every Node version, not just ones
133
+ * without native TypeScript support. This loads the compiled module
134
+ * directly — never TypeScript source — and fails fast with one actionable
135
+ * line when it is missing, instead of attempting (and failing inside) a
136
+ * source fallback.
137
+ */
138
+ async function loadCodexRuntimeAdapter() {
139
+ const root = findOwnPackageRoot();
140
+ const compiledPath = path.join(root, 'dist', 'src', 'mcp', 'adapters', 'codex-runtime.js');
141
+ if (!nodeExistsSync(compiledPath)) {
142
+ throw new Error(
143
+ 'aiwg mcp profile login requires the built CLI (missing dist/src/mcp/adapters/codex-runtime.js). ' +
144
+ 'Use an installed `aiwg` for this command; a source checkout must be built first (see docs/contributing).',
145
+ );
146
+ }
147
+ return import(pathToFileURL(compiledPath).href);
148
+ }
149
+
24
150
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
151
 
26
152
  /**
@@ -42,6 +168,8 @@ Usage:
42
168
  aiwg mcp inject [opts] Inject servers into provider configs
43
169
  aiwg mcp uninject [opts] Remove unchanged AIWG-owned OMP server entries
44
170
  aiwg mcp profile <sub> Manage MCP profiles (named server subsets)
171
+ aiwg mcp credential-policy [literal|references|none]
172
+ Show or set the registry's default credential policy
45
173
 
46
174
  Server Options (for add/update):
47
175
  --url <url> Server URL (for http/sse types)
@@ -52,6 +180,8 @@ Server Options (for add/update):
52
180
  --headers <K=V,...> HTTP headers (comma-separated K=V pairs)
53
181
  --header-env <K=ENV,...>
54
182
  Resolve HTTP header values from environment variables
183
+ --env-from <K=ENV,...>
184
+ Resolve stdio server variables from environment variables
55
185
  --description <text> Optional description
56
186
 
57
187
  Inject Options:
@@ -60,16 +190,25 @@ Inject Options:
60
190
  --all Inject into all previously configured providers
61
191
  --servers <a,b,...> Only inject specific servers (comma-separated names)
62
192
  --dry-run Show what would change without writing
193
+ --strict-credentials Refuse servers with literal env/headers values; render
194
+ only --header-env/--env-from references
195
+ --no-credentials Refuse servers with any credential-bearing field,
196
+ references included
63
197
 
64
198
  Serve Options:
65
199
  --transport <type> Transport type: stdio (default), http
66
200
  --port <number> Port for HTTP transport (default: 3100)
201
+ --toolsets <csv> Opt-in toolsets: flows, missions, memory, kb, research,
202
+ activity-log, index, ralph, mc, ops, sandbox, decision, workers
203
+ all excludes workers; enable explicitly with --toolsets all,workers
67
204
 
68
205
  Examples:
69
206
  # Define MCP servers
70
207
  aiwg mcp add fortemi --url https://memory.s9.internal/mcp --type http
71
208
  aiwg mcp add fortemi-enterprise --url https://memory.example.internal/mcp --type http \
72
209
  --header-env Authorization=AIWG_FORTEMI_TOKEN
210
+ aiwg mcp add github --type stdio --command github-mcp-server \
211
+ --env-from GITHUB_PERSONAL_ACCESS_TOKEN=GITHUB_TOKEN
73
212
  aiwg mcp add gitea --url https://mcp-gitea.integrolabs.net/mcp
74
213
  aiwg mcp add mytools --type stdio --command npx --args mcp-server-mytools
75
214
 
@@ -87,7 +226,8 @@ Examples:
87
226
  aiwg mcp remove fortemi
88
227
 
89
228
  # Install AIWG's own MCP server
90
- aiwg mcp install claude
229
+ aiwg mcp install claude-code
230
+ aiwg mcp install claude-desktop
91
231
  aiwg mcp serve
92
232
  aiwg mcp info
93
233
  `);
@@ -96,20 +236,24 @@ Examples:
96
236
  /**
97
237
  * Generate MCP client configuration
98
238
  */
99
- async function generateConfig(target, projectDir = '.') {
239
+ async function generateConfig(target, projectDir = '.', scope = 'project') {
100
240
  const homeDir = process.env.HOME || process.env.USERPROFILE;
101
241
 
102
242
  const configs = {
103
- claude: {
104
- path: path.join(projectDir, '.claude/settings.local.json'),
243
+ 'claude-desktop': {
244
+ path: claudeDesktopConfigPath(),
245
+ content: { mcpServers: { aiwg: { command: 'aiwg', args: ['mcp', 'serve'] } } },
246
+ },
247
+ 'claude-code': {
248
+ path: scope === 'user' ? path.join(homeDir, '.claude.json') : path.join(projectDir, '.mcp.json'),
249
+ userScope: scope === 'user',
105
250
  content: {
106
251
  mcpServers: {
107
252
  aiwg: {
108
253
  command: 'aiwg',
109
254
  args: ['mcp', 'serve'],
110
- env: {
111
- AIWG_ROOT: process.env.AIWG_ROOT || '~/.local/share/ai-writing-guide'
112
- }
255
+ ...(scope === 'user' && process.env.AIWG_ROOT
256
+ ? { env: { AIWG_ROOT: process.env.AIWG_ROOT } } : {})
113
257
  }
114
258
  }
115
259
  }
@@ -133,6 +277,7 @@ async function generateConfig(target, projectDir = '.') {
133
277
  })
134
278
  },
135
279
  factory: {
280
+ userScope: projectDir === '.' || projectDir === 'global',
136
281
  // Factory stores MCP config at user level in ~/.factory/mcp.json
137
282
  // or project level in .factory/mcp.json
138
283
  path: projectDir === '.' || projectDir === 'global'
@@ -158,7 +303,8 @@ async function generateConfig(target, projectDir = '.') {
158
303
  },
159
304
  codex: {
160
305
  // Codex stores config in ~/.codex/config.toml (TOML format)
161
- path: path.join(homeDir, '.codex/config.toml'),
306
+ path: path.resolve(process.env.CODEX_HOME || path.join(homeDir, '.codex'), 'config.toml'),
307
+ userScope: true,
162
308
  // We generate TOML snippet to append, not JSON
163
309
  content: null,
164
310
  toml: `
@@ -185,8 +331,8 @@ enabled_tools = [
185
331
  let existing = '';
186
332
  try {
187
333
  existing = await fs.readFile(configPath, 'utf-8');
188
- } catch {
189
- // File doesn't exist
334
+ } catch (error) {
335
+ if (error.code !== 'ENOENT') throw error;
190
336
  }
191
337
 
192
338
  // Check if AIWG MCP already configured
@@ -198,8 +344,7 @@ enabled_tools = [
198
344
  // Append TOML config
199
345
  const updated = existing.trimEnd() + '\n' + tomlContent.trim() + '\n';
200
346
 
201
- await fs.mkdir(path.dirname(configPath), { recursive: true });
202
- await fs.writeFile(configPath, updated);
347
+ await writeConfigAtomic(configPath, updated, { userScope: true });
203
348
  console.log(`MCP configuration appended to: ${configPath}`);
204
349
  console.log(`\nTo use AIWG MCP server with Codex:`);
205
350
  console.log(` 1. Restart Codex CLI`);
@@ -209,12 +354,14 @@ enabled_tools = [
209
354
  },
210
355
  openai: {
211
356
  // Alias for codex
212
- path: path.join(homeDir, '.codex/config.toml'),
357
+ path: path.resolve(process.env.CODEX_HOME || path.join(homeDir, '.codex'), 'config.toml'),
358
+ userScope: true,
213
359
  alias: 'codex'
214
360
  },
215
361
  windsurf: {
216
362
  // Windsurf stores MCP config at ~/.codeium/windsurf/mcp_config.json
217
363
  path: path.join(homeDir, '.codeium/windsurf/mcp_config.json'),
364
+ userScope: true,
218
365
  content: {
219
366
  mcpServers: {
220
367
  aiwg: {
@@ -312,17 +459,21 @@ enabled_tools = [
312
459
 
313
460
  // Find existing config
314
461
  for (const loc of locations) {
462
+ await assertConfigDestination(loc, projectRoot);
315
463
  try {
316
464
  const rawContent = await fs.readFile(loc, 'utf-8');
317
- // Strip JSONC comments for parsing
318
- const jsonContent = rawContent
319
- .replace(/\/\/.*$/gm, '')
320
- .replace(/\/\*[\s\S]*?\*\//g, '');
465
+ // Strip JSONC comments while preserving strings such as URLs.
466
+ const jsonContent = rawContent.replace(
467
+ /"(?:\\.|[^"\\])*"|\/\/[^\r\n]*|\/\*[\s\S]*?\*\//g,
468
+ match => match.startsWith('"') ? match : ' ',
469
+ );
321
470
  existing = JSON.parse(jsonContent);
471
+ assertConfigObject(existing, 'mcp');
322
472
  targetPath = loc;
323
473
  break;
324
- } catch {
325
- // Continue to next location
474
+ } catch (error) {
475
+ if (error instanceof SyntaxError) throw new Error(`Refusing to overwrite malformed MCP config ${loc}: invalid JSON`);
476
+ if (error.code !== 'ENOENT') throw error;
326
477
  }
327
478
  }
328
479
 
@@ -335,8 +486,7 @@ enabled_tools = [
335
486
  // Merge configuration
336
487
  const merged = mergeFunc(existing, content);
337
488
 
338
- await fs.mkdir(path.dirname(targetPath), { recursive: true });
339
- await fs.writeFile(targetPath, JSON.stringify(merged, null, 2));
489
+ await writeConfigAtomic(targetPath, JSON.stringify(merged, null, 2), { userScope, projectRoot });
340
490
  console.log(`MCP configuration written to: ${targetPath}`);
341
491
  console.log(`\nTo use AIWG MCP server with OpenCode:`);
342
492
  console.log(` 1. Restart OpenCode`);
@@ -358,21 +508,61 @@ enabled_tools = [
358
508
  config = configs[config.alias];
359
509
  }
360
510
 
511
+ // Destination-derived, not scope-derived (and never from a static
512
+ // `config.userScope` flag): a target whose resolved path still lands inside
513
+ // the project directory gets project containment, whatever scope was
514
+ // requested. Providers with no real user-scope destination (Cursor,
515
+ // opencode) refuse `--scope user` instead of silently writing the project
516
+ // file. `config.path` is null only for providers with no config file at all
517
+ // (Warp, UI-configured), which never reach a destination check.
518
+ const projectBase = target === 'opencode' && projectDir === 'global' ? process.cwd() : projectDir;
519
+ const { userScope, projectRoot } = config.path
520
+ ? classifyMcpDestination(target, scope, config.path, projectBase)
521
+ : { userScope: Boolean(config.userScope), projectRoot: undefined };
522
+ if (config.path) await assertConfigDestination(config.path, projectRoot);
523
+
361
524
  // Handle custom handler (for TOML configs like Codex, or OpenCode JSON)
362
525
  if (config.handler) {
363
526
  return await config.handler(config.path, config.toml, config.content, config.merge);
364
527
  }
365
528
 
366
- // Ensure directory exists
367
- await fs.mkdir(path.dirname(config.path), { recursive: true });
368
-
369
529
  // Check if file exists and merge
370
530
  let existing = {};
371
531
  try {
372
532
  const content = await fs.readFile(config.path, 'utf-8');
373
533
  existing = JSON.parse(content);
374
- } catch {
375
- // File doesn't exist, start fresh
534
+ } catch (error) {
535
+ if (error instanceof SyntaxError) throw new Error(`Refusing to overwrite malformed MCP config ${config.path}: invalid JSON`);
536
+ if (error.code !== 'ENOENT') throw error;
537
+ }
538
+
539
+ const installServerMapKey = Object.keys(config.content)[0];
540
+ assertConfigObject(existing, installServerMapKey);
541
+
542
+ // Install-generated server definitions go through the same resolved
543
+ // credential policy as `aiwg mcp inject`, before any write. AIWG_ROOT (the
544
+ // only literal env value any install target's fixed content can ever
545
+ // carry, and only on the built-in "aiwg" entry) is a named, narrow
546
+ // exemption - installPolicyCheckedServer/INSTALL_POLICY_EXEMPT_ENV - not
547
+ // an unexamined bypass: any other literal env/header is still refused.
548
+ const installRegistry = new McpServerRegistry();
549
+ assertCredentialPolicy(
550
+ Object.entries(config.content[installServerMapKey] || {})
551
+ .map(([name, server]) => installPolicyCheckedServer(name, server)),
552
+ resolveCredentialPolicy({
553
+ registryPolicy: await installRegistry.getCredentialPolicy(),
554
+ floorPolicy: await installRegistry.getCredentialPolicyFloor(),
555
+ floorSource: await installRegistry.getCredentialPolicyFloorSource(),
556
+ }),
557
+ );
558
+
559
+ // Only the claude target's install content can ever embed a literal credential
560
+ // (`AIWG_ROOT`, gated to `scope === 'user'` above, and exempted above); every
561
+ // other target's `configs[...]` entry above is a fixed `aiwg mcp serve`
562
+ // stdio/TOML stanza with no env/headers field, so there is nothing further
563
+ // for assertProjectCredentials to check there.
564
+ if (target === 'claude-code' && scope !== 'user') {
565
+ assertProjectCredentials(Object.entries(config.content.mcpServers).map(([name, server]) => ({ name, ...server })), config.path);
376
566
  }
377
567
 
378
568
  // Merge configuration using custom merge function if available
@@ -387,7 +577,13 @@ enabled_tools = [
387
577
  }
388
578
  };
389
579
 
390
- await fs.writeFile(config.path, JSON.stringify(merged, null, 2));
580
+ const migration = ['claude-code', 'claude-desktop'].includes(target)
581
+ ? await legacyClaudeMigration(projectDir) : [];
582
+ await writeConfigTransaction([
583
+ { file: config.path, content: JSON.stringify(merged, null, 2), options: { userScope, projectRoot } },
584
+ ...migration,
585
+ ]);
586
+ if (migration.length) console.log('Removed legacy AIWG MCP entry from .claude/settings.local.json');
391
587
  console.log(`MCP configuration written to: ${config.path}`);
392
588
  console.log(`\nTo use AIWG MCP server with ${target}:`);
393
589
  console.log(` 1. Restart ${target}`);
@@ -408,20 +604,22 @@ POSITIONING:
408
604
  MCP is optional and provider-agnostic. Baseline AIWG reachability is file
409
605
  deployment plus CLI discovery: aiwg discover / aiwg show <type> <name>.
410
606
 
411
- CORE TOOLS (15, always registered):
607
+ CORE TOOLS (17, always registered):
412
608
  discover Cross-type ranked catalog search
413
609
  skill-list / skill-show Skill catalog and SKILL.md body fetch
414
610
  command-list / command-show CLI command catalog and command spec fetch
611
+ rule-inventory Scoped rule activation and binding inventory
415
612
  rule-list / rule-show Rule catalog and rule body fetch
416
613
  agent-list / agent-show Agent catalog and agent definition fetch
417
614
  template-list / template-show Template catalog and raw template fetch
418
615
  template-render Render AIWG template with variables
419
616
  command-run Allow-listed CLI dispatch; confirmation-gated when needed
420
617
  artifact-read / artifact-write Project .aiwg/ artifact IO
618
+ ask-user Structured user question via elicitation or Markdown
421
619
 
422
- OPT-IN TOOLSETS (60 additional tools):
620
+ OPT-IN TOOLSETS (71 additional tools, including explicit workers):
423
621
  flows flow-list / flow-show / flow-run
424
- missions mission-guide / mission-dispatch / mission-status
622
+ missions mission-guide / mission-dispatch / mission-status / worker-resolve
425
623
  memory memory-* and reflections-* storage operations
426
624
  kb kb-* storage operations
427
625
  research provenance-* and research-store-* storage operations
@@ -431,10 +629,13 @@ OPT-IN TOOLSETS (60 additional tools):
431
629
  mc start / dispatch / status / stop / list
432
630
  ops status / list / use / push
433
631
  sandbox fleet inventory / mutation / reconciliation and governed activity
632
+ decision validation / evaluation / rules / features / calibration
633
+ workers worker-invoke (explicit opt-in; all excludes workers)
434
634
 
435
635
  Enable opt-in tools:
436
636
  AIWG_MCP_TOOLSETS=flows,missions,memory,kb,ralph,sandbox aiwg mcp serve
437
- aiwg mcp serve --toolsets=all
637
+ aiwg mcp serve --toolsets all
638
+ aiwg mcp serve --toolsets all,workers # explicitly enable worker-invoke
438
639
 
439
640
  RESOURCES:
440
641
  aiwg://prompts/catalog List of prompt templates
@@ -455,7 +656,7 @@ TRANSPORTS:
455
656
 
456
657
  ENVIRONMENT:
457
658
  AIWG_ROOT Path to AIWG installation (default: ~/.local/share/ai-writing-guide)
458
- AIWG_MCP_TOOLSETS Comma-separated opt-in toolsets; use all for every toolset
659
+ AIWG_MCP_TOOLSETS Comma-separated opt-in toolsets; all excludes workers
459
660
  AIWG_SANDBOX_MANAGEMENT_URL Sandbox management API origin (HTTPS except loopback)
460
661
  AIWG_SANDBOX_MANAGEMENT_TOKEN_FILE Mode-0600 management bearer file for sandbox tools
461
662
 
@@ -477,6 +678,16 @@ function parseFlag(args, flag) {
477
678
  return args[idx + 1];
478
679
  }
479
680
 
681
+ /** Skip option values when locating a server name, regardless of flag order. */
682
+ function serverName(args) {
683
+ const valueFlags = new Set(['--type', '--url', '--command', '--args', '--env',
684
+ '--headers', '--header-env', '--env-from', '--description']);
685
+ for (let i = 0; i < args.length; i++) {
686
+ if (valueFlags.has(args[i])) { i++; continue; }
687
+ if (!args[i].startsWith('--')) return args[i];
688
+ }
689
+ }
690
+
480
691
  /**
481
692
  * Parse comma-separated key=value pairs into an object
482
693
  */
@@ -495,8 +706,7 @@ function parseKVPairs(str) {
495
706
  * Handle `aiwg mcp add <name> [opts]`
496
707
  */
497
708
  async function handleAdd(args) {
498
- const positional = args.filter(a => !a.startsWith('--'));
499
- const name = positional[0];
709
+ const name = serverName(args);
500
710
 
501
711
  if (!name) {
502
712
  console.error('Usage: aiwg mcp add <name> --url <url> [--type http|stdio|sse] [--command <cmd>] [--args <a,b>]');
@@ -510,6 +720,7 @@ async function handleAdd(args) {
510
720
  const envStr = parseFlag(args, '--env');
511
721
  const headersStr = parseFlag(args, '--headers');
512
722
  const headerEnvStr = parseFlag(args, '--header-env');
723
+ const envFromStr = parseFlag(args, '--env-from');
513
724
  const description = parseFlag(args, '--description');
514
725
 
515
726
  if (type === 'stdio' && !command) {
@@ -531,13 +742,17 @@ async function handleAdd(args) {
531
742
  env: parseKVPairs(envStr),
532
743
  headers: parseKVPairs(headersStr),
533
744
  headerEnv: parseKVPairs(headerEnvStr),
745
+ envFrom: parseKVPairs(envFromStr),
534
746
  description,
535
747
  });
536
748
 
537
749
  console.log(`Added MCP server: ${name}`);
538
- if (url) console.log(` URL: ${url}`);
539
- if (command) console.log(` Command: ${command}`);
750
+ if (url) console.log(` URL: ${redactUrlUserinfo(url)}`);
751
+ if (command) console.log(` Command: ${redactCredentialText(command)}`);
540
752
  console.log(` Type: ${type}`);
753
+ for (const [key, values] of [['env', parseKVPairs(envStr)], ['headers', parseKVPairs(headersStr)]]) {
754
+ if (values) console.log(` ${key}: ${Object.keys(values).join(', ')}`);
755
+ }
541
756
  console.log(`\nUse "aiwg mcp inject --provider <name>" to inject into a provider config.`);
542
757
  }
543
758
 
@@ -562,8 +777,7 @@ async function handleRemove(args) {
562
777
  * Handle `aiwg mcp update <name> [opts]`
563
778
  */
564
779
  async function handleUpdate(args) {
565
- const positional = args.filter(a => !a.startsWith('--'));
566
- const name = positional[0];
780
+ const name = serverName(args);
567
781
 
568
782
  if (!name) {
569
783
  console.error('Usage: aiwg mcp update <name> --url <url> [--type <type>] ...');
@@ -578,6 +792,7 @@ async function handleUpdate(args) {
578
792
  const envStr = parseFlag(args, '--env');
579
793
  const headersStr = parseFlag(args, '--headers');
580
794
  const headerEnvStr = parseFlag(args, '--header-env');
795
+ const envFromStr = parseFlag(args, '--env-from');
581
796
  const description = parseFlag(args, '--description');
582
797
 
583
798
  if (url !== undefined) updates.url = url;
@@ -587,6 +802,7 @@ async function handleUpdate(args) {
587
802
  if (envStr !== undefined) updates.env = parseKVPairs(envStr);
588
803
  if (headersStr !== undefined) updates.headers = parseKVPairs(headersStr);
589
804
  if (headerEnvStr !== undefined) updates.headerEnv = parseKVPairs(headerEnvStr);
805
+ if (envFromStr !== undefined) updates.envFrom = parseKVPairs(envFromStr);
590
806
  if (description !== undefined) updates.description = description;
591
807
 
592
808
  if (Object.keys(updates).length === 0) {
@@ -598,17 +814,24 @@ async function handleUpdate(args) {
598
814
  await registry.update(name, updates);
599
815
  console.log(`Updated MCP server: ${name}`);
600
816
  for (const [key, value] of Object.entries(updates)) {
601
- console.log(` ${key}: ${typeof value === 'object' ? JSON.stringify(value) : value}`);
817
+ const display = key === 'url' ? redactUrlUserinfo(value)
818
+ : key === 'args' ? JSON.stringify(redactCredentialArgs(value))
819
+ : ['env', 'headers', 'headerEnv', 'envFrom'].includes(key) ? Object.keys(value || {}).join(', ')
820
+ : typeof value === 'object' ? JSON.stringify(redactCredentialRecord(value)) : redactCredentialText(value);
821
+ console.log(` ${key}: ${display}`);
602
822
  }
603
823
  console.log(`\nRe-run "aiwg mcp inject --all" to propagate changes to provider configs.`);
604
824
  }
605
825
 
826
+ // Refusal and all display paths share the scanner in credentials.mjs.
827
+ const redactUrlUserinfo = redactCredentialText;
828
+
606
829
  /**
607
830
  * Handle `aiwg mcp list` (managed servers from registry)
608
831
  */
609
832
  async function handleList() {
610
833
  const registry = new McpServerRegistry();
611
- const servers = await registry.list();
834
+ const servers = redactCredentialRecord(await registry.list());
612
835
 
613
836
  if (servers.length === 0) {
614
837
  console.log('No MCP servers registered.');
@@ -621,13 +844,17 @@ async function handleList() {
621
844
  for (const server of servers) {
622
845
  console.log(` ${server.name}`);
623
846
  console.log(` Type: ${server.type}`);
624
- if (server.url) console.log(` URL: ${server.url}`);
625
- if (server.command) console.log(` Command: ${server.command}${server.args ? ' ' + server.args.join(' ') : ''}`);
847
+ if (server.url) console.log(` URL: ${redactUrlUserinfo(server.url)}`);
848
+ if (server.command) console.log(` Command: ${server.command}${server.args ? ' ' + redactCredentialArgs(server.args).join(' ') : ''}`);
626
849
  if (server.headerEnv) {
627
850
  const refs = Object.entries(server.headerEnv).map(([header, envName]) => `${header}←${envName}`);
628
851
  console.log(` Credential refs: ${refs.join(', ')}`);
629
852
  }
630
- if (server.description) console.log(` Description: ${server.description}`);
853
+ if (server.envFrom) {
854
+ const refs = Object.entries(server.envFrom).map(([key, envName]) => `${key}←${envName}`);
855
+ console.log(` Env refs: ${refs.join(', ')}`);
856
+ }
857
+ if (server.description) console.log(` Description: ${redactCredentialText(server.description)}`);
631
858
  if (server.injectedProviders && server.injectedProviders.length > 0) {
632
859
  console.log(` Injected into: ${server.injectedProviders.join(', ')}`);
633
860
  }
@@ -660,6 +887,12 @@ async function handleInject(args) {
660
887
  const profileName = parseFlag(args, '--profile');
661
888
  const ephemeral = args.includes('--ephemeral');
662
889
  const outPath = parseFlag(args, '--out');
890
+ const strictCredentials = args.includes('--strict-credentials');
891
+ const noCredentials = args.includes('--no-credentials');
892
+ if (strictCredentials && noCredentials) {
893
+ console.error('Error: --strict-credentials and --no-credentials are mutually exclusive.');
894
+ process.exit(1);
895
+ }
663
896
 
664
897
  if (!provider && !injectAll) {
665
898
  console.error('Usage: aiwg mcp inject --provider <name> [--profile <p>] [--ephemeral] [--servers a,b] [--dry-run]');
@@ -669,12 +902,19 @@ async function handleInject(args) {
669
902
  }
670
903
 
671
904
  const registry = new McpServerRegistry();
905
+ const credentialPolicy = resolveCredentialPolicy({
906
+ flag: noCredentials ? 'none' : strictCredentials ? 'references' : undefined,
907
+ registryPolicy: await registry.getCredentialPolicy(),
908
+ floorPolicy: await registry.getCredentialPolicyFloor(),
909
+ floorSource: await registry.getCredentialPolicyFloorSource(),
910
+ });
672
911
 
673
912
  // Resolve server filter: --profile takes precedence over --servers
674
913
  let serverFilter;
914
+ let profile;
675
915
  if (profileName) {
676
916
  const profiles = new McpProfileRegistry();
677
- const profile = await profiles.get(profileName);
917
+ profile = await profiles.resolve(profileName);
678
918
  if (!profile) {
679
919
  const all = await profiles.list();
680
920
  console.error(`Profile "${profileName}" not found.`);
@@ -707,6 +947,20 @@ async function handleInject(args) {
707
947
  providers = [normalized];
708
948
  }
709
949
 
950
+ if (injectAll && !ephemeral) {
951
+ const selected = (await registry.list()).filter(server => !serverFilter || serverFilter.includes(server.name));
952
+ for (const p of providers) {
953
+ const configPath = getProviderConfigPath(p, projectDir, { scope });
954
+ const { projectRoot } = classifyMcpDestination(p, scope, configPath, projectDir);
955
+ await assertConfigDestination(configPath, projectRoot);
956
+ // Every destination inside the project directory is meant to be committed
957
+ // to version control; home destinations are private.
958
+ if (projectRoot) {
959
+ assertProjectCredentials(selected, configPath, projectCredentialRemedy(p, projectDir));
960
+ }
961
+ }
962
+ }
963
+
710
964
  if (ephemeral) {
711
965
  for (const p of providers) {
712
966
  const mcpDefinition = getMcpInjectionDefinition(p);
@@ -723,23 +977,27 @@ async function handleInject(args) {
723
977
 
724
978
  let totalInjected = 0;
725
979
 
980
+ const printWarnings = (warnings = []) => {
981
+ for (const warning of warnings) console.error(` WARNING ${warning}`);
982
+ };
983
+
726
984
  for (const p of providers) {
985
+ const toolFilters = profile ? resolveToolFilters(profile, p) : undefined;
727
986
  if (ephemeral) {
728
987
  // Generate a standalone ephemeral config file
729
- const targetPath = outPath ?? path.join(
730
- process.env.TMPDIR || '/tmp',
731
- `aiwg-mcp-${profileName ?? 'custom'}-${p}-${Date.now()}.json`,
732
- );
733
-
734
988
  const allServers = await registry.list();
735
989
  const servers = serverFilter
736
990
  ? allServers.filter(s => serverFilter.includes(s.name))
737
991
  : allServers;
992
+ const toolPlan = hasToolFilters(toolFilters)
993
+ ? planToolFilters(p, servers.map(server => server.name), toolFilters)
994
+ : null;
738
995
 
739
996
  if (servers.length === 0) {
740
997
  console.error(` ${p}: no servers to write`);
741
998
  continue;
742
999
  }
1000
+ assertCredentialPolicy(servers, credentialPolicy);
743
1001
 
744
1002
  // Build ephemeral config in provider's format
745
1003
  const mcpDefinition = getMcpInjectionDefinition(p);
@@ -752,32 +1010,59 @@ async function handleInject(args) {
752
1010
  console.log(` Use "aiwg session --provider codex --profile ${profileName}" instead.`);
753
1011
  continue;
754
1012
  }
755
- const cfg = {};
756
- if (server.type === 'stdio') {
757
- cfg.command = server.command;
758
- cfg.args = server.args || [];
759
- if (server.env) cfg.env = server.env;
760
- } else {
761
- cfg.url = server.url;
762
- if (server.headers) cfg.headers = server.headers;
763
- }
764
- mcpBlock[server.name] = cfg;
1013
+ mcpBlock[server.name] = buildServerConfig(server, p);
765
1014
  }
766
1015
 
767
1016
  if (Object.keys(mcpBlock).length === 0) continue;
768
1017
 
769
1018
  const config = { [mcpKey]: mcpBlock };
1019
+ if (toolPlan) applyJsonToolFilterPlan(config, mcpKey, toolPlan);
1020
+ const tempDir = outPath || dryRun
1021
+ ? undefined
1022
+ : await fs.mkdtemp(path.join(os.tmpdir(), 'aiwg-mcp-'));
1023
+ const targetPath = outPath ?? path.join(
1024
+ tempDir ?? path.join(os.tmpdir(), 'aiwg-mcp-<random>'),
1025
+ `${profileName ?? 'custom'}-${p}.json`,
1026
+ );
1027
+
1028
+ // Claude Code reads permission rules from settings, not from --mcp-config.
1029
+ const settingsPath = toolPlan?.claudePermissions ? targetPath.replace(/(\.json)?$/, '.settings.json') : null;
1030
+ // Explicit --out paths may be outside the project. Check every parent from
1031
+ // the filesystem root so neither destination can traverse a symlink.
1032
+ const outputRoot = path.parse(path.resolve(targetPath)).root;
1033
+ await assertConfigDestination(targetPath, outputRoot);
1034
+ const realProject = await fs.realpath(path.resolve(projectDir)).catch(error => {
1035
+ if (error.code !== 'ENOENT') throw error;
1036
+ return path.resolve(projectDir);
1037
+ });
1038
+ if (isPathInsideDir(targetPath, projectDir) || isPathInsideDir(targetPath, realProject)) {
1039
+ assertProjectCredentials(servers, targetPath, projectCredentialRemedy(p, projectDir));
1040
+ }
1041
+ const preparedPermissions = settingsPath
1042
+ ? await prepareClaudePermissions(settingsPath, toolPlan.claudePermissions, {
1043
+ projectRoot: outputRoot,
1044
+ userScope: true,
1045
+ managedDir: path.dirname(registry.getPath()),
1046
+ sidecar: true,
1047
+ mcpPath: targetPath,
1048
+ })
1049
+ : null;
770
1050
 
771
1051
  if (!dryRun) {
772
- await fs.mkdir(path.dirname(targetPath), { recursive: true });
773
- await fs.writeFile(targetPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
1052
+ await writeConfigTransaction([
1053
+ { file: targetPath, content: JSON.stringify(config, null, 2) + '\n',
1054
+ options: { userScope: true, projectRoot: outputRoot } },
1055
+ ...(preparedPermissions?.writes || []),
1056
+ ]);
774
1057
  }
775
1058
 
776
1059
  const prefix = dryRun ? '[DRY RUN] ' : '';
777
1060
  console.log(`${prefix}${p}: ${targetPath}`);
778
1061
  console.log(` ${prefix}Servers: ${Object.keys(mcpBlock).join(', ')}`);
1062
+ if (settingsPath) console.log(` ${prefix}Tool permissions: ${settingsPath}`);
1063
+ printWarnings([...(toolPlan?.warnings || []), ...(preparedPermissions?.warnings || [])]);
779
1064
  if (!dryRun && (p === 'claude-code' || p === 'claude')) {
780
- console.log(` Launch with: claude --mcp-config ${targetPath}`);
1065
+ console.log(` Launch with: claude --mcp-config ${targetPath}${settingsPath ? ` --settings ${settingsPath}` : ''}`);
781
1066
  }
782
1067
  totalInjected += Object.keys(mcpBlock).length;
783
1068
  continue;
@@ -789,6 +1074,8 @@ async function handleInject(args) {
789
1074
  projectDir,
790
1075
  dryRun,
791
1076
  scope,
1077
+ ...(hasToolFilters(toolFilters) ? { toolFilters } : {}),
1078
+ credentialPolicy,
792
1079
  });
793
1080
 
794
1081
  if (result.error) {
@@ -806,6 +1093,8 @@ async function handleInject(args) {
806
1093
  if (result.alreadyPresent.length > 0) {
807
1094
  console.log(` ${prefix}Updated in place: ${result.alreadyPresent.join(', ')}`);
808
1095
  }
1096
+ if (result.settingsPath) console.log(` ${prefix}Tool permissions: ${result.settingsPath}`);
1097
+ printWarnings(result.warnings);
809
1098
  }
810
1099
 
811
1100
  if (!dryRun && totalInjected > 0 && !ephemeral) {
@@ -825,14 +1114,18 @@ function printProfileUsage() {
825
1114
  aiwg mcp profile — MCP server profiles (named server subsets)
826
1115
 
827
1116
  Usage:
828
- aiwg mcp profile add <name> --servers a,b,c [--description "..."]
1117
+ aiwg mcp profile add <name> --servers a,b,c [--extends base,...] [--description "..."]
1118
+ [--provider <p|*>] [--tool-deny s__t,...] [--tool-allow s__t,...]
829
1119
  aiwg mcp profile list
830
- aiwg mcp profile show <name>
1120
+ aiwg mcp profile show <name> [--json]
831
1121
  aiwg mcp profile edit <name> [--add-server x] [--remove-server y] [--description "..."]
1122
+ [--provider <p|*>] [--tool-deny s__t,...] [--tool-allow s__t,...]
1123
+ [--clear-tool-filters]
832
1124
  aiwg mcp profile remove <name>
833
1125
  aiwg mcp profile import <file>
834
1126
  aiwg mcp profile export <name> [--out <file>]
835
1127
  aiwg mcp profile init-presets
1128
+ aiwg mcp profile login <name> [--api-key-stdin | --api-key-file <path> | --api-key-env <ENV_VAR_NAME>]
836
1129
 
837
1130
  Profiles let you define named subsets of your registered MCP servers:
838
1131
  aiwg mcp profile add dev --servers git-gitea,memory-fortemi --description "Dev work"
@@ -840,11 +1133,60 @@ Profiles let you define named subsets of your registered MCP servers:
840
1133
  aiwg mcp inject --provider claude --profile dev --ephemeral
841
1134
  aiwg session --provider claude --profile dev
842
1135
 
1136
+ Layered configuration (organisation base, identity overlay):
1137
+ AIWG_CONFIG_LAYERS=/etc/aiwg/org:/home/me/.aiwg/acme aiwg mcp inject \
1138
+ --provider claude --profile acme-dev --ephemeral --out /tmp/acme.json
1139
+ Directories are listed lowest precedence first (path delimiter separated).
1140
+ A server or profile in a later layer replaces one of the same name in an
1141
+ earlier layer. Writes go to the last layer. --extends lets a profile inherit
1142
+ another profile's servers and tool filters from any layer.
1143
+
1144
+ Tool filters name tools as <server>__<tool>; <tool> may contain *. --provider
1145
+ defaults to *, which applies to every provider. inject renders them into each
1146
+ provider's own setting and warns about any filter the provider cannot express:
1147
+ aiwg mcp profile edit dev --tool-deny git-gitea__delete_repo
1148
+ aiwg mcp profile edit dev --provider codex --tool-allow git-gitea__list_repos
1149
+
843
1150
  Preset profiles (minimal, dev, ops, research, incident, full):
844
1151
  aiwg mcp profile init-presets
1152
+
1153
+ Logging in to a Codex profile's isolated runtime home (#3062):
1154
+ aiwg mcp profile login ops interactive (requires a terminal)
1155
+ vault read -field=key secret/openai |
1156
+ aiwg mcp profile login ops --api-key-stdin headless, most preferred — key never
1157
+ touches an environment variable or argv
1158
+ aiwg mcp profile login ops --api-key-file ~/.openai-key
1159
+ headless — file must be owned by you,
1160
+ mode 600, not a symlink
1161
+ aiwg mcp profile login ops --api-key-env OPENAI_KEY
1162
+ headless, least preferred — reads
1163
+ $OPENAI_KEY; visible to every other
1164
+ child process this shell starts
1165
+ In every case the key reaches codex over stdin only; it never appears on
1166
+ argv, in logs, or in error messages. Exactly one of the three may be
1167
+ given. Without a terminal and without any of the three, this errors
1168
+ naming all three. See "Supplying credentials safely" in the docs:
1169
+ docs/mcp/codex-profiles.md#supplying-credentials-safely
845
1170
  `);
846
1171
  }
847
1172
 
1173
+ /**
1174
+ * Parse --provider / --tool-deny / --tool-allow into a providerOverrides map.
1175
+ */
1176
+ function parseToolFilterFlags(args) {
1177
+ const deny = parseFlag(args, '--tool-deny');
1178
+ const allow = parseFlag(args, '--tool-allow');
1179
+ if (!deny && !allow) return undefined;
1180
+ const split = value => value.split(',').map(s => s.trim()).filter(Boolean);
1181
+ const provider = parseFlag(args, '--provider') || '*';
1182
+ return {
1183
+ [provider]: {
1184
+ ...(deny ? { toolDeny: split(deny) } : {}),
1185
+ ...(allow ? { toolAllow: split(allow) } : {}),
1186
+ },
1187
+ };
1188
+ }
1189
+
848
1190
  /**
849
1191
  * Handle `aiwg mcp profile add <name> [opts]`
850
1192
  */
@@ -858,17 +1200,26 @@ async function handleProfileAdd(args) {
858
1200
 
859
1201
  const serversStr = parseFlag(args, '--servers');
860
1202
  const description = parseFlag(args, '--description');
1203
+ const extendsStr = parseFlag(args, '--extends');
1204
+ const bases = extendsStr ? extendsStr.split(',').map(s => s.trim()).filter(Boolean) : undefined;
861
1205
 
862
- if (!serversStr && name !== 'minimal') {
1206
+ if (!serversStr && !bases && name !== 'minimal') {
863
1207
  console.error('Warning: no --servers specified. Profile will start empty.');
864
1208
  }
865
1209
 
866
1210
  const servers = serversStr ? serversStr.split(',').map(s => s.trim()).filter(Boolean) : [];
1211
+ const providerOverrides = parseToolFilterFlags(args);
867
1212
 
868
1213
  const profiles = new McpProfileRegistry();
869
1214
  const registry = new McpServerRegistry();
870
1215
 
871
- await profiles.add({ name, description, servers }, registry);
1216
+ await profiles.add({
1217
+ name,
1218
+ description,
1219
+ servers,
1220
+ ...(bases ? { extends: bases } : {}),
1221
+ ...(providerOverrides ? { providerOverrides } : {}),
1222
+ }, registry);
872
1223
 
873
1224
  console.log(`Profile added: ${name}`);
874
1225
  if (description) console.log(` Description: ${description}`);
@@ -881,7 +1232,7 @@ async function handleProfileAdd(args) {
881
1232
  */
882
1233
  async function handleProfileList() {
883
1234
  const profiles = new McpProfileRegistry();
884
- const all = await profiles.list();
1235
+ const all = redactCredentialRecord(await profiles.list());
885
1236
 
886
1237
  if (all.length === 0) {
887
1238
  console.log('No profiles defined.');
@@ -911,7 +1262,7 @@ async function handleProfileShow(args) {
911
1262
  }
912
1263
 
913
1264
  const profiles = new McpProfileRegistry();
914
- const profile = await profiles.get(name);
1265
+ const profile = redactCredentialRecord(await profiles.get(name));
915
1266
 
916
1267
  if (!profile) {
917
1268
  const all = await profiles.list();
@@ -922,8 +1273,18 @@ async function handleProfileShow(args) {
922
1273
  process.exit(1);
923
1274
  }
924
1275
 
1276
+ if (args.includes('--json')) {
1277
+ console.log(JSON.stringify(redactCredentialRecord(profile), null, 2));
1278
+ return;
1279
+ }
1280
+
925
1281
  console.log(`Profile: ${profile.name}`);
926
1282
  if (profile.description) console.log(`Description: ${profile.description}`);
1283
+ if (profile.extends?.length) {
1284
+ console.log(`Extends: ${profile.extends.join(', ')}`);
1285
+ const resolved = await profiles.resolve(name);
1286
+ console.log(`Resolved servers: ${resolved.servers.join(', ') || '(none)'}`);
1287
+ }
927
1288
  console.log(`\nServers (${profile.servers.length}):`);
928
1289
 
929
1290
  if (profile.servers.length === 0) {
@@ -934,11 +1295,11 @@ async function handleProfileShow(args) {
934
1295
  // Resolve server configs
935
1296
  const registry = new McpServerRegistry();
936
1297
  for (const serverName of profile.servers) {
937
- const server = await registry.get(serverName);
1298
+ const server = redactCredentialRecord(await registry.get(serverName));
938
1299
  if (server) {
939
1300
  const detail = server.type === 'stdio'
940
- ? `stdio ${server.command}${server.args ? ' ' + server.args.join(' ') : ''}`
941
- : `${server.type} ${server.url}`;
1301
+ ? `stdio ${server.command}${server.args ? ' ' + redactCredentialArgs(server.args).join(' ') : ''}`
1302
+ : `${server.type} ${redactUrlUserinfo(server.url)}`;
942
1303
  console.log(` ${serverName.padEnd(24)} ${detail}`);
943
1304
  if (server.description) console.log(` ${''.padEnd(24)} ${server.description}`);
944
1305
  } else {
@@ -975,14 +1336,18 @@ async function handleProfileEdit(args) {
975
1336
  const removeServer = parseFlag(args, '--remove-server');
976
1337
  const description = parseFlag(args, '--description');
977
1338
 
1339
+ const providerOverrides = parseToolFilterFlags(args);
1340
+ const clearToolFilters = args.includes('--clear-tool-filters');
978
1341
  const changes = {
979
1342
  description,
980
1343
  addServers: addServer ? addServer.split(',').map(s => s.trim()) : undefined,
981
1344
  removeServers: removeServer ? removeServer.split(',').map(s => s.trim()) : undefined,
1345
+ providerOverrides,
1346
+ clearToolFilters: clearToolFilters ? (parseFlag(args, '--provider') || '*') : undefined,
982
1347
  };
983
1348
 
984
- if (!description && !addServer && !removeServer) {
985
- console.error('No changes specified. Use --add-server, --remove-server, or --description.');
1349
+ if (!description && !addServer && !removeServer && !providerOverrides && !clearToolFilters) {
1350
+ console.error('No changes specified. Use --add-server, --remove-server, --description, --tool-deny, --tool-allow or --clear-tool-filters.');
986
1351
  process.exit(1);
987
1352
  }
988
1353
 
@@ -1055,6 +1420,135 @@ async function handleProfileInitPresets() {
1055
1420
  }
1056
1421
  }
1057
1422
 
1423
+ /**
1424
+ * Read all of stdin and return it as one trimmed string (go/no-go review,
1425
+ * blocker 1C's `--api-key-stdin`). Mirrors `src/cli/handlers/decision.ts`'s
1426
+ * own private `readStdin()` + `.trim()` for `--token-stdin` — duplicated
1427
+ * rather than imported for the same source/dist reason documented on
1428
+ * `requireFlagValue` above (this file runs both compiled and directly from
1429
+ * source). Input is expected to be a single line; a value containing
1430
+ * additional lines is not specially rejected, matching that precedent.
1431
+ */
1432
+ async function readStdinAll() {
1433
+ const chunks = [];
1434
+ for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
1435
+ return Buffer.concat(chunks).toString('utf8');
1436
+ }
1437
+
1438
+ /**
1439
+ * Handle `aiwg mcp profile login <name> [--api-key-env <ENV_VAR_NAME> |
1440
+ * --api-key-stdin | --api-key-file <path>]` (#3062, #3069, go/no-go review
1441
+ * blocker 1C).
1442
+ *
1443
+ * Wires `loginInProfile()` (src/mcp/adapters/codex-runtime.ts), which
1444
+ * already implements the full headless-safe behavior this command needs:
1445
+ * with a terminal attached and none of the three given, it runs the
1446
+ * interactive `codex login` OAuth flow. Exactly one credential source may
1447
+ * be given; more than one is a usage error naming the flags given. In
1448
+ * every case the key reaches `loginInProfile()` only as an in-memory value
1449
+ * (or, for `--api-key-env`, a variable *name*) — never on argv, in logs, or
1450
+ * in an error message — and travels to `codex` only on its stdin (see
1451
+ * `spawnCodexLoginWithApiKey()`'s own doc comment). Without a terminal and
1452
+ * without any of the three, `loginInProfile()` itself refuses, naming all
1453
+ * three flags.
1454
+ *
1455
+ * `--api-key-env` reads the key from that named environment variable, as
1456
+ * before. `--api-key-stdin` reads one line from this process's own stdin
1457
+ * (refusing if stdin is a terminal, or if it is empty) — the least
1458
+ * observable source, since the key then never exists in any environment or
1459
+ * on any command line. `--api-key-file <path>` reads the key from a file
1460
+ * that `readApiKeyFromFile()` requires to be a non-symlink, single-hard-
1461
+ * link, regular file owned by the current user with no group/other
1462
+ * permission bits on POSIX — refused otherwise, without ever printing the
1463
+ * path's contents. `--api-key-env` remains the least preferred of the
1464
+ * three (see "Supplying credentials safely" in the docs): the named
1465
+ * variable is visible to every other child process this shell starts.
1466
+ */
1467
+ async function handleProfileLogin(args) {
1468
+ let apiKeyEnv;
1469
+ let apiKeyFilePath;
1470
+ try {
1471
+ // rejectEmpty: an explicitly-given-but-empty variable name/path can
1472
+ // never name a real environment variable or file (#3062 second review,
1473
+ // finding 4; go/no-go review blocker 1C).
1474
+ apiKeyEnv = requireFlagValue(args, { rejectEmpty: true }, '--api-key-env');
1475
+ apiKeyFilePath = requireFlagValue(args, { rejectEmpty: true }, '--api-key-file');
1476
+ } catch (err) {
1477
+ if (err instanceof MissingFlagValueError) {
1478
+ console.error(`${err.message}.`);
1479
+ process.exit(2);
1480
+ }
1481
+ throw err;
1482
+ }
1483
+ const useStdin = args.includes('--api-key-stdin');
1484
+
1485
+ const sourcesGiven = [
1486
+ apiKeyEnv !== undefined ? '--api-key-env' : null,
1487
+ useStdin ? '--api-key-stdin' : null,
1488
+ apiKeyFilePath !== undefined ? '--api-key-file' : null,
1489
+ ].filter(Boolean);
1490
+ if (sourcesGiven.length > 1) {
1491
+ console.error(`Only one credential source may be given; got ${sourcesGiven.join(', ')}.`);
1492
+ process.exit(2);
1493
+ }
1494
+
1495
+ // Skip --api-key-env's/--api-key-file's own value when hunting for the
1496
+ // positional profile name, so e.g. `login --api-key-env FOO` (missing
1497
+ // name) never misreads FOO as the name (mirrors this file's own
1498
+ // `serverName()` helper). Stops at a bare `--` the same way
1499
+ // requireFlagValue() does.
1500
+ const positional = [];
1501
+ for (let i = 0; i < args.length; i += 1) {
1502
+ const token = args[i];
1503
+ if (token === '--') break;
1504
+ if (token === '--api-key-env' || token === '--api-key-file') { i += 1; continue; }
1505
+ if (!token.startsWith('--')) positional.push(token);
1506
+ }
1507
+ const name = positional[0];
1508
+ if (!name) {
1509
+ console.error('Usage: aiwg mcp profile login <name> [--api-key-env <ENV_VAR_NAME> | --api-key-stdin | --api-key-file <path>]');
1510
+ process.exit(1);
1511
+ }
1512
+
1513
+ let apiKey;
1514
+ if (useStdin) {
1515
+ if (process.stdin.isTTY) {
1516
+ console.error(
1517
+ '--api-key-stdin requires piped input, not a terminal; run ' +
1518
+ `\`aiwg mcp profile login ${name}\` with no credential flag for the interactive login instead.`,
1519
+ );
1520
+ process.exit(2);
1521
+ }
1522
+ apiKey = (await readStdinAll()).trim();
1523
+ if (!apiKey) {
1524
+ console.error('--api-key-stdin received no key (stdin was empty).');
1525
+ process.exit(2);
1526
+ }
1527
+ } else if (apiKeyFilePath !== undefined) {
1528
+ const { readApiKeyFromFile } = await loadCodexRuntimeAdapter();
1529
+ try {
1530
+ apiKey = await readApiKeyFromFile(apiKeyFilePath);
1531
+ } catch (err) {
1532
+ console.error(err instanceof Error ? err.message : String(err));
1533
+ process.exit(2);
1534
+ }
1535
+ if (!apiKey) {
1536
+ console.error(`--api-key-file "${apiKeyFilePath}" is empty.`);
1537
+ process.exit(2);
1538
+ }
1539
+ }
1540
+
1541
+ try {
1542
+ const { loginInProfile } = await loadCodexRuntimeAdapter();
1543
+ const loginOptions = apiKey !== undefined ? { apiKey } : apiKeyEnv ? { apiKeyEnv } : {};
1544
+ await loginInProfile(name, loginOptions);
1545
+ console.log(`\nLogged in to profile "${name}".`);
1546
+ } catch (err) {
1547
+ console.error(err instanceof Error ? err.message : String(err));
1548
+ process.exit(1);
1549
+ }
1550
+ }
1551
+
1058
1552
  /**
1059
1553
  * Route `aiwg mcp profile <subcommand>`
1060
1554
  */
@@ -1080,6 +1574,9 @@ async function handleProfile(args) {
1080
1574
  case 'rm':
1081
1575
  await handleProfileRemove(subArgs);
1082
1576
  break;
1577
+ case 'login':
1578
+ await handleProfileLogin(subArgs);
1579
+ break;
1083
1580
  case 'import':
1084
1581
  await handleProfileImport(subArgs);
1085
1582
  break;
@@ -1137,14 +1634,25 @@ export async function main(args = process.argv.slice(2)) {
1137
1634
  if (!['project', 'user'].includes(scope)) throw new Error('Scope must be project or user');
1138
1635
  const projectDir = parseFlag(args, '--project') || (args[2] && !args[2].startsWith('--') ? args[2] : '.');
1139
1636
  const configPath = getProviderConfigPath('omp', projectDir, { scope });
1140
- const result = await manageOmpMcp(configPath, [{ name: 'aiwg', type: 'stdio', command: 'aiwg', args: ['mcp', 'serve'] }], { dryRun: args.includes('--dry-run') });
1637
+ // Destination-derived, not scope-derived: a provider home-directory
1638
+ // override (e.g. PI_CODING_AGENT_DIR) that still resolves inside the
1639
+ // project must get project containment, whatever scope was requested.
1640
+ const { userScope, projectRoot } = classifyMcpDestination('omp', scope, configPath, projectDir);
1641
+ await assertConfigDestination(configPath, projectRoot);
1642
+ const result = await manageOmpMcp(configPath, [{ name: 'aiwg', type: 'stdio', command: 'aiwg', args: ['mcp', 'serve'] }], { dryRun: args.includes('--dry-run'), userScope, projectRoot });
1141
1643
  console.log(JSON.stringify(result, null, 2));
1142
1644
  break;
1143
1645
  }
1144
1646
  // Parse install arguments (skip flags)
1145
- const installArgs = args.slice(1).filter(a => !a.startsWith('--'));
1146
- const target = installArgs[0] || 'claude';
1147
- const projectDir = installArgs[1] || '.';
1647
+ const scope = parseFlag(args, '--scope') || 'project';
1648
+ if (!['project', 'user'].includes(scope)) throw new Error('Scope must be project or user');
1649
+ const installArgs = args.slice(1).filter((a, index, rest) => !a.startsWith('--') && !['--scope', '--project'].includes(rest[index - 1]));
1650
+ const requestedTarget = installArgs[0] || 'claude';
1651
+ const target = resolveClaudeInstallTarget(requestedTarget, scope);
1652
+ if (requestedTarget === 'claude') {
1653
+ console.warn(`Deprecated MCP install target "claude" selected ${target}; use "aiwg mcp install ${target}" explicitly.`);
1654
+ }
1655
+ const projectDir = parseFlag(args, '--project') || installArgs[1] || '.';
1148
1656
 
1149
1657
  // Check for --dry-run flag
1150
1658
  if (args.includes('--dry-run')) {
@@ -1152,13 +1660,14 @@ export async function main(args = process.argv.slice(2)) {
1152
1660
  console.log(`[DRY RUN] Would generate MCP config for: ${target}`);
1153
1661
  console.log(`[DRY RUN] Target directory: ${projectDir}`);
1154
1662
  const configPaths = {
1155
- claude: '.claude/settings.local.json',
1663
+ 'claude-desktop': claudeDesktopConfigPath(),
1664
+ 'claude-code': scope === 'user' ? path.join(homeDir, '.claude.json') : path.join(projectDir, '.mcp.json'),
1156
1665
  cursor: '.cursor/mcp.json',
1157
1666
  factory: (projectDir === '.' || projectDir === 'global')
1158
1667
  ? path.join(homeDir, '.factory/mcp.json')
1159
1668
  : path.join(projectDir, '.factory/mcp.json'),
1160
- codex: path.join(homeDir, '.codex/config.toml'),
1161
- openai: path.join(homeDir, '.codex/config.toml'),
1669
+ codex: path.resolve(process.env.CODEX_HOME || path.join(homeDir, '.codex'), 'config.toml'),
1670
+ openai: path.resolve(process.env.CODEX_HOME || path.join(homeDir, '.codex'), 'config.toml'),
1162
1671
  vscode: '.vscode/mcp.json',
1163
1672
  copilot: '.vscode/mcp.json',
1164
1673
  opencode: (projectDir === '.' || projectDir === 'global')
@@ -1171,7 +1680,7 @@ export async function main(args = process.argv.slice(2)) {
1171
1680
  break;
1172
1681
  }
1173
1682
 
1174
- await generateConfig(target, projectDir);
1683
+ if (!await generateConfig(target, projectDir, scope)) process.exitCode = 1;
1175
1684
  break;
1176
1685
  }
1177
1686
 
@@ -1208,9 +1717,9 @@ export async function main(args = process.argv.slice(2)) {
1208
1717
  if (!['project', 'user'].includes(scope)) throw new Error('Scope must be project or user');
1209
1718
  const remove = (parseFlag(subArgs, '--servers') || '').split(',').map(s => s.trim()).filter(Boolean);
1210
1719
  if (!remove.length) throw new Error('uninject requires --servers name[,name]');
1211
- const configPath = getProviderConfigPath(provider, parseFlag(subArgs, '--project') || '.', { scope });
1720
+ const projectDir = parseFlag(subArgs, '--project') || '.';
1721
+ const configPath = getProviderConfigPath(provider, projectDir, { scope });
1212
1722
  if (provider === 'grok-build') {
1213
- const projectDir = parseFlag(subArgs, '--project') || '.';
1214
1723
  const result = await unmanageGrokBuildMcp(configPath, remove, {
1215
1724
  dryRun: subArgs.includes('--dry-run'),
1216
1725
  root: scope === 'user' ? path.dirname(path.dirname(configPath)) : path.resolve(projectDir),
@@ -1218,7 +1727,17 @@ export async function main(args = process.argv.slice(2)) {
1218
1727
  console.log(JSON.stringify(result, null, 2));
1219
1728
  break;
1220
1729
  }
1221
- const result = await manageOmpMcp(configPath, [], { remove, dryRun: subArgs.includes('--dry-run') });
1730
+ // Destination-derived, not scope-derived: a provider home-directory
1731
+ // override that still resolves inside the project must get project
1732
+ // containment, whatever scope was requested.
1733
+ const { userScope: ompUserScope, projectRoot: ompProjectRoot } =
1734
+ classifyMcpDestination(provider, scope, configPath, projectDir);
1735
+ const result = await manageOmpMcp(configPath, [], {
1736
+ remove,
1737
+ dryRun: subArgs.includes('--dry-run'),
1738
+ userScope: ompUserScope,
1739
+ projectRoot: ompProjectRoot,
1740
+ });
1222
1741
  console.log(JSON.stringify(result, null, 2));
1223
1742
  break;
1224
1743
  }
@@ -1227,6 +1746,22 @@ export async function main(args = process.argv.slice(2)) {
1227
1746
  await handleProfile(subArgs);
1228
1747
  break;
1229
1748
 
1749
+ case 'credential-policy': {
1750
+ const registry = new McpServerRegistry();
1751
+ const value = subArgs.find(a => !a.startsWith('--'));
1752
+ if (!value) {
1753
+ console.log(await registry.getCredentialPolicy() || 'literal');
1754
+ break;
1755
+ }
1756
+ if (!CREDENTIAL_POLICIES.includes(value)) {
1757
+ console.error(`Unknown credential policy "${value}". Use one of: ${CREDENTIAL_POLICIES.join(', ')}`);
1758
+ process.exit(1);
1759
+ }
1760
+ await registry.setCredentialPolicy(value);
1761
+ console.log(`Credential policy: ${value}`);
1762
+ break;
1763
+ }
1764
+
1230
1765
  case '--help':
1231
1766
  case '-h':
1232
1767
  case 'help':