@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.
- package/README.md +2 -2
- package/agentic/code/providers/capability-matrix.yaml +270 -0
- package/agentic/code/providers/model-capabilities.v1.json +3183 -24
- package/bin/aiwg.mjs +135 -47
- package/dist/src/a2a/codecs.js +9 -0
- package/dist/src/agents/grounding/knowledge-base.js +54 -6
- package/dist/src/artifacts/browser-export.js +6 -0
- package/dist/src/artifacts/checksum-manifest.js +1 -0
- package/dist/src/artifacts/citation-parser.js +173 -92
- package/dist/src/artifacts/citation-table-schema.js +59 -0
- package/dist/src/artifacts/cli.js +57 -18
- package/dist/src/artifacts/corpus-tools/snapshot.js +1 -1
- package/dist/src/artifacts/corpus-tools/vision-extract.js +107 -15
- package/dist/src/artifacts/corpus-views/ref-parser.js +3 -1
- package/dist/src/artifacts/corpus-views/renderers.js +3 -3
- package/dist/src/artifacts/discovery-eval.js +6 -4
- package/dist/src/artifacts/fortemi-core-query-adapter.js +36 -3
- package/dist/src/artifacts/fortemi-core-sync.js +166 -40
- package/dist/src/artifacts/framework-capabilities.js +171 -0
- package/dist/src/artifacts/framework-index-paths.js +73 -0
- package/dist/src/artifacts/framework-source-digest.js +75 -0
- package/dist/src/artifacts/index-builder.js +285 -131
- package/dist/src/artifacts/index-files.js +132 -18
- package/dist/src/artifacts/index-preparation.js +65 -0
- package/dist/src/artifacts/index-refresh.js +576 -0
- package/dist/src/artifacts/index-worker.mjs +9 -0
- package/dist/src/artifacts/prebuilt-build-lock.js +47 -4
- package/dist/src/artifacts/prebuilt-deployment.js +46 -0
- package/dist/src/artifacts/prebuilt-relations.js +80 -0
- package/dist/src/artifacts/query-engine.js +410 -88
- package/dist/src/artifacts/relations-builder.js +168 -0
- package/dist/src/artifacts/relations.js +148 -0
- package/dist/src/artifacts/repair.js +9 -4
- package/dist/src/artifacts/stats.js +5 -2
- package/dist/src/artifacts/types.js +30 -10
- package/dist/src/artifacts/watcher.js +1 -1
- package/dist/src/auth/credential-store.js +2 -1
- package/dist/src/catalog/cli.d.mts +5 -0
- package/dist/src/channel/manager.d.mts +20 -0
- package/dist/src/channel/manager.mjs +78 -12
- package/dist/src/cli/child-process-result.js +40 -0
- package/dist/src/cli/command-log.js +2 -3
- package/dist/src/cli/env.js +38 -16
- package/dist/src/cli/flag-parsing.js +236 -0
- package/dist/src/cli/handlers/artifacts.js +3 -0
- package/dist/src/cli/handlers/auth.js +3 -0
- package/dist/src/cli/handlers/decision.js +218 -5
- package/dist/src/cli/handlers/diagnose.js +13 -5
- package/dist/src/cli/handlers/effect.js +4 -2
- package/dist/src/cli/handlers/evidence.js +3 -0
- package/dist/src/cli/handlers/feedback.js +14 -5
- package/dist/src/cli/handlers/help.js +1 -0
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/init.js +248 -30
- package/dist/src/cli/handlers/lint.js +5 -0
- package/dist/src/cli/handlers/mc.js +23 -0
- package/dist/src/cli/handlers/models.js +169 -10
- package/dist/src/cli/handlers/ralph-launcher.js +2 -0
- package/dist/src/cli/handlers/ralph.js +3 -0
- package/dist/src/cli/handlers/refresh.js +28 -0
- package/dist/src/cli/handlers/regenerate.js +14 -0
- package/dist/src/cli/handlers/resource-versions.js +3 -0
- package/dist/src/cli/handlers/rules.js +195 -0
- package/dist/src/cli/handlers/run.js +4 -0
- package/dist/src/cli/handlers/runtime-info.js +25 -11
- package/dist/src/cli/handlers/script-runner.js +19 -8
- package/dist/src/cli/handlers/session.js +406 -62
- package/dist/src/cli/handlers/sessions.js +243 -76
- package/dist/src/cli/handlers/setup-manifest.js +474 -79
- package/dist/src/cli/handlers/setup.js +180 -39
- package/dist/src/cli/handlers/steward.js +11 -0
- package/dist/src/cli/handlers/subcommands.js +60 -3
- package/dist/src/cli/handlers/team.js +18 -8
- package/dist/src/cli/handlers/uhp.js +3 -0
- package/dist/src/cli/handlers/use.js +307 -55
- package/dist/src/cli/handlers/utilities.js +125 -26
- package/dist/src/cli/handlers/version.js +8 -0
- package/dist/src/cli/prompt-utils.js +72 -0
- package/dist/src/cli/provider-resolution.js +11 -0
- package/dist/src/cli/router.js +15 -1
- package/dist/src/cli/services/deployment-verification.js +19 -9
- package/dist/src/cli/services/provider-prepare-coordinator.js +24 -0
- package/dist/src/cli/skill-usage.js +2 -3
- package/dist/src/config/aiwg-config.js +349 -35
- package/dist/src/config/cli.js +256 -23
- package/dist/src/config/decision-config.js +106 -0
- package/dist/src/config/dispatch-thresholds-runtime.d.mts +8 -0
- package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
- package/dist/src/config/gitignore.js +50 -0
- package/dist/src/config/index-graph-overrides.js +36 -0
- package/dist/src/config/project-artifacts-health.d.mts +37 -0
- package/dist/src/config/project-artifacts-health.mjs +14 -3
- package/dist/src/config/project-artifacts-runtime.d.mts +16 -0
- package/dist/src/config/user-config-dir.d.mts +9 -0
- package/dist/src/config/user-config.js +38 -11
- package/dist/src/config/user-registry.js +23 -15
- package/dist/src/config/user-state.js +7 -0
- package/dist/src/decision/adapters/jev.js +75 -27
- package/dist/src/decision/ask.js +203 -43
- package/dist/src/decision/context-live-qualification.js +2 -2
- package/dist/src/decision/driver.js +14 -7
- package/dist/src/decision/ensemble/aggregate.js +23 -10
- package/dist/src/decision/ensemble-study/multifact-study.js +6 -2
- package/dist/src/decision/entry.js +25 -6
- package/dist/src/decision/graph-live-qualification.js +1 -1
- package/dist/src/decision/graph-run.js +9 -0
- package/dist/src/decision/heldout/collector.js +15 -14
- package/dist/src/decision/heldout/contract.js +30 -7
- package/dist/src/decision/index.js +1 -0
- package/dist/src/decision/interop.js +60 -8
- package/dist/src/decision/jev-credentials.js +15 -1
- package/dist/src/decision/ledger.js +942 -0
- package/dist/src/decision/offload-activation.js +137 -0
- package/dist/src/decision/offload.js +550 -0
- package/dist/src/decision/qualification/load.js +4 -6
- package/dist/src/decision/savings.js +28 -0
- package/dist/src/decision/validate.js +6 -3
- package/dist/src/effects/dispatch-verifier.js +252 -0
- package/dist/src/effects/index.js +1 -0
- package/dist/src/effects/types.js +5 -1
- package/dist/src/effects/verifiers/git.js +2 -1
- package/dist/src/effects/verifiers/tracker.js +26 -14
- package/dist/src/extensions/commands/definitions.js +19 -0
- package/dist/src/extensions/project-local-remove.js +36 -1
- package/dist/src/features/installer.js +2 -2
- package/dist/src/flow/graph-metadata.js +5 -0
- package/dist/src/gates/providers/loader.js +64 -18
- package/dist/src/installation/manager-command.d.mts +18 -0
- package/dist/src/installation/manager-command.mjs +15 -3
- package/dist/src/installation/manager.d.mts +49 -0
- package/dist/src/installation/manager.mjs +95 -5
- package/dist/src/issues/cli.js +4 -4
- package/dist/src/issues/local.js +24 -5
- package/dist/src/jobs/runner.js +1 -1
- package/dist/src/lint/cli.js +27 -1
- package/dist/src/lint/loader.js +2 -1
- package/dist/src/lint/reporters.js +25 -1
- package/dist/src/lint/rule-activation.js +43 -0
- package/dist/src/lint/runner.js +137 -62
- package/dist/src/mcp/adapters/codex-runtime.js +366 -56
- package/dist/src/mcp/claude-install.d.mts +4 -0
- package/dist/src/mcp/claude-install.mjs +41 -0
- package/dist/src/mcp/cli.d.mts +1 -0
- package/dist/src/mcp/cli.mjs +620 -85
- package/dist/src/mcp/config-file.d.mts +28 -0
- package/dist/src/mcp/config-file.mjs +279 -0
- package/dist/src/mcp/config-layers.d.mts +36 -0
- package/dist/src/mcp/config-layers.mjs +348 -0
- package/dist/src/mcp/credentials.d.mts +51 -0
- package/dist/src/mcp/credentials.mjs +484 -0
- package/dist/src/mcp/grok-build-config.d.mts +6 -0
- package/dist/src/mcp/grok-build-config.mjs +17 -27
- package/dist/src/mcp/helpers.mjs +109 -4
- package/dist/src/mcp/muse-native-config.d.mts +13 -0
- package/dist/src/mcp/muse-native-config.mjs +1 -0
- package/dist/src/mcp/omp-config.d.mts +4 -0
- package/dist/src/mcp/omp-config.mjs +50 -29
- package/dist/src/mcp/profiles.js +97 -23
- package/dist/src/mcp/profiles.mjs +99 -27
- package/dist/src/mcp/registry.js +248 -131
- package/dist/src/mcp/registry.mjs +169 -100
- package/dist/src/mcp/server-config.d.mts +3 -0
- package/dist/src/mcp/server-config.mjs +89 -0
- package/dist/src/mcp/server.mjs +127 -82
- package/dist/src/mcp/test-simple.mjs +2 -1
- package/dist/src/mcp/toml-editor.d.mts +4 -0
- package/dist/src/mcp/toml-string.d.mts +1 -0
- package/dist/src/mcp/toml-string.mjs +9 -0
- package/dist/src/mcp/toml-strip-mcp.d.mts +1 -0
- package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
- package/dist/src/mcp/tool-filters.d.mts +46 -0
- package/dist/src/mcp/tool-filters.mjs +357 -0
- package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
- package/dist/src/mcp/tools/decision.mjs +22 -3
- package/dist/src/mcp/tools/discovery.mjs +62 -55
- package/dist/src/mcp/tools/orchestration.mjs +50 -2
- package/dist/src/mcp/tools/subsystems.mjs +39 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +2856 -0
- package/dist/src/mcp/tools/worker-shell-control.mjs +289 -0
- package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
- package/dist/src/memory/project-registry.js +2 -2
- package/dist/src/mission-protocol/codecs.js +647 -1
- package/dist/src/mission-protocol/types.js +110 -0
- package/dist/src/mission-protocol/worker-status.js +2 -0
- package/dist/src/models/config-loader.js +76 -14
- package/dist/src/models/grok-build-models.js +93 -41
- package/dist/src/models/model-capabilities.v1.json +3183 -24
- package/dist/src/models/model-discovery.js +931 -67
- package/dist/src/models/provider-policy.js +719 -11
- package/dist/src/models/qualification-digest.d.mts +2 -0
- package/dist/src/models/qualification-digest.mjs +17 -0
- package/dist/src/models/resolver.js +261 -0
- package/dist/src/models/worker-direct-readiness.js +550 -0
- package/dist/src/models/worker-dispatch-offer.js +169 -0
- package/dist/src/models/worker-health-readiness.js +144 -0
- package/dist/src/models/worker-installation.d.mts +39 -0
- package/dist/src/models/worker-installation.mjs +986 -0
- package/dist/src/models/wrapper-route.js +18 -0
- package/dist/src/ops/registry.js +9 -14
- package/dist/src/plugins/standalone-packager.js +4 -3
- package/dist/src/process/bounded-child-process.d.mts +84 -0
- package/dist/src/process/bounded-child-process.mjs +430 -0
- package/dist/src/process/cli-command.d.mts +16 -0
- package/dist/src/process/cli-command.mjs +75 -0
- package/dist/src/process/signal-child.d.mts +6 -0
- package/dist/src/process/signal-child.mjs +7 -0
- package/dist/src/providers/capability-matrix.js +8 -0
- package/dist/src/providers/capability-matrix.yaml +270 -0
- package/dist/src/providers/deployed-skill-roots.js +20 -0
- package/dist/src/providers/grok-build-acp.js +3 -2
- package/dist/src/providers/grok-build-headless.js +53 -23
- package/dist/src/providers/grok-build-inspect.js +1 -0
- package/dist/src/providers/omp-agent.d.mts +8 -0
- package/dist/src/providers/omp-diagnostics.d.mts +1 -0
- package/dist/src/providers/omp-paths.d.mts +13 -0
- package/dist/src/providers/provider-definitions.js +9 -1
- package/dist/src/providers/provider-definitions.mjs +9 -2
- package/dist/src/providers/skill-ownership.d.mts +9 -0
- package/dist/src/providers/skill-ownership.mjs +88 -0
- package/dist/src/rules/activation.d.mts +21 -0
- package/dist/src/rules/activation.mjs +62 -0
- package/dist/src/rules/inline-budget.d.mts +26 -0
- package/dist/src/rules/inline-budget.mjs +66 -0
- package/dist/src/rules/inventory.js +129 -0
- package/dist/src/rules/pin-state.js +73 -0
- package/dist/src/rules/pins.d.mts +14 -0
- package/dist/src/rules/pins.mjs +112 -0
- package/dist/src/rules/rule-frontmatter.schema.json +197 -0
- package/dist/src/serve/executor-registry.js +16 -24
- package/dist/src/serve/fleet-mission-conductor.js +9 -0
- package/dist/src/serve/mission-conductor.js +47 -12
- package/dist/src/serve/shared-host-scheduler.js +2 -0
- package/dist/src/serve/worker-invocation.js +558 -0
- package/dist/src/serve/worker-stdin-frame.d.mts +10 -0
- package/dist/src/serve/worker-stdin-frame.mjs +107 -0
- package/dist/src/sessions/adapter-registry.js +49 -0
- package/dist/src/sessions/adapters/claude.js +39 -54
- package/dist/src/sessions/adapters/codex.js +82 -67
- package/dist/src/sessions/adapters/copilot.js +4 -4
- package/dist/src/sessions/adapters/cursor.js +40 -57
- package/dist/src/sessions/adapters/deepseek-harness.js +15 -12
- package/dist/src/sessions/adapters/factory.js +31 -27
- package/dist/src/sessions/adapters/generic.js +43 -9
- package/dist/src/sessions/adapters/grok-build.js +11 -11
- package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
- package/dist/src/sessions/adapters/grokbot.js +434 -24
- package/dist/src/sessions/adapters/hermes.js +27 -29
- package/dist/src/sessions/adapters/muse-workspace-scan.js +128 -0
- package/dist/src/sessions/adapters/muse.js +200 -104
- package/dist/src/sessions/adapters/omp.js +28 -14
- package/dist/src/sessions/adapters/openclaw.js +89 -79
- package/dist/src/sessions/adapters/opencode.js +27 -24
- package/dist/src/sessions/adapters/openhuman.js +18 -60
- package/dist/src/sessions/adapters/pi.js +30 -14
- package/dist/src/sessions/adapters/warp.js +3 -3
- package/dist/src/sessions/adapters/windsurf.js +19 -48
- package/dist/src/sessions/batch-contracts.js +25 -5
- package/dist/src/sessions/batch-import.js +72 -51
- package/dist/src/sessions/candidates.js +47 -11
- package/dist/src/sessions/contracts.js +75 -2
- package/dist/src/sessions/coverage-preflight.js +134 -0
- package/dist/src/sessions/discovery.js +3 -0
- package/dist/src/sessions/export-plan.js +4 -3
- package/dist/src/sessions/grok-build-cli.js +67 -5
- package/dist/src/sessions/import-exclusions.js +72 -0
- package/dist/src/sessions/import-lease.js +34 -18
- package/dist/src/sessions/import-limits.js +37 -0
- package/dist/src/sessions/importer.js +84 -46
- package/dist/src/sessions/index.js +2 -0
- package/dist/src/sessions/lifecycle.js +45 -0
- package/dist/src/sessions/policy.js +133 -25
- package/dist/src/sessions/readers.js +106 -53
- package/dist/src/sessions/refresh.js +106 -0
- package/dist/src/sessions/repository.js +199 -63
- package/dist/src/sessions/workspace-discovery.js +315 -88
- package/dist/src/sessions/workspace-matching.js +107 -0
- package/dist/src/skills/adapters/clawhub.js +10 -8
- package/dist/src/skills/adapters/openclaw.js +10 -7
- package/dist/src/skills/importer.js +1 -0
- package/dist/src/smiths/context-pipeline/aiwg-md.js +11 -0
- package/dist/src/smiths/context-pipeline/claude-hook.js +3 -26
- package/dist/src/smiths/context-pipeline/finalization.js +6 -5
- package/dist/src/smiths/context-pipeline/generator.js +21 -4
- package/dist/src/smiths/context-pipeline/parallelism-section.js +195 -14
- package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
- package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
- package/dist/src/smiths/mcpsmith/example.js +1 -1
- package/dist/src/smiths/mcpsmith/generator.js +1 -1
- package/dist/src/smiths/toolsmith/runtime-discovery.d.mts +10 -0
- package/dist/src/smiths/toolsmith/runtime-discovery.mjs +85 -31
- package/dist/src/storage/backends/fs.js +18 -76
- package/dist/src/storage/backends/logseq.js +68 -7
- package/dist/src/storage/backends/obsidian.js +1 -0
- package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
- package/dist/src/testing/generators/test-code-generator.js +57 -30
- package/dist/src/tracker/forge-http.js +2 -1
- package/dist/src/uhp/client.js +11 -1
- package/dist/src/uhp/mission.js +5 -0
- package/dist/src/update/checker.d.mts +2 -0
- package/dist/src/update/checker.mjs +5 -5
- package/dist/src/update/notifier.mjs +54 -2
- package/dist/src/update/service.d.mts +22 -0
- package/dist/src/utils/contained-filesystem.d.mts +37 -0
- package/dist/src/utils/contained-filesystem.mjs +683 -0
- package/dist/src/utils/temporary-directory.js +36 -0
- package/dist/src/writing/example-generator.js +1 -1
- package/dist/src/writing/fidelity.js +18 -1
- package/package.json +32 -2
- package/schemas/mission-v1.schema.json +2429 -0
- package/schemas/models/model-catalog.v1.schema.json +301 -0
- package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
- package/schemas/models/model-policy.v1.schema.json +14 -0
- package/schemas/models/provider-model-capabilities.v1.schema.json +1072 -0
- package/tools/_resolve-impl.mjs +39 -2
- package/tools/agents/deploy-agents.mjs +64 -23
- package/tools/agents/providers/base.mjs +153 -166
- package/tools/agents/providers/claude.mjs +8 -24
- package/tools/agents/providers/codex.mjs +18 -11
- package/tools/agents/providers/cursor.mjs +19 -18
- package/tools/agents/providers/grok-build.mjs +3 -1
- package/tools/agents/providers/grokbot.mjs +11 -12
- package/tools/agents/providers/hermes.mjs +62 -49
- package/tools/agents/providers/omp.mjs +5 -2
- package/tools/agents/providers/openclaw-translator.mjs +19 -5
- package/tools/agents/providers/openclaw.mjs +9 -4
- package/tools/agents/providers/pi.mjs +5 -0
- package/tools/agents/providers/provider-prepare-channel.mjs +5 -0
- package/tools/agents/providers/provider-prepare-coordinator.mjs +247 -0
- package/tools/agents/providers/provider-prepare-worker.mjs +84 -0
- package/tools/agents/providers/rule-delivery.mjs +1270 -0
- package/tools/agents/providers/warp.mjs +20 -26
- package/tools/cli/decision-offload-report.mjs +40 -0
- package/tools/cli/workspace-status.mjs +862 -0
- package/tools/plugin/package-plugins.mjs +186 -8
- package/tools/plugin/plugin-links.mjs +114 -0
- package/tools/providers/antigravity-transport.mjs +3 -1
- package/tools/providers/deepseek-harness-transport.mjs +360 -0
- package/tools/providers/omp-transport.mjs +194 -0
- package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
- package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
- package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +1748 -0
- package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
- package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
- package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +55 -0
- package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
- package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
- package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
- package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
- package/tools/ralph-external/lib/pi-adapter.mjs +329 -0
- package/tools/ralph-external/lib/provider-adapter.mjs +748 -0
- package/tools/ralph-external/lib/recovery-probe.mjs +72 -0
- package/tools/ralph-external/lib/session-environment.mjs +28 -0
- package/tools/ralph-external/lib/systemd-client.mjs +49 -0
- package/tools/ralph-external/lib/worker-health.mjs +439 -0
- package/tools/ralph-external/lib/worker-isolation.mjs +717 -0
- package/tools/ralph-external/lib/worker-overflow.mjs +50 -0
- package/tools/ralph-external/process-monitor.mjs +1017 -0
- package/tools/ralph-external/session-launcher.mjs +5057 -0
- package/tools/scripts/resource-preflight.mjs +541 -0
- package/tools/skills/deploy-skills-codex.mjs +2 -1
- 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
|
|
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
|
|
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 {
|
|
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 =
|
|
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
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
92
|
-
baseConfig = raw
|
|
93
|
-
.replace(/\[mcp_servers\.[^\]]+\][\s\S]*?(?=\n\[|\s*$)/g, '')
|
|
94
|
-
.trimEnd();
|
|
129
|
+
baseConfig = stripMcpServers(raw).trimEnd();
|
|
95
130
|
}
|
|
96
|
-
catch {
|
|
97
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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>;
|