@humwick/runner 0.0.0-stage → 0.130.1
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/LICENSE +167 -0
- package/agent-bin/agent-relay-browser +10 -0
- package/agent-bin/humwick +10 -0
- package/hooks/fff-search-redirect/fff-search-redirect.cjs +161 -0
- package/hooks/fff-search-redirect/hooks.json +13 -0
- package/hooks/rg-r-guard/hooks.json +18 -0
- package/hooks/rg-r-guard/rg-r-guard.cjs +144 -0
- package/hooks/web-tools-redirect/hooks.json +13 -0
- package/hooks/web-tools-redirect/web-tools-redirect.cjs +84 -0
- package/node_modules/@humwick/offload/LICENSE +167 -0
- package/node_modules/@humwick/offload/README.md +57 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts +18 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/async-runner.js +158 -0
- package/node_modules/@humwick/offload/dist/async-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts +25 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.js +80 -0
- package/node_modules/@humwick/offload/dist/envelope.js.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +10 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +59 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +1 -0
- package/node_modules/@humwick/offload/dist/index.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/index.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/index.js +5 -0
- package/node_modules/@humwick/offload/dist/index.js.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +62 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js +295 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/types.d.ts +111 -0
- package/node_modules/@humwick/offload/dist/types.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/types.js +176 -0
- package/node_modules/@humwick/offload/dist/types.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts +23 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js +435 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +2 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js +27 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.js +1 -0
- package/node_modules/@humwick/offload/dist/worker.js.map +1 -0
- package/node_modules/@humwick/offload/package.json +42 -0
- package/node_modules/@humwick/offload/src/async-runner.ts +170 -0
- package/node_modules/@humwick/offload/src/envelope.ts +92 -0
- package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +49 -0
- package/node_modules/@humwick/offload/src/index.ts +4 -0
- package/node_modules/@humwick/offload/src/subprocess-runner.ts +334 -0
- package/node_modules/@humwick/offload/src/types.ts +235 -0
- package/node_modules/@humwick/offload/src/worker-pool.ts +463 -0
- package/node_modules/@humwick/offload/src/worker-runtime.ts +32 -0
- package/node_modules/@humwick/offload/src/worker.ts +5 -0
- package/package.json +47 -4
- package/plugins/browser/skills/playwright-cli/SKILL.md +53 -0
- package/plugins/claude/.claude-plugin/plugin.json +8 -0
- package/plugins/claude/claude-statusline-probe.cjs +144 -0
- package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +914 -0
- package/plugins/claude/hooks/gate-run-guard.sh +9 -0
- package/plugins/claude/hooks/gate-run-guard.ts +237 -0
- package/plugins/claude/hooks/hooks.json +158 -0
- package/plugins/claude/hooks/kill-guard.provisioned.mjs +7027 -0
- package/plugins/claude/hooks/kill-guard.sh +341 -0
- package/plugins/claude/hooks/kill-guard.ts +337 -0
- package/plugins/claude/hooks/permission-request.sh +22 -0
- package/plugins/claude/hooks/post-compact.sh +10 -0
- package/plugins/claude/hooks/post-tool-use.sh +8 -0
- package/plugins/claude/hooks/pre-compact.sh +12 -0
- package/plugins/claude/hooks/read-only-bash-guard.sh +587 -0
- package/plugins/claude/hooks/relay-status.sh +373 -0
- package/plugins/claude/hooks/session-end.sh +26 -0
- package/plugins/claude/hooks/session-start.sh +31 -0
- package/plugins/claude/hooks/shell-trust.ts +28 -0
- package/plugins/claude/hooks/stop-failure.sh +47 -0
- package/plugins/claude/hooks/stop.sh +48 -0
- package/plugins/claude/hooks/subagent-guard.cjs +26 -0
- package/plugins/claude/hooks/subagent-skill-policy.cjs +160 -0
- package/plugins/claude/hooks/subagent-start.sh +13 -0
- package/plugins/claude/hooks/subagent-stop.sh +19 -0
- package/plugins/claude/hooks/user-prompt-submit.sh +18 -0
- package/plugins/claude/hooks/wait-loop-guard.ts +120 -0
- package/plugins/claude/monitors/monitors.json +7 -0
- package/plugins/claude/monitors/relay-monitor.provisioned.mjs +786 -0
- package/plugins/claude/monitors/relay-monitor.ts +58 -0
- package/plugins/claude/skills/disconnect/SKILL.md +16 -0
- package/plugins/claude/skills/label/SKILL.md +23 -0
- package/plugins/claude/skills/message/SKILL.md +40 -0
- package/plugins/claude/skills/pair/SKILL.md +30 -0
- package/plugins/claude/skills/react/SKILL.md +18 -0
- package/plugins/claude/skills/read-message/SKILL.md +24 -0
- package/plugins/claude/skills/reply/SKILL.md +44 -0
- package/plugins/claude/skills/send-claimable/SKILL.md +39 -0
- package/plugins/claude/skills/slice-first/SKILL.md +32 -0
- package/plugins/claude/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/claude/skills/status/SKILL.md +16 -0
- package/plugins/claude/skills/tags/SKILL.md +25 -0
- package/plugins/claude/skills/task/SKILL.md +28 -0
- package/plugins/claude-output-style/.claude-plugin/plugin.json +5 -0
- package/plugins/claude-output-style/output-styles/agent-relay.md +5 -0
- package/plugins/codex/skills/disconnect/SKILL.md +15 -0
- package/plugins/codex/skills/guide/SKILL.md +15 -0
- package/plugins/codex/skills/label/SKILL.md +22 -0
- package/plugins/codex/skills/message/SKILL.md +40 -0
- package/plugins/codex/skills/pair/SKILL.md +29 -0
- package/plugins/codex/skills/react/SKILL.md +17 -0
- package/plugins/codex/skills/read-message/SKILL.md +23 -0
- package/plugins/codex/skills/reply/SKILL.md +44 -0
- package/plugins/codex/skills/send-claimable/SKILL.md +39 -0
- package/plugins/codex/skills/slice-first/SKILL.md +32 -0
- package/plugins/codex/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/codex/skills/status/SKILL.md +15 -0
- package/plugins/codex/skills/tags/SKILL.md +24 -0
- package/plugins/codex/skills/task/SKILL.md +28 -0
- package/src/adapter.ts +1061 -0
- package/src/adapters/__fixtures__/codex-app-server.ts +47 -0
- package/src/adapters/claude-api.ts +243 -0
- package/src/adapters/claude-context-command.ts +16 -0
- package/src/adapters/claude-credentials.ts +22 -0
- package/src/adapters/claude-delivery-lanes.ts +28 -0
- package/src/adapters/claude-delivery.ts +166 -0
- package/src/adapters/claude-effort.ts +94 -0
- package/src/adapters/claude-exit.ts +290 -0
- package/src/adapters/claude-headless-mirror.ts +88 -0
- package/src/adapters/claude-headless-refusal.ts +51 -0
- package/src/adapters/claude-headless.ts +617 -0
- package/src/adapters/claude-monitor-delivery-plan.ts +317 -0
- package/src/adapters/claude-permission.ts +243 -0
- package/src/adapters/claude-prompt-gates.ts +269 -0
- package/src/adapters/claude-prompt-hook.ts +145 -0
- package/src/adapters/claude-session-capture.ts +293 -0
- package/src/adapters/claude-session-probe.ts +414 -0
- package/src/adapters/claude-shutdown.ts +190 -0
- package/src/adapters/claude-status-detectors.ts +426 -0
- package/src/adapters/claude-subagents.ts +93 -0
- package/src/adapters/claude-submit-confirmation.ts +287 -0
- package/src/adapters/claude-tmux.ts +548 -0
- package/src/adapters/claude-token-usage-capture.ts +253 -0
- package/src/adapters/claude-tool-policy.ts +165 -0
- package/src/adapters/claude-transcript-tail.ts +193 -0
- package/src/adapters/claude-transcript.ts +812 -0
- package/src/adapters/claude.ts +1097 -0
- package/src/adapters/codex-agent-message-capture.ts +189 -0
- package/src/adapters/codex-app-server.ts +70 -0
- package/src/adapters/codex-client.ts +362 -0
- package/src/adapters/codex-effective-config.ts +87 -0
- package/src/adapters/codex-launch-config.ts +153 -0
- package/src/adapters/codex-rollout-context.ts +272 -0
- package/src/adapters/codex-skill-discovery.ts +18 -0
- package/src/adapters/codex-token-usage-capture.ts +163 -0
- package/src/adapters/codex-tool-output.ts +53 -0
- package/src/adapters/codex-turn-outcome.ts +27 -0
- package/src/adapters/codex-version.ts +64 -0
- package/src/adapters/codex.ts +1442 -0
- package/src/adapters/incremental-file.ts +110 -0
- package/src/adapters/opencode-api.ts +71 -0
- package/src/adapters/opencode-context.ts +277 -0
- package/src/adapters/opencode-model-registry.ts +126 -0
- package/src/adapters/opencode-provider.ts +169 -0
- package/src/adapters/opencode-server-retry.ts +50 -0
- package/src/adapters/opencode-soft-steer.ts +302 -0
- package/src/adapters/opencode-spawn.ts +145 -0
- package/src/adapters/opencode-subscribe.ts +62 -0
- package/src/adapters/opencode-token-usage-capture.ts +187 -0
- package/src/adapters/opencode-tool-events.ts +302 -0
- package/src/adapters/opencode-tools-config.ts +17 -0
- package/src/adapters/opencode.ts +2264 -0
- package/src/adapters/project-shell.ts +80 -0
- package/src/adapters/provider-launch-config.ts +21 -0
- package/src/adapters/provider-quota-harvest.ts +127 -0
- package/src/adapters/service-access.ts +153 -0
- package/src/adapters/tmux-offload.ts +207 -0
- package/src/adapters/tool-summary.ts +214 -0
- package/src/agent-redact.ts +75 -0
- package/src/agent-runner/commands/dispatch.ts +185 -0
- package/src/agent-runner/commands/lifecycle.ts +84 -0
- package/src/agent-runner/commands/prompt.ts +284 -0
- package/src/agent-runner/credentials.ts +208 -0
- package/src/agent-runner/http-liveness.ts +85 -0
- package/src/agent-runner/lifecycle-milestones.ts +207 -0
- package/src/agent-runner/lifecycle-phase.ts +80 -0
- package/src/agent-runner/registration.ts +163 -0
- package/src/agent-runner/runner-context.ts +158 -0
- package/src/agent-runner/session-mirror.ts +278 -0
- package/src/agent-runner/task-claims.ts +116 -0
- package/src/agent-runner/token-usage-coverage.ts +54 -0
- package/src/agent-runner/turn-report.ts +269 -0
- package/src/api-budget-accounting.ts +91 -0
- package/src/area-memory.ts +255 -0
- package/src/artifact-path-materialize.ts +52 -0
- package/src/async-guard.ts +35 -0
- package/src/attachment-cache-sweep.ts +31 -0
- package/src/attachment-cache.ts +195 -0
- package/src/background-script-reconcile.ts +106 -0
- package/src/bounded-log.ts +59 -0
- package/src/bounded-spawn-sync.ts +18 -0
- package/src/busy-reconciler.ts +282 -0
- package/src/claim-tracker.ts +140 -0
- package/src/claude-settings.ts +167 -0
- package/src/claude-workspace-trust.ts +289 -0
- package/src/config.ts +547 -0
- package/src/context-boundary.ts +101 -0
- package/src/context-command.ts +151 -0
- package/src/context-probe-state.ts +6 -0
- package/src/continuation-archive.ts +179 -0
- package/src/control-server.ts +730 -0
- package/src/delivery-policy.ts +9 -0
- package/src/delivery-result.ts +24 -0
- package/src/effort-command.ts +125 -0
- package/src/empty-turn.ts +816 -0
- package/src/free-port.ts +25 -0
- package/src/fs-offload.ts +26 -0
- package/src/gate-run-config.ts +35 -0
- package/src/gate-run-lease-probe.ts +38 -0
- package/src/host-asset-plan.ts +450 -0
- package/src/host-claude-config-worker.ts +34 -0
- package/src/host-sources.ts +40 -0
- package/src/inbox-carrier.ts +7 -0
- package/src/index.ts +386 -0
- package/src/launch-assembly.ts +955 -0
- package/src/liveness.ts +105 -0
- package/src/logger.ts +108 -0
- package/src/mcp-outbox.ts +204 -0
- package/src/mcp-preflight.ts +86 -0
- package/src/mcp-source-selection.ts +54 -0
- package/src/mcp-tool-deferral.ts +35 -0
- package/src/mcp-vault-env.ts +25 -0
- package/src/message-body-config/config.ts +16 -0
- package/src/message-body-config.ts +1 -0
- package/src/mid-turn-delivery.ts +43 -0
- package/src/native-memory-lever.ts +329 -0
- package/src/native-self-resume.ts +244 -0
- package/src/outbox.ts +453 -0
- package/src/park-post.ts +17 -0
- package/src/pending-permission-store.ts +148 -0
- package/src/process-meta.ts +9 -0
- package/src/profile-home.ts +419 -0
- package/src/profile-projection.ts +411 -0
- package/src/project-hooks.ts +65 -0
- package/src/project-instructions.ts +119 -0
- package/src/provider-double.ts +59 -0
- package/src/provider-hold.ts +228 -0
- package/src/provider-home-marker.ts +3 -0
- package/src/provider-home-tombstone.ts +4 -0
- package/src/provider-input-queue.ts +134 -0
- package/src/provider-integration.ts +93 -0
- package/src/provider-processes.ts +17 -0
- package/src/provider-submit-failure.ts +15 -0
- package/src/providers.ts +35 -0
- package/src/provisioning.ts +703 -0
- package/src/question-decision.ts +18 -0
- package/src/quota.ts +22 -0
- package/src/rate-limit.ts +351 -0
- package/src/refused-prompt-store.ts +36 -0
- package/src/relay-injection-events.ts +161 -0
- package/src/relay-instructions.ts +107 -0
- package/src/relay-mcp-proxy.long-turn-e2e.ts +183 -0
- package/src/relay-mcp-proxy.ts +704 -0
- package/src/relay-mcp.ts +583 -0
- package/src/reply-obligation-cache.ts +211 -0
- package/src/response-capture-report.ts +111 -0
- package/src/response-routing.ts +6 -0
- package/src/runner-core.ts +2955 -0
- package/src/runner-exit.ts +12 -0
- package/src/runner-helpers.ts +582 -0
- package/src/runner-insights.ts +105 -0
- package/src/runner-mcp-proxy.ts +46 -0
- package/src/runner-spawn-config.ts +68 -0
- package/src/runner-stale-scratch-sweep.ts +21 -0
- package/src/runner.ts +9 -0
- package/src/runtime-token-file.ts +45 -0
- package/src/runtime-token-remint.ts +56 -0
- package/src/service-access.ts +88 -0
- package/src/session-destroy.ts +27 -0
- package/src/session-insights.ts +118 -0
- package/src/session-scratch.ts +296 -0
- package/src/spawn-input.ts +147 -0
- package/src/startup-config-fetch.ts +48 -0
- package/src/subreaper.ts +113 -0
- package/src/template-resolver.ts +29 -0
- package/src/test-run-dedup.ts +435 -0
- package/src/transcript-context.ts +99 -0
- package/src/transport-errors.ts +48 -0
- package/src/turn-opener.ts +55 -0
- package/src/turn-reconcile-event.ts +88 -0
- package/src/vault-proxy-hop.ts +99 -0
- package/src/version.ts +44 -0
- package/src/web-tool-call-proxy.ts +128 -0
- package/src/web-tools.ts +59 -0
- package/README.md +0 -3
package/src/relay-mcp.ts
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
import { mcpServerInfo, parseToolGeneration, isRelayMcpServerName, type ToolGeneration } from "@humwick/sdk";
|
|
2
|
+
import { relayEnv, writeChildEnv } from "@humwick/sdk/env";
|
|
3
|
+
import { codexVaultMcpEnv } from "./mcp-vault-env";
|
|
4
|
+
// Single home for the relay HTTP MCP endpoint descriptor + per-provider injection
|
|
5
|
+
// args. Both adapters (and Stage 2's proxy) import from here so the endpoint path,
|
|
6
|
+
// server name, and token-handling rules live in exactly one place.
|
|
7
|
+
//
|
|
8
|
+
// Token handling: the bearer token is NEVER placed in argv (it would leak via `ps`
|
|
9
|
+
// and the agent's own process inspection). Claude expands `${AGENT_RELAY_SESSION_TOKEN}`
|
|
10
|
+
// from the env at MCP-config parse time; Codex reads it from the named env var.
|
|
11
|
+
//
|
|
12
|
+
// Why a dedicated var and NOT `AGENT_RELAY_TOKEN`: a managed session inherits the
|
|
13
|
+
// runner's scoped, identity-bearing session token, but Claude Code applies a rig/user
|
|
14
|
+
// `settings.json` `env` block OVER the inherited env. A rig that sets
|
|
15
|
+
// `env.AGENT_RELAY_TOKEN` (e.g. to the admin token, for interactive CLI use) would
|
|
16
|
+
// clobber the scoped token at parse time, so the relay MCP connection authenticates as
|
|
17
|
+
// the `server` actor with no agent identity — `relay_whoami` returns null and
|
|
18
|
+
// `relay_send_message` demands a `from` it can't supply (#233). The runner exports the
|
|
19
|
+
// scoped token under this dedicated name that rigs don't set, so nothing on the host can
|
|
20
|
+
// hijack the managed agent's identity.
|
|
21
|
+
export const RELAY_MCP_SERVER_NAME = mcpServerInfo("relay").name;
|
|
22
|
+
export { isRelayMcpServerName };
|
|
23
|
+
export const RELAY_MCP_PATH = "/api/mcp";
|
|
24
|
+
export const RELAY_MCP_TOKEN_ENV = "AGENT_RELAY_SESSION_TOKEN";
|
|
25
|
+
|
|
26
|
+
// #672 — shared host-listener MCP. The agent connects to ONE orchestrator-owned shared
|
|
27
|
+
// `callmux` listener via a per-agent `callmux bridge`, fronting the host's NON-IDENTITY MCP
|
|
28
|
+
// (tokenlean, github). The bridge is a stdio child (auto-reconnect + retry-once on a
|
|
29
|
+
// listener/downstream hiccup; `--cwd` injects `x-callmux-cwd` for per-worktree session-cwd
|
|
30
|
+
// isolation), so it rides the SAME provider injection as any provisioned stdio server. The
|
|
31
|
+
// identity-bearing relay endpoint is NEVER routed here — it stays per-agent on #215.
|
|
32
|
+
export { SHARED_MCP_SERVER_NAME };
|
|
33
|
+
// callmux's documented manual-listener endpoint (`callmux --listen 4860`, Streamable HTTP at
|
|
34
|
+
// /mcp). In managed launches the orchestrator owns the real listener URL and passes it through
|
|
35
|
+
// AGENT_RELAY_SHARED_MCP_URL; this fallback is for direct runner/manual-listener use.
|
|
36
|
+
export const DEFAULT_SHARED_MCP_URL = "http://localhost:4860/mcp";
|
|
37
|
+
export const SHARED_MCP_URL_ENV = "AGENT_RELAY_SHARED_MCP_URL";
|
|
38
|
+
export { runnerManagedToolDeferralRoute } from "./mcp-tool-deferral";
|
|
39
|
+
|
|
40
|
+
/** Resolve the shared-listener URL: explicit runner option → env → documented default. */
|
|
41
|
+
export function resolveSharedMcpUrl(explicit?: string): string {
|
|
42
|
+
return explicit ?? relayEnv(process.env, SHARED_MCP_URL_ENV) ?? DEFAULT_SHARED_MCP_URL;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function runnerMcpGeneration(env: Record<string, string | undefined>): ToolGeneration {
|
|
46
|
+
return parseToolGeneration(relayEnv(env, "AGENT_RELAY_MCP_TOOLS"));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function generationEndpoint(endpoint: string, generation: ToolGeneration): string {
|
|
50
|
+
const url = new URL(endpoint);
|
|
51
|
+
if (generation === "humwick") url.searchParams.set("tools", generation);
|
|
52
|
+
else url.searchParams.delete("tools");
|
|
53
|
+
return url.toString();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function relayMcpEndpoint(relayUrl: string, generation: ToolGeneration = "relay"): string {
|
|
57
|
+
return generationEndpoint(`${relayUrl.replace(/\/+$/, "")}${RELAY_MCP_PATH}`, generation);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// #1248 — per-server Codex MCP overrides. Default `tool_timeout_sec` (60s) and
|
|
61
|
+
// `startup_timeout_ms` are too short for our long-running tools (tests/releases run
|
|
62
|
+
// 5+ min), forcing agents to poll/sleep instead of just waiting on the call. Verified
|
|
63
|
+
// against the installed codex-cli binary (0.144.0): both fields live on the
|
|
64
|
+
// per-`[mcp_servers.<name>]` McpServerConfig struct, alongside `bearer_token_env_var`.
|
|
65
|
+
export const CODEX_MCP_TOOL_TIMEOUT_SEC = 900;
|
|
66
|
+
export const CODEX_MCP_STARTUP_TIMEOUT_MS = 30000;
|
|
67
|
+
export const OPENCODE_MCP_TIMEOUT_MS = CODEX_MCP_TOOL_TIMEOUT_SEC * 1_000;
|
|
68
|
+
|
|
69
|
+
function codexTimeoutConfigArgs(key: string): string[] {
|
|
70
|
+
return [
|
|
71
|
+
"-c",
|
|
72
|
+
`${key}.tool_timeout_sec=${CODEX_MCP_TOOL_TIMEOUT_SEC}`,
|
|
73
|
+
"-c",
|
|
74
|
+
`${key}.startup_timeout_ms=${CODEX_MCP_STARTUP_TIMEOUT_MS}`,
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Codex: `-c mcp_servers.<name>.*` overrides. `bearer_token_env_var` tells Codex to
|
|
79
|
+
// read the token from the env var itself → transport resolves to streamable_http.
|
|
80
|
+
// `endpoint` overrides the target URL (runner-local proxy, Stage 2 #215) — see above.
|
|
81
|
+
export function relayMcpCodexConfigArgs(relayUrl: string, endpoint?: string, generation: ToolGeneration = "relay"): string[] {
|
|
82
|
+
const key = `mcp_servers.${mcpServerInfo(generation).name}`;
|
|
83
|
+
return [
|
|
84
|
+
"-c",
|
|
85
|
+
`${key}.url=${tomlString(generationEndpoint(endpoint ?? relayMcpEndpoint(relayUrl), generation))}`,
|
|
86
|
+
"-c",
|
|
87
|
+
`${key}.bearer_token_env_var=${tomlString(RELAY_MCP_TOKEN_ENV)}`,
|
|
88
|
+
...codexTimeoutConfigArgs(key),
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Shared TOML string escaper for Codex `-c key=value` overrides. One home; codex.ts
|
|
93
|
+
// imports this rather than re-declaring it.
|
|
94
|
+
export function tomlString(value: string): string {
|
|
95
|
+
return JSON.stringify(value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A JSON-shaped value as one TOML inline value, for a structured `-c key=<value>` override. */
|
|
99
|
+
export function tomlInlineValue(value: unknown): string {
|
|
100
|
+
if (typeof value === "string") return tomlString(value);
|
|
101
|
+
if (typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
102
|
+
if (Array.isArray(value)) return `[${value.map(tomlInlineValue).join(",")}]`;
|
|
103
|
+
const record = plainRecord(value);
|
|
104
|
+
if (!record) throw new Error(`no TOML inline form for ${value === null ? "null" : typeof value}`);
|
|
105
|
+
return `{${Object.entries(record).filter(([, item]) => item !== undefined).map(([key, item]) => `${tomlString(key)}=${tomlInlineValue(item)}`).join(",")}}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
type TomlScalar = string | number | boolean;
|
|
109
|
+
|
|
110
|
+
function tomlValue(value: unknown): string | undefined {
|
|
111
|
+
if (typeof value === "string") return tomlString(value);
|
|
112
|
+
if (typeof value === "boolean") return String(value);
|
|
113
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
114
|
+
if (Array.isArray(value) && value.every((item): item is TomlScalar =>
|
|
115
|
+
typeof item === "string" || typeof item === "number" && Number.isFinite(item) || typeof item === "boolean")) {
|
|
116
|
+
return `[${value.map((item) => tomlValue(item)!).join(", ")}]`;
|
|
117
|
+
}
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function plainRecord(value: unknown): Record<string, unknown> | undefined {
|
|
122
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
123
|
+
? value as Record<string, unknown>
|
|
124
|
+
: undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Keep Codex-native MCP fields when projecting a host config into an isolated home. The
|
|
129
|
+
* provider-neutral shape has historically carried only command/args/url/env; dropping the
|
|
130
|
+
* native auth/header fields makes a copied remote server look applied while removing its
|
|
131
|
+
* credentials. Nested tables are returned separately because TOML tables must follow the
|
|
132
|
+
* server table rather than being emitted as dotted keys in its body.
|
|
133
|
+
*/
|
|
134
|
+
function codexNativeMcpToml(server: ProvisioningMcpServer): { root: string[]; nested: Array<{ name: string; lines: string[] }> } {
|
|
135
|
+
const root: string[] = [];
|
|
136
|
+
for (const key of ["env_vars", "cwd", "bearer_token_env_var", "disabled_tools"] as const) {
|
|
137
|
+
const rendered = tomlValue(server[key]);
|
|
138
|
+
if (rendered !== undefined) root.push(`${key} = ${rendered}`);
|
|
139
|
+
}
|
|
140
|
+
const nested: Array<{ name: string; lines: string[] }> = [];
|
|
141
|
+
for (const key of ["http_headers", "env_http_headers", "auth"] as const) {
|
|
142
|
+
const record = plainRecord(server[key]);
|
|
143
|
+
if (!record) {
|
|
144
|
+
const rendered = tomlValue(server[key]);
|
|
145
|
+
if (rendered !== undefined) root.push(`${key} = ${rendered}`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const lines = Object.entries(record).flatMap(([name, value]) => {
|
|
149
|
+
const rendered = tomlValue(value);
|
|
150
|
+
return rendered === undefined ? [] : [`${tomlString(name)} = ${rendered}`];
|
|
151
|
+
});
|
|
152
|
+
if (lines.length) nested.push({ name: key, lines });
|
|
153
|
+
}
|
|
154
|
+
return { root, nested };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// #555 — compose the launch MCP set from (the relay endpoint, if relay.mcp) PLUS the
|
|
158
|
+
// profile's relay-provisioned servers, PLUS (#662) the host USER-scope servers for an
|
|
159
|
+
// `mcp.mode:"host"` profile. One `--mcp-config` JSON carries them all. `strict` adds
|
|
160
|
+
// `--strict-mcp-config` so ONLY these load — Claude reads no on-disk config, so the
|
|
161
|
+
// project-cwd `.mcp.json` is never discovered (its enable modal can't wedge a headless
|
|
162
|
+
// agent). Whatever the assembler decides host mode should keep is injected here explicitly;
|
|
163
|
+
// strict then preserves exactly that set and nothing else.
|
|
164
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
165
|
+
import { dirname, join, resolve as resolvePath } from "node:path";
|
|
166
|
+
import { fileURLToPath } from "node:url";
|
|
167
|
+
import { SHARED_MCP_SERVER_NAME, sharedCallmuxToolCallTimeoutMsFromEnv, type ProvisioningMcpServer } from "@humwick/sdk";
|
|
168
|
+
|
|
169
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
170
|
+
|
|
171
|
+
// #1301 — a bare `command: "callmux"` resolves off PATH, which can drift from the version
|
|
172
|
+
// this package pins (root/runner/orchestrator all pin the same `callmux` dependency). npm
|
|
173
|
+
// (and bun/pnpm workspaces) always link a dependency's declared bin to `node_modules/.bin/<name>`
|
|
174
|
+
// at whichever ancestor directory it resolves/hoists to — the same layout-agnostic-bin
|
|
175
|
+
// invariant #1305/#1222 rely on for the humwick CLI itself — so walk up from this
|
|
176
|
+
// module's own location to find the PINNED bridge binary instead of trusting PATH. Falls
|
|
177
|
+
// back to the bare name (old behavior) only if no bundled bin is found anywhere above.
|
|
178
|
+
export function resolveCallmuxBin(startDir: string = __dirname): string {
|
|
179
|
+
let dir = resolvePath(startDir);
|
|
180
|
+
while (true) {
|
|
181
|
+
const candidate = join(dir, "node_modules", ".bin", "callmux");
|
|
182
|
+
if (existsSync(candidate)) return candidate;
|
|
183
|
+
const parent = dirname(dir);
|
|
184
|
+
if (parent === dir) return "callmux";
|
|
185
|
+
dir = parent;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// The shared-listener bridge as a provider-neutral stdio server descriptor: a `callmux bridge`
|
|
190
|
+
// child fronting the host's shared listener, with per-agent `--cwd "$WORKTREE"` for session-cwd
|
|
191
|
+
// isolation (#672). It flows through the existing claude/codex stdio injection unchanged.
|
|
192
|
+
export function sharedMcpBridgeServer(url: string, cwd: string): ProvisioningMcpServer {
|
|
193
|
+
return {
|
|
194
|
+
command: resolveCallmuxBin(),
|
|
195
|
+
args: ["bridge", "--url", url, "--cwd", cwd, "--call-timeout", String(sharedCallmuxToolCallTimeoutMsFromEnv())],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// #1575 — the per-agent metaOnly callmux for Codex workers. The header callmux reads to scope
|
|
200
|
+
// session-cwd stdio downstreams (tokenlean) per worktree; the shared bridge injects it dynamically,
|
|
201
|
+
// and here we pin it statically because a stdio callmux has no incoming request headers to forward.
|
|
202
|
+
const CALLMUX_CWD_HEADER = "x-callmux-cwd";
|
|
203
|
+
// The filename the per-agent metaOnly config is materialized under, inside the agent's relay-owned
|
|
204
|
+
// provisioning home. Shared by the assembler (points `callmux --config <path>` at it) and the
|
|
205
|
+
// materializer (writes it) so disk == launch.
|
|
206
|
+
export const SHARED_MCP_METAONLY_CONFIG_FILENAME = "callmux-metaonly.json";
|
|
207
|
+
|
|
208
|
+
// #1450 — the request header the per-agent metaOnly callmux sets on its relay downstream. The runner
|
|
209
|
+
// proxy reads it to decide which relay tools/list view to serve: WITHOUT it (the agent's DIRECT relay
|
|
210
|
+
// client) the proxy hides the on-demand set so only the eager hot-path set boots; WITH it (this
|
|
211
|
+
// callmux, fronting the on-demand set) the proxy serves the FULL set the relay already returned so
|
|
212
|
+
// callmux can route callmux_call to any of them. It can only ever widen the LIST back to that
|
|
213
|
+
// relay-returned set — never authorization: tools/call stays scope-gated regardless (#215 / capability
|
|
214
|
+
// neutrality). Kept out of `forward` so the relay itself never sees it.
|
|
215
|
+
const RELAY_MCP_ONDEMAND_SURFACE_HEADER = "x-agent-relay-ondemand-front";
|
|
216
|
+
export const RELAY_MCP_ONDEMAND_SURFACE_VALUE = "1";
|
|
217
|
+
|
|
218
|
+
// #1450 — a Codex worker's relay downstream inside the per-agent metaOnly callmux. It points at the
|
|
219
|
+
// runner-LOCAL proxy (never the relay directly): the proxy injects the live relay token, so identity
|
|
220
|
+
// rides through and the agent/callmux never hold the real token on disk (#215). `authSecret` is the
|
|
221
|
+
// per-session PROXY secret (which the agent already holds in AGENT_RELAY_SESSION_TOKEN — writing it to
|
|
222
|
+
// this 0600 config adds no exposure, and it is NOT the relay token). cachePolicy denies every tool:
|
|
223
|
+
// relay ops are identity-bearing/mutating and must never be served from cache.
|
|
224
|
+
export interface MetaOnlyRelayDownstream {
|
|
225
|
+
/** The runner-local proxy MCP endpoint (config.relayMcpEndpoint when the proxy is active). */
|
|
226
|
+
endpoint: string;
|
|
227
|
+
/** The per-session proxy secret the agent authenticates to the local proxy with. */
|
|
228
|
+
authSecret: string;
|
|
229
|
+
generation?: ToolGeneration;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function metaOnlyRelayServerConfig(relay: MetaOnlyRelayDownstream): Record<string, unknown> {
|
|
233
|
+
return {
|
|
234
|
+
url: generationEndpoint(relay.endpoint, relay.generation ?? "relay"),
|
|
235
|
+
transport: "streamable-http",
|
|
236
|
+
headers: {
|
|
237
|
+
Authorization: `Bearer ${relay.authSecret}`,
|
|
238
|
+
[RELAY_MCP_ONDEMAND_SURFACE_HEADER]: RELAY_MCP_ONDEMAND_SURFACE_VALUE,
|
|
239
|
+
},
|
|
240
|
+
// #1450 — never cache relay ops (identity-bearing/mutating). Wildcard-deny is explicit belt-and-
|
|
241
|
+
// suspenders on top of the config's disabled global cache TTL.
|
|
242
|
+
cachePolicy: { denyTools: ["*"] },
|
|
243
|
+
callTimeoutMs: sharedCallmuxToolCallTimeoutMsFromEnv(),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// The per-agent metaOnly callmux config: streamable-http downstreams exposing ONLY the callmux
|
|
248
|
+
// meta-tools (callmux_search_tools/callmux_call/…). A Codex worker boots with ~11 meta-tool schemas
|
|
249
|
+
// instead of the flat downstream schemas (#1570 dominant lever) and reaches the real tools on demand
|
|
250
|
+
// through callmux_call.
|
|
251
|
+
// • The shared listener (SHARED_MCP_SERVER_NAME): the worktree cwd is a STATIC downstream header —
|
|
252
|
+
// the per-agent callmux runs over stdio (no incoming headers to forwardHeaders) and the worktree
|
|
253
|
+
// is fixed per agent, so a fixed x-callmux-cwd routes the shared listener's session-cwd tools to
|
|
254
|
+
// this worktree exactly as the dynamic bridge header did.
|
|
255
|
+
// • #1450 — when `relay` is given, the identity-bearing relay endpoint is added as a SECOND
|
|
256
|
+
// downstream (via the runner-local proxy) so the on-demand relay set loads through callmux_call.
|
|
257
|
+
// Timeouts mirror the bridge's SHARED_CALLMUX_TOOL_CALL_TIMEOUT_MS floor so nested calls keep the same
|
|
258
|
+
// call-timeout budget.
|
|
259
|
+
export function sharedMcpMetaOnlyConfig(url: string, cwd: string, relay?: MetaOnlyRelayDownstream): Record<string, unknown> {
|
|
260
|
+
const callTimeoutMs = sharedCallmuxToolCallTimeoutMsFromEnv();
|
|
261
|
+
return {
|
|
262
|
+
$schema: "callmux/schema.json",
|
|
263
|
+
metaOnly: true,
|
|
264
|
+
exposeMetaTools: true,
|
|
265
|
+
callTimeoutMs,
|
|
266
|
+
servers: {
|
|
267
|
+
[SHARED_MCP_SERVER_NAME]: {
|
|
268
|
+
url,
|
|
269
|
+
transport: "streamable-http",
|
|
270
|
+
headers: { [CALLMUX_CWD_HEADER]: cwd },
|
|
271
|
+
// The shared listener owns caching; a per-agent cache would bypass the call log.
|
|
272
|
+
cachePolicy: { denyTools: ["*"] },
|
|
273
|
+
callTimeoutMs,
|
|
274
|
+
},
|
|
275
|
+
...(relay ? { [mcpServerInfo(relay.generation ?? "relay").name]: metaOnlyRelayServerConfig(relay) } : {}),
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// The per-agent metaOnly callmux as a provider-neutral stdio server descriptor: a `callmux --config
|
|
281
|
+
// <path>` child running the config above. Rides the SAME claude/codex stdio injection as the plain
|
|
282
|
+
// bridge. The config path is non-secret (a relay-owned scratch file), so it is safe in argv/config.
|
|
283
|
+
export function sharedMcpMetaOnlyServer(configPath: string): ProvisioningMcpServer {
|
|
284
|
+
return {
|
|
285
|
+
command: resolveCallmuxBin(),
|
|
286
|
+
args: ["--config", configPath],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Materialize the per-agent metaOnly config `sharedMcpMetaOnlyServer(configPath)` points at. The
|
|
291
|
+
// shared-listener URL is non-secret. #1450 — when a `relay` downstream is given, the config carries
|
|
292
|
+
// the per-session PROXY secret (NOT the relay token — the proxy injects that; #215). The agent
|
|
293
|
+
// already holds the proxy secret in AGENT_RELAY_SESSION_TOKEN, so the 0600 file adds no exposure.
|
|
294
|
+
export function writeSharedMcpMetaOnlyConfig(configPath: string, url: string, cwd: string, relay?: MetaOnlyRelayDownstream): void {
|
|
295
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
296
|
+
writeFileSync(configPath, JSON.stringify(sharedMcpMetaOnlyConfig(url, cwd, relay), null, 2) + "\n", { mode: 0o600 });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// #1292 — shared by the Claude JSON payload builder and the Codex TOML materializer, so
|
|
300
|
+
// both projections merge `configEnv` (non-secret, allowlisted literals) into `env` the
|
|
301
|
+
// same way instead of re-deriving the merge twice.
|
|
302
|
+
export function mergedConfigEnv(server: ProvisioningMcpServer): ProvisioningMcpServer {
|
|
303
|
+
if (!server.configEnv || Object.keys(server.configEnv).length === 0) return server;
|
|
304
|
+
return {
|
|
305
|
+
...server,
|
|
306
|
+
env: writeChildEnv({}, { ...server.configEnv, ...(server.env ?? {}) }),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function claudeMcpServerEntry(server: ProvisioningMcpServer): Record<string, unknown> {
|
|
311
|
+
const merged = mergedConfigEnv(server);
|
|
312
|
+
const entry: Record<string, unknown> = {};
|
|
313
|
+
if (merged.type) entry.type = merged.type;
|
|
314
|
+
if (merged.command) entry.command = merged.command;
|
|
315
|
+
if (merged.args?.length) entry.args = merged.args;
|
|
316
|
+
if (merged.env && Object.keys(merged.env).length) entry.env = merged.env;
|
|
317
|
+
if (merged.url) entry.url = merged.url;
|
|
318
|
+
if (merged.headers && Object.keys(merged.headers).length) entry.headers = merged.headers;
|
|
319
|
+
return entry;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export interface ClaudeMcpConfigOpts {
|
|
323
|
+
relayUrl: string;
|
|
324
|
+
endpoint?: string;
|
|
325
|
+
includeRelay: boolean;
|
|
326
|
+
generation?: ToolGeneration;
|
|
327
|
+
servers?: Record<string, ProvisioningMcpServer>;
|
|
328
|
+
/** #662 — host USER-scope MCP servers (`~/.claude.json` `mcpServers`), re-injected for
|
|
329
|
+
* `mcp.mode:"host"` so `--strict-mcp-config` PRESERVES them instead of dropping the
|
|
330
|
+
* ambient host MCP a host-base agent loaded before strict. Lowest precedence: the relay
|
|
331
|
+
* endpoint and relay-provisioned servers win on a name collision (relay decides). */
|
|
332
|
+
hostServers?: Record<string, ProvisioningMcpServer>;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// #1292 — the `{ mcpServers: {...} }` object Claude parses, extracted so both the launch
|
|
336
|
+
// assembler (decides whether it's written to argv or a file) and the materializer (writes
|
|
337
|
+
// the file) build the IDENTICAL payload from the same inputs.
|
|
338
|
+
export function claudeMcpConfigPayload(opts: ClaudeMcpConfigOpts): Record<string, unknown> {
|
|
339
|
+
const mcpServers: Record<string, unknown> = {};
|
|
340
|
+
// Host user-scope servers go in first so relay endpoint + provisioned servers below can
|
|
341
|
+
// override them by name (relay-owned config is authoritative over ambient host MCP).
|
|
342
|
+
for (const [name, server] of Object.entries(opts.hostServers ?? {})) {
|
|
343
|
+
if (isRelayMcpServerName(name)) continue;
|
|
344
|
+
const entry = claudeMcpServerEntry(server);
|
|
345
|
+
if (Object.keys(entry).length) mcpServers[name] = entry;
|
|
346
|
+
}
|
|
347
|
+
if (opts.includeRelay) {
|
|
348
|
+
mcpServers[mcpServerInfo(opts.generation ?? "relay").name] = {
|
|
349
|
+
type: "http",
|
|
350
|
+
url: generationEndpoint(opts.endpoint ?? relayMcpEndpoint(opts.relayUrl), opts.generation ?? "relay"),
|
|
351
|
+
headers: { Authorization: `Bearer \${${RELAY_MCP_TOKEN_ENV}}` },
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
for (const [name, server] of Object.entries(opts.servers ?? {})) {
|
|
355
|
+
// Never let a declared server shadow the relay endpoint (identity-bearing).
|
|
356
|
+
if (isRelayMcpServerName(name)) continue;
|
|
357
|
+
const entry = claudeMcpServerEntry(server);
|
|
358
|
+
if (Object.keys(entry).length) mcpServers[name] = entry;
|
|
359
|
+
}
|
|
360
|
+
return mcpServers;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// OpenCode's config strings use `{env:NAME}` rather than Claude's `${NAME}` convention.
|
|
364
|
+
// Translate only the provider-neutral env/header values where `${NAME}` is documented as an
|
|
365
|
+
// environment reference; command arguments remain literal.
|
|
366
|
+
function openCodeConfigEnvValue(value: string): string {
|
|
367
|
+
return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, "{env:$1}");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function openCodeMcpServerEntry(server: ProvisioningMcpServer): Record<string, unknown> | undefined {
|
|
371
|
+
const merged = mergedConfigEnv(server);
|
|
372
|
+
if (merged.command) {
|
|
373
|
+
const environment = Object.fromEntries(
|
|
374
|
+
Object.entries(merged.env ?? {}).map(([name, value]) => [name, openCodeConfigEnvValue(value)]),
|
|
375
|
+
);
|
|
376
|
+
return {
|
|
377
|
+
type: "local",
|
|
378
|
+
command: [merged.command, ...(merged.args ?? [])],
|
|
379
|
+
...(merged.cwd ? { cwd: merged.cwd } : {}),
|
|
380
|
+
...(Object.keys(environment).length ? { environment } : {}),
|
|
381
|
+
timeout: OPENCODE_MCP_TIMEOUT_MS,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (!merged.url) return undefined;
|
|
385
|
+
const headers = Object.fromEntries(
|
|
386
|
+
Object.entries(merged.headers ?? {}).map(([name, value]) => [name, openCodeConfigEnvValue(value)]),
|
|
387
|
+
);
|
|
388
|
+
return {
|
|
389
|
+
type: "remote",
|
|
390
|
+
url: merged.url,
|
|
391
|
+
...(Object.keys(headers).length ? { headers } : {}),
|
|
392
|
+
// Managed agents cannot complete an interactive OAuth flow. Provisioned credentials ride
|
|
393
|
+
// headers/env instead, so fail directly rather than opening a browser prompt.
|
|
394
|
+
oauth: false,
|
|
395
|
+
timeout: OPENCODE_MCP_TIMEOUT_MS,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export interface OpenCodeMcpConfigOpts {
|
|
400
|
+
relayUrl: string;
|
|
401
|
+
endpoint?: string;
|
|
402
|
+
includeRelay: boolean;
|
|
403
|
+
generation?: ToolGeneration;
|
|
404
|
+
servers?: Record<string, ProvisioningMcpServer>;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Render OpenCode's native `opencode.json#mcp` map without persisting any Relay credential. */
|
|
408
|
+
export function openCodeMcpConfigPayload(opts: OpenCodeMcpConfigOpts): Record<string, unknown> {
|
|
409
|
+
const mcp: Record<string, unknown> = {};
|
|
410
|
+
if (opts.includeRelay) {
|
|
411
|
+
mcp[mcpServerInfo(opts.generation ?? "relay").name] = {
|
|
412
|
+
type: "remote",
|
|
413
|
+
url: generationEndpoint(opts.endpoint ?? relayMcpEndpoint(opts.relayUrl), opts.generation ?? "relay"),
|
|
414
|
+
headers: { Authorization: `Bearer {env:${RELAY_MCP_TOKEN_ENV}}` },
|
|
415
|
+
oauth: false,
|
|
416
|
+
timeout: OPENCODE_MCP_TIMEOUT_MS,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
for (const [name, server] of Object.entries(opts.servers ?? {})) {
|
|
420
|
+
if (isRelayMcpServerName(name)) continue;
|
|
421
|
+
const entry = openCodeMcpServerEntry(server);
|
|
422
|
+
if (entry) mcp[name] = entry;
|
|
423
|
+
}
|
|
424
|
+
return mcp;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function claudeMcpLaunchArgs(opts: ClaudeMcpConfigOpts & {
|
|
428
|
+
strict?: boolean;
|
|
429
|
+
/** #1292 — when set, `--mcp-config` points at this path instead of carrying the JSON
|
|
430
|
+
* inline. Inline JSON lands verbatim in argv, which is world-readable via `ps`/
|
|
431
|
+
* `/proc/<pid>/cmdline`; a provisioned server's `env`/`headers` can carry a literal
|
|
432
|
+
* secret, so any launch with a relay-owned scratch dir to write into (see
|
|
433
|
+
* `providerProvisioningHomePathFor`) MUST use this instead. The caller is responsible
|
|
434
|
+
* for actually writing the file (the launch assembler is pure) — see
|
|
435
|
+
* `materializeLaunchAssembly` / `claudeMcpConfigPayload`. */
|
|
436
|
+
configFilePath?: string;
|
|
437
|
+
}): string[] {
|
|
438
|
+
const mcpServers = claudeMcpConfigPayload(opts);
|
|
439
|
+
// Nothing to inject and not enforcing strict isolation → don't pass an empty config.
|
|
440
|
+
if (Object.keys(mcpServers).length === 0 && !opts.strict) return [];
|
|
441
|
+
const configArg = opts.configFilePath ?? JSON.stringify({ mcpServers });
|
|
442
|
+
return ["--mcp-config", configArg, ...(opts.strict ? ["--strict-mcp-config"] : [])];
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// #555 (Codex) — `-c mcp_servers.<name>.*` overrides for each relay-provisioned server, the
|
|
446
|
+
// same shape relayMcpCodexConfigArgs uses for the relay endpoint. #1292 — this puts a
|
|
447
|
+
// provisioned server's literal `env`/`headers` secret material in argv (world-readable via
|
|
448
|
+
// `ps`/`/proc/<pid>/cmdline`), so it's ONLY safe to use when there is no isolated CODEX_HOME
|
|
449
|
+
// to materialize `codexProvisionedMcpToml` into instead (host-base Codex reuses the real
|
|
450
|
+
// `~/.codex`, which relay must not write provisioned secrets into — see
|
|
451
|
+
// `providerHomePathFor`). Callers with an isolated CODEX_HOME must use
|
|
452
|
+
// `codexProvisionedMcpTimeoutArgs` + `codexProvisionedMcpToml` instead.
|
|
453
|
+
export function codexProvisionedMcpArgs(servers?: Record<string, ProvisioningMcpServer>): string[] {
|
|
454
|
+
const args: string[] = [];
|
|
455
|
+
for (const [name, rawServer] of Object.entries(servers ?? {})) {
|
|
456
|
+
if (isRelayMcpServerName(name)) continue;
|
|
457
|
+
const server = codexVaultMcpEnv(mergedConfigEnv(rawServer));
|
|
458
|
+
const key = `mcp_servers.${name}`;
|
|
459
|
+
if (server.command) args.push("-c", `${key}.command=${tomlString(server.command)}`);
|
|
460
|
+
if (server.args?.length) args.push("-c", `${key}.args=[${server.args.map(tomlString).join(",")}]`);
|
|
461
|
+
if (server.url) args.push("-c", `${key}.url=${tomlString(server.url)}`);
|
|
462
|
+
if (server.env_vars) args.push("-c", `${key}.env_vars=${tomlInlineValue(server.env_vars)}`);
|
|
463
|
+
for (const [envKey, envVal] of Object.entries(server.env ?? {})) {
|
|
464
|
+
args.push("-c", `${key}.env.${envKey}=${tomlString(envVal)}`);
|
|
465
|
+
}
|
|
466
|
+
if (server.disabled_tools?.length) args.push("-c", `${key}.disabled_tools=${tomlValue(server.disabled_tools)}`);
|
|
467
|
+
args.push(...codexTimeoutConfigArgs(key)); // #1248
|
|
468
|
+
}
|
|
469
|
+
return args;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// #1292 — the non-secret half of the provisioned-server `-c` overrides (per-server
|
|
473
|
+
// tool/startup timeouts only). Pairs with `codexProvisionedMcpToml`, which carries
|
|
474
|
+
// command/args/url/env into CODEX_HOME/config.toml instead of argv.
|
|
475
|
+
export function codexProvisionedMcpTimeoutArgs(servers?: Record<string, ProvisioningMcpServer>): string[] {
|
|
476
|
+
const args: string[] = [];
|
|
477
|
+
for (const name of Object.keys(servers ?? {})) {
|
|
478
|
+
if (isRelayMcpServerName(name)) continue;
|
|
479
|
+
args.push(...codexTimeoutConfigArgs(`mcp_servers.${name}`));
|
|
480
|
+
}
|
|
481
|
+
return args;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// #1292 — renders provisioned servers as `[mcp_servers.<name>]` / `[mcp_servers.<name>.env]`
|
|
485
|
+
// TOML tables for CODEX_HOME/config.toml, mirroring the `-c mcp_servers.<name>.*` dotted-key
|
|
486
|
+
// shape `codexProvisionedMcpArgs` uses, so command/args/url/env never touch argv. Table/key
|
|
487
|
+
// names are always TOML-quoted (server + env-var names aren't guaranteed bare-key safe).
|
|
488
|
+
export function codexProvisionedMcpToml(servers?: Record<string, ProvisioningMcpServer>): string {
|
|
489
|
+
const blocks: string[] = [];
|
|
490
|
+
for (const [name, rawServer] of Object.entries(servers ?? {})) {
|
|
491
|
+
if (isRelayMcpServerName(name)) continue;
|
|
492
|
+
const server = codexVaultMcpEnv(mergedConfigEnv(rawServer));
|
|
493
|
+
const tableKey = `mcp_servers.${tomlString(name)}`;
|
|
494
|
+
const lines: string[] = [`[${tableKey}]`];
|
|
495
|
+
if (server.command) lines.push(`command = ${tomlString(server.command)}`);
|
|
496
|
+
if (server.args?.length) lines.push(`args = [${server.args.map(tomlString).join(", ")}]`);
|
|
497
|
+
if (server.url) lines.push(`url = ${tomlString(server.url)}`);
|
|
498
|
+
const native = codexNativeMcpToml(server);
|
|
499
|
+
lines.push(...native.root);
|
|
500
|
+
if (lines.length > 1) blocks.push(lines.join("\n"));
|
|
501
|
+
const envEntries = Object.entries(server.env ?? {});
|
|
502
|
+
if (envEntries.length) {
|
|
503
|
+
blocks.push([`[${tableKey}.env]`, ...envEntries.map(([k, v]) => `${tomlString(k)} = ${tomlString(v)}`)].join("\n"));
|
|
504
|
+
}
|
|
505
|
+
for (const nested of native.nested) {
|
|
506
|
+
blocks.push([`[${tableKey}.${nested.name}]`, ...nested.lines].join("\n"));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return blocks.length ? `${blocks.join("\n\n")}\n` : "";
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Replace a `[header]` TOML table's body in-place (or append it if absent). Used to keep
|
|
513
|
+
// repeated materialize calls for the same instance idempotent — a changed server env
|
|
514
|
+
// replaces the old table body instead of duplicating it.
|
|
515
|
+
/** Remove every `[prefix…]` table (header through the line before the next header). */
|
|
516
|
+
export function withoutTomlTables(config: string, prefix: string): string {
|
|
517
|
+
const lines = config.split("\n");
|
|
518
|
+
const kept: string[] = [];
|
|
519
|
+
let dropping = false;
|
|
520
|
+
for (const line of lines) {
|
|
521
|
+
const header = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/.exec(line);
|
|
522
|
+
if (header) dropping = header[1]!.trim().startsWith(prefix);
|
|
523
|
+
if (!dropping) kept.push(line);
|
|
524
|
+
}
|
|
525
|
+
return kept.join("\n");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export function upsertTomlTable(config: string, header: string, bodyLines: string[]): string {
|
|
529
|
+
const headerLine = `[${header}]`;
|
|
530
|
+
const lines = config.length ? config.split("\n") : [];
|
|
531
|
+
const headerIndex = lines.findIndex((line) => line.trim() === headerLine);
|
|
532
|
+
if (headerIndex < 0) {
|
|
533
|
+
const prefix = config.length ? (config.endsWith("\n") ? config : `${config}\n`) : "";
|
|
534
|
+
return `${prefix}${prefix.length ? "\n" : ""}${headerLine}\n${bodyLines.join("\n")}${bodyLines.length ? "\n" : ""}`;
|
|
535
|
+
}
|
|
536
|
+
let nextHeaderIndex = lines.length;
|
|
537
|
+
for (let i = headerIndex + 1; i < lines.length; i += 1) {
|
|
538
|
+
if (/^\s*\[[^\]]+\]\s*(?:#.*)?$/.test(lines[i] ?? "")) {
|
|
539
|
+
nextHeaderIndex = i;
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
lines.splice(headerIndex + 1, nextHeaderIndex - (headerIndex + 1), ...bodyLines);
|
|
544
|
+
return lines.join("\n");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// #1292 — idempotently upsert provisioned MCP servers' command/args/url/env into
|
|
548
|
+
// CODEX_HOME/config.toml, the isolated per-instance home Codex actually reads. Pairs with
|
|
549
|
+
// `codexProvisionedMcpTimeoutArgs` (the non-secret `-c` overrides that stay in argv). Only
|
|
550
|
+
// safe to call for an ISOLATED CODEX_HOME (host-base reuses the real `~/.codex`, which relay
|
|
551
|
+
// must never write provisioned secrets into) — callers gate on `providerHomePathFor`.
|
|
552
|
+
export function writeCodexMcpServersToml(codexHome: string, servers: Record<string, ProvisioningMcpServer> | undefined): void {
|
|
553
|
+
const relevant = Object.entries(servers ?? {}).filter(([name]) => !isRelayMcpServerName(name));
|
|
554
|
+
const configPath = join(codexHome, "config.toml");
|
|
555
|
+
const existing = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
|
|
556
|
+
// #3387 — every mcp_servers table in a managed CODEX_HOME is Relay-written: drop the previous
|
|
557
|
+
// launch's set first, so a host MCP class switched off on reuse leaves no stale server behind.
|
|
558
|
+
let config = withoutTomlTables(existing, "mcp_servers.");
|
|
559
|
+
if (!relevant.length) {
|
|
560
|
+
if (config !== existing) writeFileSync(configPath, config, { mode: 0o600 });
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
mkdirSync(codexHome, { recursive: true });
|
|
564
|
+
for (const [name, rawServer] of relevant) {
|
|
565
|
+
const server = codexVaultMcpEnv(mergedConfigEnv(rawServer));
|
|
566
|
+
const tableKey = `mcp_servers.${tomlString(name)}`;
|
|
567
|
+
const bodyLines: string[] = [];
|
|
568
|
+
if (server.command) bodyLines.push(`command = ${tomlString(server.command)}`);
|
|
569
|
+
if (server.args?.length) bodyLines.push(`args = [${server.args.map(tomlString).join(", ")}]`);
|
|
570
|
+
if (server.url) bodyLines.push(`url = ${tomlString(server.url)}`);
|
|
571
|
+
const native = codexNativeMcpToml(server);
|
|
572
|
+
bodyLines.push(...native.root);
|
|
573
|
+
config = upsertTomlTable(config, tableKey, bodyLines);
|
|
574
|
+
const envEntries = Object.entries(server.env ?? {});
|
|
575
|
+
if (envEntries.length) {
|
|
576
|
+
config = upsertTomlTable(config, `${tableKey}.env`, envEntries.map(([k, v]) => `${tomlString(k)} = ${tomlString(v)}`));
|
|
577
|
+
}
|
|
578
|
+
for (const nested of native.nested) {
|
|
579
|
+
config = upsertTomlTable(config, `${tableKey}.${nested.name}`, nested.lines);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
writeFileSync(configPath, config, { mode: 0o600 });
|
|
583
|
+
}
|