@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
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP credential rendering and policy.
|
|
3
|
+
*
|
|
4
|
+
* A registry entry carries credentials in two ways:
|
|
5
|
+
* - literal values in `env` / `headers` (and OMP `auth` / `oauth` secrets)
|
|
6
|
+
* - references: `headerEnv` (header -> variable) and `envFrom`
|
|
7
|
+
* (server variable -> variable), which name a variable the harness
|
|
8
|
+
* resolves when it starts the server. Only the variable name is written.
|
|
9
|
+
*
|
|
10
|
+
* Each harness spells a reference differently; ENV_REFERENCE_SYNTAX holds the
|
|
11
|
+
* documented form. `null` means the harness documents no interpolation in its
|
|
12
|
+
* MCP config, so a reference cannot be rendered and injection refuses.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const ENV_REFERENCE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Keyed by normalized MCP provider id.
|
|
19
|
+
* Sources (checked 2026-10-03):
|
|
20
|
+
* claude-code https://code.claude.com/docs/en/mcp (command, args, env, url, headers)
|
|
21
|
+
* cursor https://cursor.com/docs/context/mcp (command, args, env, url, headers)
|
|
22
|
+
* windsurf https://docs.devin.ai/desktop/cascade/mcp (command, args, env, url, headers)
|
|
23
|
+
* factory https://docs.factory.com/cli/configuration/mcp (env and headers values only)
|
|
24
|
+
* opencode https://opencode.ai/docs/config (any string value)
|
|
25
|
+
* antigravity https://antigravity.google/docs/mcp (no interpolation documented)
|
|
26
|
+
* warp https://docs.warp.dev/agent-platform/capabilities/mcp/ (no interpolation documented)
|
|
27
|
+
* codex named keys instead of interpolation (env_vars, env_http_headers)
|
|
28
|
+
*/
|
|
29
|
+
export const ENV_REFERENCE_SYNTAX = Object.freeze({
|
|
30
|
+
'claude-code': '${NAME}',
|
|
31
|
+
claude: '${NAME}',
|
|
32
|
+
cursor: '${env:NAME}',
|
|
33
|
+
windsurf: '${env:NAME}',
|
|
34
|
+
factory: '${NAME}',
|
|
35
|
+
opencode: '{env:NAME}',
|
|
36
|
+
omp: '${NAME}',
|
|
37
|
+
'grok-build': '${NAME}',
|
|
38
|
+
antigravity: null,
|
|
39
|
+
agy: null,
|
|
40
|
+
warp: null,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const CREDENTIAL_POLICIES = Object.freeze(['literal', 'references', 'none']);
|
|
44
|
+
|
|
45
|
+
export function validateEnvReferenceName(name) {
|
|
46
|
+
if (!ENV_REFERENCE_NAME.test(name)) {
|
|
47
|
+
throw new Error(`Invalid MCP environment variable reference "${name}"`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hasEntries(value) {
|
|
52
|
+
return value !== null && typeof value === 'object' && Object.keys(value).length > 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function hasReferences(server) {
|
|
56
|
+
return hasEntries(server.headerEnv) || hasEntries(server.envFrom);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Render the env and headers maps a JSON harness receives, with references in
|
|
61
|
+
* the harness's own syntax. Throws when the server carries references and the
|
|
62
|
+
* harness has no documented syntax for them.
|
|
63
|
+
*/
|
|
64
|
+
export function renderCredentialMaps(server, provider) {
|
|
65
|
+
const env = server.env ? { ...server.env } : undefined;
|
|
66
|
+
const headers = server.headers ? { ...server.headers } : undefined;
|
|
67
|
+
if (!hasReferences(server)) return { env, headers };
|
|
68
|
+
|
|
69
|
+
const syntax = ENV_REFERENCE_SYNTAX[provider];
|
|
70
|
+
if (!syntax) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`MCP server "${server.name}" uses environment references (header-env/env-from), ` +
|
|
73
|
+
`but ${provider} documents no environment interpolation in its MCP config. ` +
|
|
74
|
+
'Refusing to render it rather than writing the secret value or dropping the reference.',
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
const reference = name => {
|
|
78
|
+
validateEnvReferenceName(name);
|
|
79
|
+
return syntax.replace('NAME', name);
|
|
80
|
+
};
|
|
81
|
+
const renderedEnv = { ...(env || {}) };
|
|
82
|
+
for (const [key, name] of Object.entries(server.envFrom || {})) renderedEnv[key] = reference(name);
|
|
83
|
+
const renderedHeaders = { ...(headers || {}) };
|
|
84
|
+
for (const [header, name] of Object.entries(server.headerEnv || {})) renderedHeaders[header] = reference(name);
|
|
85
|
+
return {
|
|
86
|
+
env: Object.keys(renderedEnv).length > 0 ? renderedEnv : undefined,
|
|
87
|
+
headers: Object.keys(renderedHeaders).length > 0 ? renderedHeaders : undefined,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// A supported environment-reference value in any documented harness syntax:
|
|
92
|
+
// ${NAME} (claude/factory/omp/grok), ${env:NAME} (cursor/windsurf),
|
|
93
|
+
// {env:NAME} (opencode). Such a value names a variable; it is not a secret.
|
|
94
|
+
const CREDENTIAL_REFERENCE_VALUE = /^(?:\$\{[A-Za-z_][A-Za-z0-9_]*\}|\$\{env:[A-Za-z_][A-Za-z0-9_]*\}|\{env:[A-Za-z_][A-Za-z0-9_]*\})$/;
|
|
95
|
+
|
|
96
|
+
/** True when the value is a supported reference form rather than a literal secret. */
|
|
97
|
+
export function isCredentialReferenceValue(value) {
|
|
98
|
+
return typeof value === 'string' && CREDENTIAL_REFERENCE_VALUE.test(value);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// One scanner supplies both policy findings and display replacements. Spans
|
|
102
|
+
// retain offsets into the original text so malformed URLs and encoded keys
|
|
103
|
+
// cannot disappear during URL normalization.
|
|
104
|
+
const CREDENTIAL_ARG_NAME = /^(api[-_]?key|access[-_]?key|access[-_]?token|auth[-_]?token|client[-_]?secret|secret|password|passwd|token)$/i;
|
|
105
|
+
const CREDENTIAL_QUERY_NAME = /(?:^|[-_.])(?:api[-_]?key|access[-_]?key|private[-_]?key|signing[-_]?key|encryption[-_]?key|secret[-_]?key|api[-_]?token|access[-_]?token|auth[-_]?token|session[-_]?id|client[-_]?secret|token|secret|password|passwd|pwd|passcode|auth|authorization|signature|jwt|bearer|credential|credentials)(?:$|[-_.])/i;
|
|
106
|
+
|
|
107
|
+
function credentialName(key) {
|
|
108
|
+
const normalized = key.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
109
|
+
const parts = normalized.split(/[.\[\]]/).filter(Boolean);
|
|
110
|
+
// Dot and bracket nesting are structural; underscores/hyphens are word
|
|
111
|
+
// separators, so sort_key/cache-key stay ordinary parameter names.
|
|
112
|
+
return parts.some((part, index) =>
|
|
113
|
+
// Location and mode suffixes exclude only this part, not credential ancestors.
|
|
114
|
+
!/(?:^|[-_])(?:file|path|dir|directory|name|mode|type|location|url|uri)$/.test(part)
|
|
115
|
+
&& ((/^(?:key|sig)$/.test(part) && !parts.slice(0, index).some(parent => /^(?:sort|order|cache)$/.test(parent)))
|
|
116
|
+
|| CREDENTIAL_QUERY_NAME.test(part.replace(/[_\s]+/g, '-'))));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function literalValue(value, includeReferences) {
|
|
120
|
+
return value !== '' && (includeReferences || !isCredentialReferenceValue(value));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function scanCredentialText(value, { includeReferences = false } = {}) {
|
|
124
|
+
if (typeof value !== 'string') return [];
|
|
125
|
+
const hits = [];
|
|
126
|
+
// Authority-only userinfo, including URLs embedded in --url=... argv.
|
|
127
|
+
for (const match of value.matchAll(/[A-Za-z][A-Za-z0-9+.-]*:\/\/([^/?#]*)/g)) {
|
|
128
|
+
const at = match[1].lastIndexOf('@');
|
|
129
|
+
if (at >= 0) {
|
|
130
|
+
const start = match.index + match[0].length - match[1].length;
|
|
131
|
+
hits.push({ start, end: start + at, kind: 'userinfo', credential: true });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// WHATWG URL parsing accepts special-scheme authorities with one/no slash
|
|
135
|
+
// or backslashes. Use its userinfo decision, retaining original-text offsets
|
|
136
|
+
// for redaction. The raw scan above still covers malformed hosts.
|
|
137
|
+
const offsets = [];
|
|
138
|
+
let normalized = '';
|
|
139
|
+
for (let index = 0; index < value.length; index++) {
|
|
140
|
+
if (/[\t\r\n]/.test(value[index])) continue;
|
|
141
|
+
offsets.push(index);
|
|
142
|
+
normalized += value[index];
|
|
143
|
+
}
|
|
144
|
+
for (const match of normalized.matchAll(/[A-Za-z][A-Za-z0-9+.-]*:/g)) {
|
|
145
|
+
try {
|
|
146
|
+
const candidate = normalized.slice(match.index);
|
|
147
|
+
const parsed = new URL(candidate);
|
|
148
|
+
if (!parsed.username && !parsed.password) continue;
|
|
149
|
+
const authority = /^[A-Za-z][A-Za-z0-9+.-]*:[/\\]*([^/\\?#]*)/.exec(candidate);
|
|
150
|
+
const at = authority?.[1].lastIndexOf('@') ?? -1;
|
|
151
|
+
if (at < 0) continue;
|
|
152
|
+
const start = match.index + authority[0].length - authority[1].length;
|
|
153
|
+
hits.push({ start: offsets[start], end: offsets[start + at], kind: 'userinfo', credential: true });
|
|
154
|
+
} catch { /* Malformed URLs are handled by the raw scanner. */ }
|
|
155
|
+
}
|
|
156
|
+
const hash = value.indexOf('#');
|
|
157
|
+
const query = value.indexOf('?');
|
|
158
|
+
const regions = [];
|
|
159
|
+
if (query >= 0 && (hash < 0 || query < hash)) regions.push({ start: query + 1, end: hash < 0 ? value.length : hash, kind: 'query' });
|
|
160
|
+
if (hash >= 0) regions.push({ start: hash + 1, end: value.length, kind: 'fragment' });
|
|
161
|
+
for (const region of regions) {
|
|
162
|
+
// Scan both sides of '?' in a fragment: #token=secret?view=full and
|
|
163
|
+
// #/callback?token=secret must both retain the secret assignment.
|
|
164
|
+
const text = value.slice(region.start, region.end);
|
|
165
|
+
for (const match of text.matchAll(region.kind === 'fragment' ? /[^&?]+/g : /[^&]+/g)) {
|
|
166
|
+
const equals = match[0].indexOf('=');
|
|
167
|
+
if (equals < 0) continue;
|
|
168
|
+
const start = region.start + match.index + equals + 1;
|
|
169
|
+
// '?' can start fragment routing, but it is also part of an assignment
|
|
170
|
+
// value. Classify the entire value before deciding whether to redact it.
|
|
171
|
+
const nextPair = value.indexOf('&', start);
|
|
172
|
+
const valueEnd = region.kind === 'fragment'
|
|
173
|
+
? (nextPair < 0 ? region.end : nextPair)
|
|
174
|
+
: region.start + match.index + match[0].length;
|
|
175
|
+
const assignment = match[0].slice(0, equals + 1) + value.slice(start, valueEnd);
|
|
176
|
+
const [[key, decoded]] = new URLSearchParams(assignment.replace(/[\t\r\n]/g, ''));
|
|
177
|
+
const credential = credentialName(key) && literalValue(decoded, includeReferences);
|
|
178
|
+
// A credential value may itself contain '?'. Redact the whole value
|
|
179
|
+
// through the next '&', including any fragment routing suffix.
|
|
180
|
+
const end = region.kind === 'fragment' && credential
|
|
181
|
+
? valueEnd
|
|
182
|
+
: region.start + match.index + match[0].length;
|
|
183
|
+
hits.push({ start, end, kind: region.kind, key, credential });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const header = /^\s*([^:]+):\s*(.*)$/.exec(value);
|
|
187
|
+
if (header && credentialName(header[1])) {
|
|
188
|
+
const content = header[2];
|
|
189
|
+
const credential = content.replace(/^(?:Bearer|Basic)\s+/i, '');
|
|
190
|
+
if (literalValue(credential, includeReferences)) {
|
|
191
|
+
hits.push({ start: value.length - content.length, end: value.length, kind: 'header', credential: true });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const assignment = /^(?:--header=|-H=?)?([A-Za-z_][A-Za-z0-9_.-]*)=(.*)$/s.exec(value);
|
|
195
|
+
if (assignment && credentialName(assignment[1]) && literalValue(assignment[2], includeReferences)) {
|
|
196
|
+
hits.push({ start: value.length - assignment[2].length, end: value.length, kind: 'assignment', credential: true });
|
|
197
|
+
}
|
|
198
|
+
// curl accepts both --header=Authorization: ... and -HAuthorization: ... .
|
|
199
|
+
const inlineHeader = /^(?:--header=|-H=?)(.+:.+)$/s.exec(value);
|
|
200
|
+
if (inlineHeader) {
|
|
201
|
+
const offset = value.length - inlineHeader[1].length;
|
|
202
|
+
for (const hit of scanCredentialText(inlineHeader[1], { includeReferences })) {
|
|
203
|
+
hits.push({ ...hit, start: offset + hit.start, end: offset + hit.end });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return hits;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function redactSpans(value, hits) {
|
|
210
|
+
if (typeof value !== 'string') return value;
|
|
211
|
+
const sorted = hits.filter(hit => hit.end > hit.start).sort((a, b) => a.start - b.start);
|
|
212
|
+
const spans = [];
|
|
213
|
+
for (const hit of sorted) {
|
|
214
|
+
const previous = spans.at(-1);
|
|
215
|
+
if (previous && hit.start <= previous.end) previous.end = Math.max(previous.end, hit.end);
|
|
216
|
+
else spans.push({ start: hit.start, end: hit.end });
|
|
217
|
+
}
|
|
218
|
+
let result = '';
|
|
219
|
+
let end = 0;
|
|
220
|
+
for (const span of spans) {
|
|
221
|
+
result += value.slice(end, span.start) + '***';
|
|
222
|
+
end = span.end;
|
|
223
|
+
}
|
|
224
|
+
return result + value.slice(end);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Redact URL, fragment, header, and assignment credentials in display text. */
|
|
228
|
+
export function redactCredentialText(value) {
|
|
229
|
+
if (typeof value !== 'string') return value;
|
|
230
|
+
const hits = scanCredentialText(value);
|
|
231
|
+
return redactSpans(value, hits.filter(hit => hit.credential));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function credentialArgHits(args, options = {}) {
|
|
235
|
+
const hits = [];
|
|
236
|
+
for (let index = 0; index < args.length; index++) {
|
|
237
|
+
const arg = args[index];
|
|
238
|
+
if (typeof arg !== 'string') continue;
|
|
239
|
+
for (const hit of scanCredentialText(arg, options)) {
|
|
240
|
+
if (hit.credential) hits.push({ ...hit, index, length: 1 });
|
|
241
|
+
}
|
|
242
|
+
const flag = /^--?([^=]+)(?:=(.*))?$/s.exec(arg);
|
|
243
|
+
if (!flag || !CREDENTIAL_ARG_NAME.test(flag[1])) continue;
|
|
244
|
+
if (flag[2] !== undefined) {
|
|
245
|
+
if (literalValue(flag[2], options.includeReferences)) {
|
|
246
|
+
hits.push({ index, length: 1, start: arg.indexOf('=') + 1, end: arg.length });
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
const next = args[index + 1];
|
|
250
|
+
if (typeof next === 'string' && !next.startsWith('--') && literalValue(next, options.includeReferences)) {
|
|
251
|
+
hits.push({ index, length: 2, start: 0, end: next.length });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return hits;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function credentialLikeArgs(args, options = {}) {
|
|
259
|
+
if (!Array.isArray(args)) return [];
|
|
260
|
+
return [...new Set(credentialArgHits(args, options).map(({ index, length }) =>
|
|
261
|
+
length === 1 ? args[index] : `${args[index]} ${args[index + 1]}`))];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function redactCredentialArgs(args) {
|
|
265
|
+
if (!Array.isArray(args)) return args;
|
|
266
|
+
const byIndex = new Map();
|
|
267
|
+
for (const hit of credentialArgHits(args)) {
|
|
268
|
+
const index = hit.index + hit.length - 1;
|
|
269
|
+
if (!byIndex.has(index)) byIndex.set(index, []);
|
|
270
|
+
byIndex.get(index).push(hit);
|
|
271
|
+
}
|
|
272
|
+
return args.map((arg, index) => redactSpans(arg, byIndex.get(index) || []));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function urlCarriesUserinfo(url) {
|
|
276
|
+
return scanCredentialText(url).some(hit => hit.kind === 'userinfo' && hit.credential);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function credentialLikeQueryParams(url, options = {}) {
|
|
280
|
+
return [...new Set(scanCredentialText(url, options).filter(hit => hit.kind === 'query' && hit.credential).map(hit => hit.key))];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function credentialLikeFragmentParams(url, options = {}) {
|
|
284
|
+
return [...new Set(scanCredentialText(url, options).filter(hit => hit.kind === 'fragment' && hit.credential).map(hit => hit.key))];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Recursively redact display records, including JSON output and literal maps. */
|
|
288
|
+
export function redactCredentialRecord(value, field) {
|
|
289
|
+
if (field === 'args') return redactCredentialArgs(value);
|
|
290
|
+
if (Array.isArray(value)) return value.map(item => redactCredentialRecord(item));
|
|
291
|
+
if (value && typeof value === 'object') {
|
|
292
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key,
|
|
293
|
+
['env', 'headers'].includes(field) || key === 'clientSecret'
|
|
294
|
+
? (item === '' || isCredentialReferenceValue(item) ? item : '***')
|
|
295
|
+
: redactCredentialRecord(item, key)]));
|
|
296
|
+
}
|
|
297
|
+
return redactCredentialText(value);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* List the fields of a server that the policy forbids.
|
|
302
|
+
*
|
|
303
|
+
* literal nothing is forbidden
|
|
304
|
+
* references literal env/headers values, literal auth argument values, URL
|
|
305
|
+
* userinfo/query/fragment credentials, and literal auth/oauth secrets are forbidden;
|
|
306
|
+
* header-env/env-from are allowed
|
|
307
|
+
* none every credential-bearing field is forbidden, references included
|
|
308
|
+
*/
|
|
309
|
+
export function credentialPolicyViolations(server, policy) {
|
|
310
|
+
if (policy === 'literal' || policy === undefined) return [];
|
|
311
|
+
const violations = [];
|
|
312
|
+
if (hasEntries(server.env)) violations.push('env');
|
|
313
|
+
if (hasEntries(server.headers)) violations.push('headers');
|
|
314
|
+
if (credentialLikeArgs(server.args, { includeReferences: policy === 'none' }).length > 0) violations.push('args');
|
|
315
|
+
if (urlCarriesUserinfo(server.url)) violations.push('url userinfo');
|
|
316
|
+
if (credentialLikeQueryParams(server.url, { includeReferences: policy === 'none' }).length) violations.push('url query credentials');
|
|
317
|
+
if (credentialLikeFragmentParams(server.url, { includeReferences: policy === 'none' }).length) violations.push('url fragment credentials');
|
|
318
|
+
for (const field of ['auth', 'oauth']) {
|
|
319
|
+
for (const key of ['tokenUrl', 'redirectUri', 'resource']) {
|
|
320
|
+
if (urlCarriesUserinfo(server[field]?.[key])) violations.push(`${field}.${key} userinfo`);
|
|
321
|
+
if (credentialLikeQueryParams(server[field]?.[key], { includeReferences: policy === 'none' }).length) {
|
|
322
|
+
violations.push(`${field}.${key} query credentials`);
|
|
323
|
+
}
|
|
324
|
+
if (credentialLikeFragmentParams(server[field]?.[key], { includeReferences: policy === 'none' }).length) {
|
|
325
|
+
violations.push(`${field}.${key} fragment credentials`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (server.auth?.clientSecret) violations.push('auth.clientSecret');
|
|
330
|
+
if (server.oauth?.clientSecret) violations.push('oauth.clientSecret');
|
|
331
|
+
if (policy === 'none') {
|
|
332
|
+
if (hasEntries(server.headerEnv)) violations.push('headerEnv');
|
|
333
|
+
if (hasEntries(server.envFrom)) violations.push('envFrom');
|
|
334
|
+
if (server.auth) violations.push('auth');
|
|
335
|
+
if (server.oauth) violations.push('oauth');
|
|
336
|
+
}
|
|
337
|
+
return [...new Set(violations)];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Throw if any server violates the policy; names every offending server and field. */
|
|
341
|
+
export function assertCredentialPolicy(servers, policy) {
|
|
342
|
+
if (!CREDENTIAL_POLICIES.includes(policy ?? 'literal')) {
|
|
343
|
+
throw new Error(`Unknown MCP credential policy "${policy}". Use one of: ${CREDENTIAL_POLICIES.join(', ')}`);
|
|
344
|
+
}
|
|
345
|
+
const offenders = servers
|
|
346
|
+
.map(server => ({ name: server.name, fields: credentialPolicyViolations(server, policy) }))
|
|
347
|
+
.filter(entry => entry.fields.length > 0);
|
|
348
|
+
if (offenders.length === 0) return;
|
|
349
|
+
const detail = offenders.map(entry => `${entry.name} (${entry.fields.join(', ')})`).join('; ');
|
|
350
|
+
const remedy = policy === 'none'
|
|
351
|
+
? 'Credential policy "none" renders no credential-bearing field at all.'
|
|
352
|
+
: 'Credential policy "references" renders only header-env/env-from references; move literal values into environment variables.';
|
|
353
|
+
throw new Error(`Refusing to render MCP servers with credentials: ${detail}. ${remedy}`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Relaxations can be encountered repeatedly while loading layers and rendering.
|
|
357
|
+
const warnedRelaxations = new Set();
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* `source`, when given, is the enforcing layer's config file path (never a
|
|
361
|
+
* credential value - just a filesystem path) and is named in the message so
|
|
362
|
+
* the operator knows which layer to change. Deduplicates once per process
|
|
363
|
+
* per attempted `candidate` value, independent of `source`.
|
|
364
|
+
*/
|
|
365
|
+
export function warnCredentialPolicyRelaxation(candidate, floor, source) {
|
|
366
|
+
if (warnedRelaxations.has(candidate)) return;
|
|
367
|
+
warnedRelaxations.add(candidate);
|
|
368
|
+
const location = source ? ` (set in ${source})` : '';
|
|
369
|
+
console.warn(`Ignoring MCP credential policy "${candidate}": cannot relax credential policy floor "${floor}"${location}.`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Tracked separately from warnedRelaxations: this is a distinct message
|
|
373
|
+
// (the overlay is being dropped from the write layer on save), not the
|
|
374
|
+
// resolver's generic relaxation message, but follows the same once-per-value rule.
|
|
375
|
+
const warnedDroppedOverlays = new Set();
|
|
376
|
+
|
|
377
|
+
export function warnDroppedOverlayPolicy(candidate, floor, source) {
|
|
378
|
+
if (warnedDroppedOverlays.has(candidate)) return;
|
|
379
|
+
warnedDroppedOverlays.add(candidate);
|
|
380
|
+
const location = source ? ` (set in ${source})` : '';
|
|
381
|
+
console.warn(`Dropping ignored MCP overlay credential policy "${candidate}": cannot relax credential policy floor "${floor}"${location}.`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Test-only: clears the per-process relaxation-warning dedup state. Each of
|
|
386
|
+
* these warnings is designed to fire once per process per attempted value,
|
|
387
|
+
* which is exactly what a long-running CLI/session process needs - but a
|
|
388
|
+
* test file that exercises the same candidate value more than once (for
|
|
389
|
+
* example running the same scenario against both the `.ts` and `.mjs`
|
|
390
|
+
* registry implementations) needs to reset it between cases to observe the
|
|
391
|
+
* warning each time, since both implementations share this module's state.
|
|
392
|
+
*/
|
|
393
|
+
export function resetCredentialPolicyWarnings() {
|
|
394
|
+
warnedRelaxations.clear();
|
|
395
|
+
warnedDroppedOverlays.clear();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function validatePolicyValue(candidate) {
|
|
399
|
+
if (!CREDENTIAL_POLICIES.includes(candidate)) {
|
|
400
|
+
throw new Error(`Unknown MCP credential policy "${candidate}". Use one of: ${CREDENTIAL_POLICIES.join(', ')}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Resolve a credential policy for one invocation.
|
|
406
|
+
*
|
|
407
|
+
* `registryPolicy` (the personal/stored policy), `env`
|
|
408
|
+
* (AIWG_MCP_CREDENTIAL_POLICY) and `flag` compose as a default: the most
|
|
409
|
+
* specific source wins outright, including a relaxation. `flag` overrides
|
|
410
|
+
* `env`, which overrides `registryPolicy`, which overrides the built-in
|
|
411
|
+
* `literal` default - a flag or the environment may freely relax or tighten
|
|
412
|
+
* a personal stored policy.
|
|
413
|
+
*
|
|
414
|
+
* `floorPolicy`, when given, is an enforced policy from a lower (shared or
|
|
415
|
+
* organisation) configuration layer - the strictest value declared in any
|
|
416
|
+
* such layer. It cannot be relaxed by `registryPolicy`, `env` or `flag`,
|
|
417
|
+
* only tightened further. An attempt to relax below it is ignored, with a
|
|
418
|
+
* warning once per process per attempted value, and the floor is used
|
|
419
|
+
* instead. `floorSource` (the enforcing layer's config file path) is named
|
|
420
|
+
* in that warning when given.
|
|
421
|
+
*/
|
|
422
|
+
export function resolveCredentialPolicy({ flag, registryPolicy, floorPolicy, floorSource, env = process.env } = {}) {
|
|
423
|
+
let policy = 'literal';
|
|
424
|
+
// Whether any of registryPolicy/env/flag actually supplied a value, as
|
|
425
|
+
// opposed to `policy` merely sitting at the built-in default. The default
|
|
426
|
+
// landing below an enforced floor is ordinary inheritance, not a
|
|
427
|
+
// relaxation attempt, and must not warn.
|
|
428
|
+
let explicit = false;
|
|
429
|
+
// An empty environment string is unset; empty layer/flag values are errors.
|
|
430
|
+
const envPolicy = env.AIWG_MCP_CREDENTIAL_POLICY === '' ? undefined : env.AIWG_MCP_CREDENTIAL_POLICY;
|
|
431
|
+
for (const candidate of [registryPolicy, envPolicy, flag]) {
|
|
432
|
+
if (candidate === undefined) continue;
|
|
433
|
+
validatePolicyValue(candidate);
|
|
434
|
+
policy = candidate;
|
|
435
|
+
explicit = true;
|
|
436
|
+
}
|
|
437
|
+
if (floorPolicy !== undefined) {
|
|
438
|
+
validatePolicyValue(floorPolicy);
|
|
439
|
+
if (CREDENTIAL_POLICIES.indexOf(policy) < CREDENTIAL_POLICIES.indexOf(floorPolicy)) {
|
|
440
|
+
if (explicit) warnCredentialPolicyRelaxation(policy, floorPolicy, floorSource);
|
|
441
|
+
policy = floorPolicy;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return policy;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* The stricter of two credential policies; `undefined` means no preference
|
|
449
|
+
* and defers to the other side. Folds an organisation floor across several
|
|
450
|
+
* lower configuration layers (each layer's own declared value is validated).
|
|
451
|
+
*/
|
|
452
|
+
export function stricterCredentialPolicy(a, b) {
|
|
453
|
+
if (a !== undefined) validatePolicyValue(a);
|
|
454
|
+
if (b !== undefined) validatePolicyValue(b);
|
|
455
|
+
if (a === undefined) return b;
|
|
456
|
+
if (b === undefined) return a;
|
|
457
|
+
return CREDENTIAL_POLICIES.indexOf(a) >= CREDENTIAL_POLICIES.indexOf(b) ? a : b;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Env keys install-generated server definitions may carry as literal values
|
|
462
|
+
* without tripping credential-policy checks, keyed by the server name that
|
|
463
|
+
* may carry them. `AIWG_ROOT` is an install-generated, non-secret filesystem
|
|
464
|
+
* path (the AIWG installation directory) embedded in the built-in "aiwg"
|
|
465
|
+
* server's own `env`, not an authorization credential - unlike a registry
|
|
466
|
+
* server's `env`, which always comes from the operator and is never exempt.
|
|
467
|
+
* The exemption is scoped to this exact server name and key; it does not
|
|
468
|
+
* weaken the check for any other env/header field or any other server.
|
|
469
|
+
*/
|
|
470
|
+
export const INSTALL_POLICY_EXEMPT_ENV = Object.freeze({ aiwg: Object.freeze(['AIWG_ROOT']) });
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* View an install-generated server definition for a credential-policy
|
|
474
|
+
* check, with `INSTALL_POLICY_EXEMPT_ENV[name]` removed from `env` first.
|
|
475
|
+
* Use only for install-generated definitions (`aiwg mcp install`); registry
|
|
476
|
+
* servers rendered by `aiwg mcp inject` must never pass through this - their
|
|
477
|
+
* `env` is operator-supplied and always subject to the full check.
|
|
478
|
+
*/
|
|
479
|
+
export function installPolicyCheckedServer(name, server) {
|
|
480
|
+
const exempt = INSTALL_POLICY_EXEMPT_ENV[name];
|
|
481
|
+
if (!exempt || !hasEntries(server.env)) return { name, ...server };
|
|
482
|
+
const env = Object.fromEntries(Object.entries(server.env).filter(([key]) => !exempt.includes(key)));
|
|
483
|
+
return { name, ...server, env: Object.keys(env).length > 0 ? env : undefined };
|
|
484
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function renderGrokMcpServer(server: any): string;
|
|
2
|
+
export function mergeGrokMcpServers(text: string, servers: any[]): string;
|
|
3
|
+
export function removeGrokMcpServers(text: string, names: string[]): { text: string; removed: string[] };
|
|
4
|
+
export function manageGrokBuildMcp(configPath: string, servers: any[], options?: any): Promise<any>;
|
|
5
|
+
export function unmanageGrokBuildMcp(configPath: string, names: string[], options?: any): Promise<any>;
|
|
6
|
+
export function inspectGrokBuildNative(options?: any): Promise<any>;
|
|
@@ -1,18 +1,15 @@
|
|
|
1
|
+
import { writeConfigAtomic } from './config-file.mjs';
|
|
1
2
|
import { constants } from 'node:fs';
|
|
2
|
-
import { access, lstat,
|
|
3
|
+
import { access, lstat, readFile } from 'node:fs/promises';
|
|
3
4
|
import { dirname, relative, resolve } from 'node:path';
|
|
4
5
|
import { spawnSync } from 'node:child_process';
|
|
5
6
|
import { replaceServer } from './toml-editor.mjs';
|
|
7
|
+
import { tomlString as string } from './toml-string.mjs';
|
|
6
8
|
|
|
7
9
|
const START = name => `# >>> AIWG-managed Grok MCP: ${name}`;
|
|
8
10
|
const END = name => `# <<< AIWG-managed Grok MCP: ${name}`;
|
|
9
11
|
const ENV_REF = /^(?:[^$]|\$\{[A-Za-z_][A-Za-z0-9_]*(?::-[^}]*)?\})*$/;
|
|
10
12
|
|
|
11
|
-
function string(value) {
|
|
12
|
-
if (typeof value !== 'string') throw new Error('Grok MCP string fields must be strings');
|
|
13
|
-
return JSON.stringify(value).replace(/\u007f/g, '\\u007f');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
13
|
function key(value) {
|
|
17
14
|
return /^[A-Za-z0-9_-]+$/.test(value) ? value : string(value);
|
|
18
15
|
}
|
|
@@ -28,7 +25,11 @@ export function renderGrokMcpServer(server) {
|
|
|
28
25
|
if (!server.command) throw new Error(`Grok MCP stdio server "${server.name}" requires command`);
|
|
29
26
|
lines.push(`command = ${string(server.command)}`);
|
|
30
27
|
if (server.args?.length) lines.push(`args = [${server.args.map(string).join(', ')}]`);
|
|
31
|
-
|
|
28
|
+
const env = { ...(server.env || {}) };
|
|
29
|
+
for (const [key, envName] of Object.entries(server.envFrom || {})) {
|
|
30
|
+
env[key] = `\${${envName}}`;
|
|
31
|
+
}
|
|
32
|
+
if (Object.keys(env).length) lines.push(`env = ${inline(env)}`);
|
|
32
33
|
} else {
|
|
33
34
|
if (!server.url) throw new Error(`Grok MCP HTTP server "${server.name}" requires url`);
|
|
34
35
|
lines.push(`url = ${string(server.url)}`);
|
|
@@ -94,14 +95,14 @@ export function removeGrokMcpServers(text, names) {
|
|
|
94
95
|
return { text: output, removed };
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
async function assertSafeTarget(configPath, root) {
|
|
98
|
+
async function assertSafeTarget(configPath, root, userScope) {
|
|
98
99
|
const absolute = resolve(configPath);
|
|
99
100
|
const base = resolve(root);
|
|
100
101
|
const rel = relative(base, absolute);
|
|
101
102
|
if (!rel || rel.startsWith('..') || rel.startsWith('/') || rel.includes('\0')) {
|
|
102
103
|
throw new Error('Grok MCP config path must be a file beneath the selected configuration root');
|
|
103
104
|
}
|
|
104
|
-
for (let current = dirname(absolute); current !== base; current = dirname(current)) {
|
|
105
|
+
for (let current = dirname(absolute); !userScope && current !== base; current = dirname(current)) {
|
|
105
106
|
try {
|
|
106
107
|
if ((await lstat(current)).isSymbolicLink()) throw new Error('Refusing Grok MCP path through a symbolic link');
|
|
107
108
|
} catch (error) {
|
|
@@ -116,45 +117,34 @@ async function assertSafeTarget(configPath, root) {
|
|
|
116
117
|
}
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
async function atomicWrite(file, content) {
|
|
120
|
-
await
|
|
121
|
-
const temp = `${file}.aiwg-${process.pid}-${Date.now()}.tmp`;
|
|
122
|
-
try {
|
|
123
|
-
const handle = await open(temp, 'wx', 0o600);
|
|
124
|
-
await handle.writeFile(content, 'utf8');
|
|
125
|
-
await handle.sync();
|
|
126
|
-
await handle.close();
|
|
127
|
-
await rename(temp, file);
|
|
128
|
-
} catch (error) {
|
|
129
|
-
await rm(temp, { force: true });
|
|
130
|
-
throw error;
|
|
131
|
-
}
|
|
120
|
+
async function atomicWrite(file, content, options) {
|
|
121
|
+
await writeConfigAtomic(file, content, options);
|
|
132
122
|
}
|
|
133
123
|
|
|
134
124
|
export async function manageGrokBuildMcp(configPath, servers, options = {}) {
|
|
135
125
|
const root = options.root || dirname(dirname(configPath));
|
|
136
126
|
if (options.blockedByPolicy) return { configPath, state: 'blocked-by-policy', serversInjected: [], alreadyPresent: [], error: options.blockedByPolicy };
|
|
137
|
-
await assertSafeTarget(configPath, root);
|
|
127
|
+
await assertSafeTarget(configPath, root, options.userScope);
|
|
138
128
|
let before = '';
|
|
139
129
|
try { before = await readFile(configPath, 'utf8'); } catch (error) { if (error?.code !== 'ENOENT') throw error; }
|
|
140
130
|
const alreadyPresent = servers.filter(server => managedRange(before, server.name)).map(server => server.name);
|
|
141
131
|
const after = mergeGrokMcpServers(before, servers);
|
|
142
|
-
if (!options.dryRun && after !== before) await atomicWrite(configPath, after);
|
|
132
|
+
if (!options.dryRun && after !== before) await atomicWrite(configPath, after, { userScope: options.userScope ?? true, projectRoot: options.userScope ? undefined : root });
|
|
143
133
|
return { configPath, state: options.disabled ? 'disabled' : 'configured', serversInjected: servers.map(server => server.name), alreadyPresent };
|
|
144
134
|
}
|
|
145
135
|
|
|
146
136
|
export async function unmanageGrokBuildMcp(configPath, names, options = {}) {
|
|
147
137
|
const root = options.root || dirname(dirname(configPath));
|
|
148
|
-
await assertSafeTarget(configPath, root);
|
|
138
|
+
await assertSafeTarget(configPath, root, options.userScope);
|
|
149
139
|
let before;
|
|
150
140
|
try { before = await readFile(configPath, 'utf8'); } catch (error) { if (error?.code === 'ENOENT') return { configPath, state: 'absent', removed: [] }; throw error; }
|
|
151
141
|
const result = removeGrokMcpServers(before, names);
|
|
152
|
-
if (!options.dryRun && result.text !== before) await atomicWrite(configPath, result.text);
|
|
142
|
+
if (!options.dryRun && result.text !== before) await atomicWrite(configPath, result.text, { userScope: options.userScope ?? true, projectRoot: options.userScope ? undefined : root });
|
|
153
143
|
return { configPath, state: result.removed.length ? 'configured' : 'absent', removed: result.removed };
|
|
154
144
|
}
|
|
155
145
|
|
|
156
146
|
function runJson(binary, args, cwd, env) {
|
|
157
|
-
const run = spawnSync(binary, args, { cwd, env, encoding: 'utf8', timeout: 30_000 });
|
|
147
|
+
const run = spawnSync(binary, args, { cwd, env, encoding: 'utf8', timeout: 30_000, killSignal: 'SIGKILL' });
|
|
158
148
|
if (run.error || run.status !== 0) return { ok: false, detail: run.error?.message || run.stderr || `exit ${run.status}` };
|
|
159
149
|
try { return { ok: true, value: JSON.parse(run.stdout || '{}') }; }
|
|
160
150
|
catch { return { ok: false, detail: 'Grok returned malformed JSON' }; }
|