Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 11 additions & 13 deletions .choir/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,17 @@ verify_cmd = "moon check --target native && moon test --target native"
# Per-agent model selection (choir-w5m7). Passthrough to `codex --model <value>`.
# dev (gpt-5.3-codex-spark) PARKED: spark could not drive the agentic TDD-leaf
# protocol (mis-sequenced Red->Done, false verify-pass, exit-255 build) on first
# trial. gpt-5.6-luna PARKED too (2026-07-09): every codex dev leaf spawned
# under luna had its `choir mcp-stdio` MCP subprocess launch with a completely
# empty CHOIR_* environment (confirmed via /proc/<pid>/environ), unlike gpt-5.5
# leaves and unlike root's own Claude-Code-spawned mcp-stdio process, which both
# correctly inherit CHOIR_AGENT_TOKEN etc. Missing token -> registration
# silently rejected server-side (serve_authenticate_register_request) but
# mcp_perform_registration_handshake only checks for a non-empty response, not
# success, so the bridge proceeds as if authenticated and the next real
# tools/call (tdd_intent et al) then fails opaquely as "server restarting,
# retry" -- see choir bead for the write-up. Root cause not fully pinned
# (whether it's luna specifically or the whole 5.6 family under some codex
# runtime mode), so reverting dev to the known-good gpt-5.5 baseline rather
# than guessing at terra. Effort is the real quota lever — see choir-mn0l.
# trial. gpt-5.6-luna was parked on 2026-07-09 blaming it for codex leaves'
# `choir mcp-stdio` subprocess launching with an empty CHOIR_* environment;
# that was disproven the same day — gpt-5.5 leaves had identically scrubbed
# env. Real cause: codex's MCP stdio launcher builds server environments from
# scratch (env_clear() + DEFAULT_ENV_VARS whitelist in rmcp-client's
# create_env_for_mcp_server; shell_environment_policy only affects shell/exec
# tools, not MCP), and choir's codex MCP entry set no per-server env
# forwarding (choir-ghl9, fixed via
# `mcp_servers.choir.env_vars` in codex_mcp_override_args). Model was never at
# fault; re-tiering dev beyond gpt-5.5 is the operator's call once the fix
# proves out. Effort is the real quota lever — see choir-mn0l.
[model.codex]
default = "gpt-5.5"

Expand Down
13 changes: 13 additions & 0 deletions src/workspace/launch.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,25 @@ pub fn codex_mcp_override_args(
]
}
let args_lit = toml_string_array_literal(elems)
// codex's MCP stdio launcher builds each server's environment from scratch
// (`env_clear()` + a DEFAULT_ENV_VARS whitelist in rmcp-client's
// `create_env_for_mcp_server`; `shell_environment_policy` only governs
// shell/exec tools, not MCP), so `choir mcp-stdio` never inherits CHOIR_*
// ambiently (choir-ghl9). `env_vars` is codex's names-only forward
// list: values come from codex's own environment at spawn time and never
// ride this argv (argv leaks into ps/proc; the token is a secret). Identity
// and the socket path come from the per-leaf .choir/config.local.toml in
// `cwd` plus the --role/--name args above, so the token is the only
// env-sourced input registration needs.
let env_vars_lit = toml_string_array_literal(["CHOIR_AGENT_TOKEN"])
let args = [
"-c",
"mcp_servers.choir.command=\"choir\"",
"-c",
"mcp_servers.choir.args=" + args_lit,
"-c",
"mcp_servers.choir.env_vars=" + env_vars_lit,
"-c",
"mcp_servers.choir.cwd=\"" + escape_toml_string(workspace_path) + "\"",
]
if semble_pilot {
Expand Down
21 changes: 21 additions & 0 deletions src/workspace/workspace_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,27 @@ test "codex_mcp_override_args is argv-shaped Toml without sh" {
assert_false(args[3].contains("-lc"))
}

///|
test "codex_mcp_override_args forwards registration env by name only" {
for role in [@types.Role::TL, @types.Role::Dev, @types.Role::Worker] {
let args = @workspace.codex_mcp_override_args(role, "main.leaf", "/proj")
// codex does not inherit the shell environment into MCP stdio
// subprocesses; the names-only `env_vars` forward list is the one
// mechanism that gets CHOIR_AGENT_TOKEN there without the value.
assert_true(
args.contains("mcp_servers.choir.env_vars=[\"CHOIR_AGENT_TOKEN\"]"),
)
for arg in args {
// The token value must never ride argv: no `env` value-map entry
// (`mcp_servers.choir.env.KEY="..."` / `env={...}`) and no
// KEY=value assignment for the token anywhere.
assert_false(arg.contains("mcp_servers.choir.env."))
assert_false(arg.contains("mcp_servers.choir.env="))
assert_false(arg.contains("CHOIR_AGENT_TOKEN="))
}
}
}

///|
test "codex_mcp_override_args includes semble server when pilot is enabled" {
let args = @workspace.codex_mcp_override_args(
Expand Down
Loading