@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
@@ -4,21 +4,31 @@
4
4
  * Codex has no native per-session config flag (unlike `claude --mcp-config`).
5
5
  * This adapter implements the sysops `codex-role.sh` pattern:
6
6
  *
7
- * 1. Create ~/.codex/roles-runtime/<profile>/ per profile
7
+ * 1. Create <CODEX_HOME or ~/.codex>/roles-runtime/<profile>/ per profile
8
8
  * 2. Symlink shared state (history, sessions) into the runtime home
9
9
  * 3. Write a profile-scoped config.toml (stripped global MCP, only profile servers)
10
- * 4. Launch with HOME=<runtime-home> codex
10
+ * 4. Launch with CODEX_HOME=<runtime-home> codex
11
11
  * 5. Auth flows execute against the runtime home — OAuth tokens are isolated per profile
12
12
  *
13
13
  * Reference: roctinam/sysops:scripts/mcp-roles/codex-role.sh
14
14
  *
15
15
  * @implements #892
16
16
  */
17
- import { readFile, writeFile, mkdir, symlink, access, readdir } from 'fs/promises';
17
+ import { signalChild } from '../../process/signal-child.mjs';
18
+ import { readFile, mkdir, symlink, access, readdir, chmod, lstat, open } from 'fs/promises';
18
19
  import { join } from 'path';
19
20
  import { homedir } from 'os';
20
- import { existsSync } from 'fs';
21
- import { spawnSync } from 'child_process';
21
+ import { existsSync, constants } from 'fs';
22
+ import { spawn, spawnSync } from 'child_process';
23
+ import { boundedSpawnSync, resolveSyncProcessTimeoutMs } from '../../process/bounded-child-process.mjs';
24
+ import { isInteractive } from '../../cli/env.js';
25
+ import { exitCodeForSpawnResult } from '../../cli/child-process-result.js';
26
+ import { buildServerToml } from '../registry.js';
27
+ import { assertCredentialPolicy, resolveCredentialPolicy } from '../credentials.mjs';
28
+ import { planToolFilters } from '../tool-filters.mjs';
29
+ import { assertConfigDestination, writeConfigAtomic } from '../config-file.mjs';
30
+ import { stripMcpServers } from '../toml-strip-mcp.mjs';
31
+ import { resolveUserConfigDir } from '../../config/user-config-dir.mjs';
22
32
  const DEFAULT_SHARED_STATE = {
23
33
  // Shared: operator history and session index are cross-profile
24
34
  shared: ['history.jsonl', 'sessions'],
@@ -29,12 +39,15 @@ const DEFAULT_SHARED_STATE = {
29
39
  // Paths
30
40
  // ─────────────────────────────────────────────
31
41
  function codexHome() {
32
- return join(homedir(), '.codex');
42
+ return process.env.CODEX_HOME || join(homedir(), '.codex');
33
43
  }
34
44
  function runtimeHomesDir() {
35
45
  return join(codexHome(), 'roles-runtime');
36
46
  }
37
47
  function runtimeHomePath(profile) {
48
+ if (!/^[a-z0-9-]+$/.test(profile)) {
49
+ throw new Error('Invalid profile name. Names must match [a-z0-9-]+.');
50
+ }
38
51
  return join(runtimeHomesDir(), profile);
39
52
  }
40
53
  function runtimeConfigPath(profile) {
@@ -43,14 +56,22 @@ function runtimeConfigPath(profile) {
43
56
  // ─────────────────────────────────────────────
44
57
  // Runtime home management
45
58
  // ─────────────────────────────────────────────
59
+ async function ensurePrivateRuntimeHome(profile) {
60
+ const rtHome = runtimeHomePath(profile);
61
+ // The global .codex home may be managed by a dotfile symlink. Refuse
62
+ // symlinks below it, including roles-runtime, the profile, and config.
63
+ await assertConfigDestination(runtimeConfigPath(profile), codexHome());
64
+ await mkdir(rtHome, { recursive: true, mode: 0o700 });
65
+ await chmod(rtHome, 0o700);
66
+ return rtHome;
67
+ }
46
68
  /**
47
69
  * Ensure the runtime home directory exists for a profile.
48
70
  * Creates the directory and sets up shared-state symlinks.
49
71
  * Returns the runtime home path.
50
72
  */
51
73
  export async function ensureRuntimeHome(profile, policy = DEFAULT_SHARED_STATE) {
52
- const rtHome = runtimeHomePath(profile);
53
- await mkdir(rtHome, { recursive: true });
74
+ const rtHome = await ensurePrivateRuntimeHome(profile);
54
75
  const globalHome = codexHome();
55
76
  // Create symlinks for shared state (only if source exists in global home)
56
77
  for (const name of policy.shared) {
@@ -77,64 +98,75 @@ export async function ensureRuntimeHome(profile, policy = DEFAULT_SHARED_STATE)
77
98
  }
78
99
  /**
79
100
  * Write a profile-scoped config.toml into the runtime home.
80
- * Strips all [mcp_servers.*] blocks from any existing global config.toml
101
+ * Strips the entire mcp_servers subtree from any existing global config.toml
81
102
  * and writes only the profile's servers.
103
+ *
104
+ * `options.floorPolicy`, when given, is an enforced organisation floor
105
+ * (see `resolveCredentialPolicy`): this function resolves and enforces it
106
+ * itself, so a direct caller that omits `credentialPolicy` or passes a
107
+ * weaker value cannot write literal credentials below the floor. Pass
108
+ * `floorPolicy` from the same registry whose servers are being written
109
+ * (`McpServerRegistry#getCredentialPolicyFloor`); omitting it disables floor
110
+ * enforcement for this call, matching a single registry with no layering.
82
111
  */
83
- export async function writeProfileConfig(profile, servers) {
84
- const rtHome = runtimeHomePath(profile);
85
- await mkdir(rtHome, { recursive: true });
112
+ export async function writeProfileConfig(profile, servers, options = {}) {
113
+ // env is deliberately excluded: the caller (CLI/session) already resolved
114
+ // any environment/flag override before calling here, and `credentialPolicy`
115
+ // carries that result as `flag`; this call only adds the floor enforcement.
116
+ const resolvedPolicy = resolveCredentialPolicy({
117
+ flag: options.credentialPolicy, floorPolicy: options.floorPolicy, env: {},
118
+ });
119
+ assertCredentialPolicy(servers, resolvedPolicy);
120
+ const toolPlan = options.toolFilters
121
+ ? planToolFilters('codex', servers.map((server) => server.name), options.toolFilters)
122
+ : null;
123
+ await ensurePrivateRuntimeHome(profile);
86
124
  // Load global config.toml as base (strip existing mcp_servers blocks)
87
125
  let baseConfig = '';
88
126
  const globalConfigPath = join(codexHome(), 'config.toml');
89
127
  try {
90
128
  const raw = await readFile(globalConfigPath, 'utf-8');
91
- // Remove all [mcp_servers.*] sections and their content
92
- baseConfig = raw
93
- .replace(/\[mcp_servers\.[^\]]+\][\s\S]*?(?=\n\[|\s*$)/g, '')
94
- .trimEnd();
129
+ baseConfig = stripMcpServers(raw).trimEnd();
95
130
  }
96
- catch {
97
- // No global config — start empty
131
+ catch (error) {
132
+ // Only a missing global config permits an empty base. Classification and
133
+ // read errors must never silently copy or replace a previous config.
134
+ if (error.code !== 'ENOENT')
135
+ throw error;
98
136
  }
99
137
  // Build profile-specific [mcp_servers.*] TOML sections
100
- const mcpSections = [];
101
- for (const server of servers) {
102
- const lines = [`[mcp_servers.${server.name}]`];
103
- if (server.type === 'stdio') {
104
- lines.push(`command = "${server.command}"`);
105
- if (server.args && server.args.length > 0) {
106
- const argsStr = server.args.map((a) => `"${a}"`).join(', ');
107
- lines.push(`args = [${argsStr}]`);
108
- }
109
- if (server.env) {
110
- for (const [k, v] of Object.entries(server.env)) {
111
- lines.push(`env.${k} = "${v}"`);
112
- }
113
- }
114
- }
115
- else {
116
- lines.push(`url = "${server.url}"`);
117
- if (server.headers) {
118
- for (const [k, v] of Object.entries(server.headers)) {
119
- lines.push(`headers.${k} = "${v}"`);
120
- }
121
- }
122
- }
123
- lines.push(`startup_timeout_sec = 10.0`);
124
- lines.push(`tool_timeout_sec = 60.0`);
125
- mcpSections.push(lines.join('\n'));
126
- }
138
+ const mcpSections = servers.map((server) => [buildServerToml(server), ...(toolPlan?.tomlLines[server.name] ?? [])].join('\n'));
127
139
  const configContent = (baseConfig ? baseConfig + '\n\n' : '') +
128
140
  mcpSections.join('\n\n') +
129
141
  '\n';
130
- await writeFile(runtimeConfigPath(profile), configContent, 'utf-8');
142
+ const configPath = runtimeConfigPath(profile);
143
+ await writeConfigAtomic(configPath, configContent, { userScope: true, projectRoot: codexHome() });
144
+ return toolPlan?.warnings ?? [];
145
+ }
146
+ /**
147
+ * True iff `extraArgs` already selects one of codex's documented
148
+ * non-interactive forms, so launching with them is safe to bound (#3062).
149
+ * `exec` is the only evidenced one today (`codex exec <prompt>`; see
150
+ * tools/ralph-external/lib/codex-adapter.mjs). Anything else — including no
151
+ * args at all — hands the terminal to the interactive TUI.
152
+ */
153
+ function isNonInteractiveCodexInvocation(extraArgs) {
154
+ return extraArgs[0] === 'exec';
131
155
  }
132
156
  /**
133
157
  * Launch Codex with a profile's runtime home.
134
- * Sets HOME=<runtime-home> so Codex reads its profile-scoped config and
158
+ * Sets CODEX_HOME and HOME to the runtime home so Codex reads its profile-scoped config and
135
159
  * OAuth tokens are written to the runtime home (isolated from other profiles).
160
+ *
161
+ * Headless (#3062): when `extraArgs` is already a non-interactive form (see
162
+ * {@link isNonInteractiveCodexInvocation}), the call never hands over the
163
+ * terminal, so it is bounded like any other child process and never inherits
164
+ * stdin — unless `options.input` is given, in which case it carries the
165
+ * prompt on stdin instead (review item 8). Otherwise this launches the
166
+ * interactive TUI, which requires a terminal — without one, this refuses
167
+ * instead of spawning something that would hang.
136
168
  */
137
- export function launchWithProfile(profile, extraArgs = []) {
169
+ export function launchWithProfile(profile, extraArgs = [], options = {}) {
138
170
  const rtHome = runtimeHomePath(profile);
139
171
  if (!existsSync(rtHome)) {
140
172
  throw new Error(`Runtime home for profile "${profile}" does not exist.\n` +
@@ -143,29 +175,306 @@ export function launchWithProfile(profile, extraArgs = []) {
143
175
  const env = {
144
176
  ...process.env,
145
177
  HOME: rtHome,
178
+ CODEX_HOME: rtHome,
179
+ // Resolve before HOME changes so the child retains the operator's
180
+ // installation identity, channel and other provider-neutral configuration.
181
+ AIWG_CONFIG: resolveUserConfigDir(),
146
182
  };
183
+ if (isNonInteractiveCodexInvocation(extraArgs)) {
184
+ // Bounded (#3062): `codex exec -` never hands over the terminal; the
185
+ // prompt (if any) rides on stdin via `options.input`, never on argv.
186
+ return boundedSpawnSync('codex', extraArgs, 'long', {
187
+ stdio: options.input !== undefined ? ['pipe', 'inherit', 'inherit'] : ['ignore', 'inherit', 'inherit'],
188
+ env,
189
+ ...(options.input !== undefined ? { input: options.input } : {}),
190
+ });
191
+ }
192
+ if (!(options.interactive ?? isInteractive())) {
193
+ throw new Error(`Launching codex for profile "${profile}" with no arguments hands over the terminal and requires one to be attached.\n` +
194
+ 'Pass a non-interactive form instead — e.g. run `aiwg session --provider codex --profile ' +
195
+ `${profile} --prompt "<text>"\` (maps to \`codex exec -\`, with the prompt on stdin).`);
196
+ }
197
+ // sync-spawn: interactive codex-exec — replaces the terminal with the codex
198
+ // binary the user is meant to talk to, when extraArgs is not already a
199
+ // non-interactive form (see tools/testing/sync-spawn-allowlist.json).
200
+ // Reached only when `options.interactive ?? isInteractive()` is true
201
+ // (#3062) — the non-interactive and no-terminal paths above are handled
202
+ // before this point. Not a candidate for a bounded timeout.
147
203
  return spawnSync('codex', extraArgs, {
148
204
  stdio: 'inherit',
149
205
  env,
150
206
  });
151
207
  }
208
+ /**
209
+ * Build a child environment that excludes `envVarName` (when given) and
210
+ * any other variable holding the identical value (#3062 second review,
211
+ * finding 1): the API key must never be visible in the child's own
212
+ * environment, only on its stdin, including as a coincidental duplicate
213
+ * under some other name. `envVarName` is `undefined` when the key came
214
+ * from somewhere other than a named environment variable (`--api-key-stdin`
215
+ * / `--api-key-file`, go/no-go review blocker 1C) — the by-value scrub
216
+ * still applies in that case.
217
+ */
218
+ function envWithoutSecretValue(base, envVarName, secretValue) {
219
+ const result = {};
220
+ for (const [key, value] of Object.entries(base)) {
221
+ if (value === undefined)
222
+ continue;
223
+ if (envVarName !== undefined && key === envVarName)
224
+ continue;
225
+ if (value === secretValue)
226
+ continue;
227
+ result[key] = value;
228
+ }
229
+ return result;
230
+ }
231
+ /**
232
+ * Read an API key from a private file (#3062 go/no-go review, blocker 1C):
233
+ * `--api-key-file <path>` reads the key from disk instead of an
234
+ * environment variable, so a file that does not meet the same bar the
235
+ * decision ledger's own file writer enforces
236
+ * (`src/decision/ledger.ts`'s `openLedgerFileForAppend`) is refused rather
237
+ * than silently trusted. This mirrors that check for a read instead of a
238
+ * write — there is no already-exported helper at this layer to reuse (the
239
+ * ledger's checks are private to that module and tied to its own
240
+ * create-if-missing write path), so this re-implements the same checks for
241
+ * a read: never follow a symlink; the open file descriptor must resolve to
242
+ * a regular file with exactly one hard link; and, on POSIX, must be owned
243
+ * by the current user with no group/other permission bits. The file's
244
+ * contents are returned trimmed; no error message here ever includes them.
245
+ */
246
+ async function readApiKeyFromFile(filePath) {
247
+ try {
248
+ const pre = await lstat(filePath);
249
+ if (pre.isSymbolicLink()) {
250
+ throw new Error(`--api-key-file: "${filePath}" is a symlink; refusing to follow it.`);
251
+ }
252
+ }
253
+ catch (error) {
254
+ if (error.code === 'ENOENT') {
255
+ throw new Error(`--api-key-file: "${filePath}" does not exist.`);
256
+ }
257
+ if (error instanceof Error && error.message.startsWith('--api-key-file:'))
258
+ throw error;
259
+ throw new Error(`--api-key-file: cannot stat "${filePath}": ${error.message}`);
260
+ }
261
+ const hasNoFollow = typeof constants.O_NOFOLLOW === 'number';
262
+ const flags = constants.O_RDONLY | (hasNoFollow ? constants.O_NOFOLLOW : 0);
263
+ let handle;
264
+ try {
265
+ handle = await open(filePath, flags);
266
+ }
267
+ catch (error) {
268
+ const code = error.code;
269
+ if (code === 'ELOOP')
270
+ throw new Error(`--api-key-file: "${filePath}" is a symlink; refusing to follow it.`);
271
+ if (code === 'ENOENT')
272
+ throw new Error(`--api-key-file: "${filePath}" does not exist.`);
273
+ throw new Error(`--api-key-file: cannot open "${filePath}": ${error.message}`);
274
+ }
275
+ try {
276
+ const stat = await handle.stat();
277
+ if (!stat.isFile()) {
278
+ throw new Error(`--api-key-file: "${filePath}" is not a regular file.`);
279
+ }
280
+ if (stat.nlink !== 1) {
281
+ throw new Error(`--api-key-file: "${filePath}" has more than one hard link; refusing to read it.`);
282
+ }
283
+ if (process.platform !== 'win32') {
284
+ const uid = typeof process.getuid === 'function' ? process.getuid() : null;
285
+ if (uid !== null && stat.uid !== uid) {
286
+ throw new Error(`--api-key-file: "${filePath}" is not owned by the current user.`);
287
+ }
288
+ if ((stat.mode & 0o077) !== 0) {
289
+ throw new Error(`--api-key-file: "${filePath}" must not be readable or writable by group or other ` +
290
+ '(chmod 600 it).');
291
+ }
292
+ }
293
+ return (await handle.readFile({ encoding: 'utf-8' })).trim();
294
+ }
295
+ finally {
296
+ await handle.close();
297
+ }
298
+ }
299
+ /**
300
+ * Async, credential-safe launch of `codex login --with-api-key` (#3062
301
+ * second review, finding 1). This is an *interactive-site exception* to
302
+ * the synchronous bounded-child-process helper — not a sync call at all,
303
+ * so the sync-child-process gate does not (and should not) apply to it.
304
+ *
305
+ * `boundedSpawnSync`'s `input` option places the secret inside the
306
+ * options object `child_process` hands to its native binding, which
307
+ * `NODE_DEBUG=child_process` (and any other dump of those options)
308
+ * prints verbatim — confirmed by the review with a live probe. An async
309
+ * `spawn()` has no `input` option at all: the key is written directly to
310
+ * the already-created child's own `stdin` stream, after the options
311
+ * object was already built and handed off, so it is never part of any
312
+ * options object, logged value, or thrown Error. The 'network'-tier bound
313
+ * (same as the previous synchronous call) is enforced here by hand, with
314
+ * the same SIGKILL-and-enriched-message contract the bounded helper
315
+ * itself uses, since this call can no longer go through that helper.
316
+ */
317
+ function spawnCodexLoginWithApiKey(apiKey, env) {
318
+ const tier = 'network';
319
+ const boundMs = resolveSyncProcessTimeoutMs(tier);
320
+ return new Promise((resolve) => {
321
+ const child = spawn('codex', ['login', '--with-api-key'], {
322
+ env,
323
+ stdio: ['pipe', 'inherit', 'inherit'],
324
+ });
325
+ let settled = false;
326
+ let timer;
327
+ // #3062 go/no-go review, blocker 2: stdin error, write completion,
328
+ // child 'error', child 'exit', and the deadline all funnel through this
329
+ // one function, which settles exactly once, always clears the timer and
330
+ // removes every listener this call added, and kills the child if it is
331
+ // still alive on a failing settlement (a running process on the
332
+ // timeout path; a process that may still be running on an unexpected
333
+ // stdin-write error). The 'exit' path never reaches the kill — by the
334
+ // time that event fires the child has already exited.
335
+ const settle = (result) => {
336
+ if (settled)
337
+ return;
338
+ settled = true;
339
+ if (timer)
340
+ clearTimeout(timer);
341
+ child.stdin?.removeListener('error', onStdinError);
342
+ child.removeListener('error', onChildError);
343
+ child.removeListener('exit', onExit);
344
+ if (result.error && child.exitCode === null && child.signalCode === null) {
345
+ try {
346
+ signalChild(child, 'SIGKILL');
347
+ }
348
+ catch { /* already gone */ }
349
+ }
350
+ resolve(result);
351
+ };
352
+ timer = setTimeout(() => {
353
+ settle({
354
+ status: null,
355
+ signal: 'SIGKILL',
356
+ error: Object.assign(new Error(`\`codex login --with-api-key\` exceeded the '${tier}' tier bound of ${boundMs}ms and was killed (SIGKILL).`), { code: 'ETIMEDOUT' }),
357
+ });
358
+ }, boundMs);
359
+ const onChildError = (err) => settle({ status: null, signal: null, error: err });
360
+ child.on('error', onChildError);
361
+ const onExit = (code, signal) => settle({ status: code, signal });
362
+ child.on('exit', onExit);
363
+ // A child that exits (or closes its read end) before reading stdin
364
+ // raises EPIPE/ECONNRESET on the *stdin stream*, not on the child
365
+ // object — `child.on('error', ...)` above never sees it, so without
366
+ // this listener it is an unhandled 'error' event, which Node turns into
367
+ // an uncaught exception. Installed before the write/end below, per the
368
+ // review. EPIPE/ECONNRESET specifically mean "the child already went
369
+ // away"; the already-firing (or about-to-fire) 'exit' event carries the
370
+ // real, non-secret result, so this swallows exactly those two codes and
371
+ // lets settle() happen there instead. Anything else is a genuine,
372
+ // unexpected stdin failure and is itself the settling error.
373
+ const onStdinError = (err) => {
374
+ if (err.code === 'EPIPE' || err.code === 'ECONNRESET')
375
+ return;
376
+ settle({ status: null, signal: null, error: err });
377
+ };
378
+ child.stdin.on('error', onStdinError);
379
+ // Written directly to the live stream — never part of the spawn
380
+ // options object above, so NODE_DEBUG=child_process (and any dump of
381
+ // those options) cannot print it. `end(chunk, callback)` reports a
382
+ // failed write through its callback; `onStdinError` above is the
383
+ // primary path (Node always emits 'error' on the stream too), so this
384
+ // callback only needs to catch the case where, for some Node version or
385
+ // stream state, the callback fires but the 'error' event does not.
386
+ child.stdin.end(apiKey, (err) => {
387
+ const code = err?.code;
388
+ if (err && code !== 'EPIPE' && code !== 'ECONNRESET') {
389
+ settle({ status: null, signal: null, error: err });
390
+ }
391
+ });
392
+ });
393
+ }
152
394
  /**
153
395
  * Run `codex login` within a profile's runtime home to isolate OAuth tokens.
396
+ *
397
+ * Headless (#3062): pass `apiKeyEnv` to log in non-interactively. codex-cli
398
+ * documents `--with-api-key` for exactly this (verified: `codex login
399
+ * --help`, codex-cli 0.162.1 — "Read the API key from stdin (e.g. `printenv
400
+ * OPENAI_API_KEY | codex login --with-api-key`)"); this function reads
401
+ * `process.env[apiKeyEnv]` and feeds it to codex over stdin, so the key
402
+ * itself never appears on argv or in a process listing — only the
403
+ * environment-variable *name* is ever passed around. Without `apiKeyEnv`
404
+ * and without a terminal, this refuses instead of spawning the interactive
405
+ * OAuth flow, which would hang waiting for input nobody can supply.
406
+ *
407
+ * Wired to `aiwg mcp profile login <name> [--api-key-env <NAME>]`
408
+ * (`src/mcp/cli.mjs`, #3069). The `apiKeyEnv` branch never places the key
409
+ * in a spawn options object or the child's own environment — see
410
+ * `spawnCodexLoginWithApiKey()`'s doc comment.
154
411
  */
155
- export async function loginInProfile(profile) {
412
+ export async function loginInProfile(profile, options = {}) {
156
413
  await ensureRuntimeHome(profile);
157
414
  const rtHome = runtimeHomePath(profile);
415
+ const env = {
416
+ ...process.env,
417
+ HOME: rtHome,
418
+ CODEX_HOME: rtHome,
419
+ AIWG_CONFIG: resolveUserConfigDir(),
420
+ };
421
+ // `options.apiKey` (go/no-go review, blocker 1C) is the already-resolved
422
+ // key from `--api-key-stdin`/`--api-key-file` — the caller (`src/mcp/
423
+ // cli.mjs`) read and validated it before calling here, so there is no
424
+ // single named source environment variable to exclude below, only the
425
+ // by-value scrub `envWithoutSecretValue()` always does. `options.apiKeyEnv`
426
+ // is unchanged: still read from `process.env` here, so its *name* is the
427
+ // one thing ever passed around. Exactly one of the two is ever set — the
428
+ // CLI layer enforces that mutual exclusivity before calling this function.
429
+ let apiKey;
430
+ let sourceDescription;
431
+ if (options.apiKey !== undefined) {
432
+ apiKey = options.apiKey;
433
+ sourceDescription = 'the supplied key';
434
+ }
435
+ else if (options.apiKeyEnv) {
436
+ apiKey = process.env[options.apiKeyEnv];
437
+ if (!apiKey) {
438
+ throw new Error(`Environment variable "${options.apiKeyEnv}" is not set; cannot log in to profile "${profile}" non-interactively.`);
439
+ }
440
+ sourceDescription = `an API key from $${options.apiKeyEnv}`;
441
+ }
442
+ if (apiKey !== undefined) {
443
+ console.log(`\n Logging in to profile "${profile}" with ${sourceDescription}...`);
444
+ // Credential-safe async launch (#3062 second review, finding 1): the
445
+ // key never enters a spawn options object, env dump, log line, or
446
+ // thrown value — see spawnCodexLoginWithApiKey()'s own doc comment for
447
+ // why this can no longer be the synchronous bounded helper.
448
+ const safeEnv = envWithoutSecretValue(env, options.apiKeyEnv, apiKey);
449
+ const result = await spawnCodexLoginWithApiKey(apiKey, safeEnv);
450
+ if (result.status !== 0) {
451
+ // #3062 review item 7: surface the bounded helper's `error`/`signal`
452
+ // detail (e.g. the enriched timeout message naming the bound and
453
+ // override variable) instead of only the exit code.
454
+ const { message } = exitCodeForSpawnResult(result, 'codex login --with-api-key');
455
+ throw new Error(message ?? `codex login --with-api-key failed with exit code ${result.status ?? 'unknown'}`);
456
+ }
457
+ return;
458
+ }
459
+ if (!isInteractive()) {
460
+ throw new Error(`Logging in to profile "${profile}" requires an interactive terminal, or a key supplied via ` +
461
+ '--api-key-env, --api-key-stdin, or --api-key-file (never pass the key itself on the command ' +
462
+ `line), or run \`codex login\` yourself with CODEX_HOME=${rtHome}.`);
463
+ }
158
464
  console.log(`\n Running codex login in profile "${profile}" runtime home...`);
159
465
  console.log(` Auth tokens will be isolated to: ${rtHome}\n`);
466
+ // sync-spawn: interactive codex-login — runs the codex OAuth login flow,
467
+ // which prompts the user directly (see tools/testing/
468
+ // sync-spawn-allowlist.json). Reached only when isInteractive() is true
469
+ // and no apiKeyEnv was given (#3062). Not a candidate for a bounded
470
+ // timeout.
160
471
  const result = spawnSync('codex', ['login'], {
161
472
  stdio: 'inherit',
162
- env: {
163
- ...process.env,
164
- HOME: rtHome,
165
- },
473
+ env,
166
474
  });
167
475
  if (result.status !== 0) {
168
- throw new Error(`codex login failed with exit code ${result.status ?? 'unknown'}`);
476
+ const { message } = exitCodeForSpawnResult(result, 'codex login');
477
+ throw new Error(message ?? `codex login failed with exit code ${result.status ?? 'unknown'}`);
169
478
  }
170
479
  }
171
480
  // ─────────────────────────────────────────────
@@ -206,7 +515,8 @@ export async function removeRuntimeHome(profile) {
206
515
  const rtHome = runtimeHomePath(profile);
207
516
  if (!existsSync(rtHome))
208
517
  return false;
209
- const result = spawnSync('rm', ['-rf', rtHome], { stdio: 'inherit' });
518
+ // Local filesystem delete; bounded as a quick-tier call (#3062).
519
+ const result = boundedSpawnSync('rm', ['-rf', rtHome], 'quick', { stdio: 'inherit' });
210
520
  return result.status === 0;
211
521
  }
212
522
  /**
@@ -220,5 +530,5 @@ export async function detectOrphanedRuntimes(knownProfiles) {
220
530
  // ─────────────────────────────────────────────
221
531
  // Re-exports (convenience)
222
532
  // ─────────────────────────────────────────────
223
- export { DEFAULT_SHARED_STATE };
533
+ export { DEFAULT_SHARED_STATE, readApiKeyFromFile };
224
534
  //# sourceMappingURL=codex-runtime.js.map
@@ -0,0 +1,4 @@
1
+ import type { ConfigWrite } from './config-file.mjs';
2
+ export function claudeDesktopConfigPath(platform?: string, env?: NodeJS.ProcessEnv): string;
3
+ export function resolveClaudeInstallTarget(target: string, scope: string, env?: NodeJS.ProcessEnv): string;
4
+ export function legacyClaudeMigration(projectDir: string): Promise<ConfigWrite[]>;
@@ -0,0 +1,41 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { assertConfigDestination, assertConfigObject } from './config-file.mjs';
5
+
6
+ /** Desktop is a user-level surface, independent of the Code project config. */
7
+ export function claudeDesktopConfigPath(platform = process.platform, env = process.env) {
8
+ const home = env.HOME || env.USERPROFILE || os.homedir();
9
+ const base = platform === 'darwin' ? path.join(home, 'Library', 'Application Support')
10
+ : platform === 'win32' ? (env.APPDATA || path.join(home, 'AppData', 'Roaming'))
11
+ : path.join(home, '.config');
12
+ return path.join(base, 'Claude', 'claude_desktop_config.json');
13
+ }
14
+
15
+ export function resolveClaudeInstallTarget(target, scope, env = process.env) {
16
+ if (target !== 'claude') return target;
17
+ return scope === 'user' || env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT || env.CLAUDE_CODE_DESKTOP_APP_VERSION
18
+ ? 'claude-code' : 'claude-desktop';
19
+ }
20
+
21
+ /** Plan removal only of the exact legacy built-in stanza, never custom servers. */
22
+ export async function legacyClaudeMigration(projectDir) {
23
+ const file = path.resolve(projectDir, '.claude/settings.local.json');
24
+ await assertConfigDestination(file, projectDir);
25
+ let settings;
26
+ try {
27
+ settings = JSON.parse(await fs.readFile(file, 'utf8'));
28
+ } catch (error) {
29
+ if (error.code === 'ENOENT') return [];
30
+ throw error;
31
+ }
32
+ assertConfigObject(settings, 'mcpServers');
33
+ const server = settings.mcpServers?.aiwg;
34
+ if (!server || server.command !== 'aiwg' || JSON.stringify(server.args) !== '["mcp","serve"]'
35
+ || Object.keys(server).some(key => !['command', 'args', 'env'].includes(key))
36
+ || (server.env !== undefined && (!server.env || typeof server.env !== 'object' || Array.isArray(server.env)
37
+ || Object.keys(server.env).some(key => key !== 'AIWG_ROOT')))) return [];
38
+ delete settings.mcpServers.aiwg;
39
+ if (Object.keys(settings.mcpServers).length === 0) delete settings.mcpServers;
40
+ return [{ file, content: JSON.stringify(settings, null, 2), options: { projectRoot: projectDir } }];
41
+ }
@@ -0,0 +1 @@
1
+ export function main(args?: string[]): Promise<void>;