claude-multiacc 2.0.38 → 2.0.39

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.
@@ -41,6 +41,62 @@ mkdir -p "$ACC/tmp" "$FAKEBIN"
41
41
  cat > "$FAKEBIN/claude" <<'EOF'
42
42
  #!/usr/bin/env bash
43
43
  # fake real claude for tests (not a multiacc shim)
44
+ if [ "${1:-}" = "mcp" ] && [ -n "${CLAUDE_CONFIG_DIR:-}" ]; then
45
+ # A stand-in for the real `claude mcp add|add-json|remove`: `-s user` edits the
46
+ # config dir's .claude.json mcpServers, the default `local` scope edits
47
+ # projects[$PWD].mcpServers, exactly where the real client keeps them.
48
+ python3 - "$CLAUDE_CONFIG_DIR/.claude.json" "$PWD" "$@" <<'PY'
49
+ import json, os, sys
50
+ path, cwd, argv = sys.argv[1], sys.argv[2], sys.argv[3:]
51
+ try:
52
+ doc = json.load(open(path))
53
+ except Exception:
54
+ doc = {}
55
+ verb, rest = argv[1], argv[2:]
56
+ scope, name, cmd, envs, js = None, None, [], {}, None
57
+ i = 0
58
+ while i < len(rest):
59
+ a = rest[i]
60
+ if a in ("-s", "--scope"):
61
+ scope = rest[i + 1]; i += 2; continue
62
+ if a in ("-e", "--env"):
63
+ k, v = rest[i + 1].split("=", 1); envs[k] = v; i += 2; continue
64
+ if a == "--":
65
+ cmd = rest[i + 1:]; break
66
+ if name is None:
67
+ name = a
68
+ elif verb == "add-json" and js is None:
69
+ js = a
70
+ i += 1
71
+ def user_bucket(create):
72
+ return doc.setdefault("mcpServers", {}) if create else (doc.get("mcpServers") or {})
73
+ def local_bucket(create):
74
+ if create:
75
+ return doc.setdefault("projects", {}).setdefault(cwd, {}).setdefault("mcpServers", {})
76
+ return ((doc.get("projects") or {}).get(cwd) or {}).get("mcpServers") or {}
77
+ if verb in ("add", "add-json"):
78
+ block = json.loads(js) if verb == "add-json" else {"type": "stdio", "command": cmd[0], "args": cmd[1:], "env": envs}
79
+ (user_bucket(True) if scope == "user" else local_bucket(True))[name] = block
80
+ print("Added stdio MCP server %s to %s config" % (name, scope or "local"))
81
+ else:
82
+ buckets = {"user": user_bucket, "local": local_bucket}
83
+ order = [scope] if scope else ["local", "user"]
84
+ for sc in order:
85
+ b = buckets[sc](False)
86
+ if name in b:
87
+ del b[name]; print("Removed MCP server %s from %s config" % (name, sc)); break
88
+ else:
89
+ print("No MCP server found with name: %s" % name, file=sys.stderr); sys.exit(1)
90
+ with open(path + ".tmp", "w") as f:
91
+ json.dump(doc, f, indent=2)
92
+ os.replace(path + ".tmp", path)
93
+ PY
94
+ rc=$?
95
+ # FAKE_MCP_WRITE_THEN_FAIL=1: the client DID write the file and then died (a crash
96
+ # after its save) — the shim must pass the failure through and mirror nothing.
97
+ [ "$rc" = 0 ] && [ -n "${FAKE_MCP_WRITE_THEN_FAIL:-}" ] && exit 1
98
+ exit $rc
99
+ fi
44
100
  if [ "${1:-}" = "auth" ] && [ "${2:-}" = "status" ]; then
45
101
  if [ -n "${FAKE_AUTH_FAIL:-}" ]; then echo '{"loggedIn": false}'; exit 0; fi
46
102
  # A setup token is minted with scope user:inference ALONE, so the real CLI answers
@@ -3931,6 +3987,29 @@ EOF
3931
3987
  [ "$n_remov" = "2" ] && t_ok "removal propagation ran on each target" \
3932
3988
  || t_fail "per-target removal" "expected 2 removal sweeps, saw $n_remov"
3933
3989
  grep -q "post-sync" "$SSHLOG" && t_ok "post-sync hooks invoked" || t_fail "post-sync hooks" "none recorded"
3990
+ # The MCP registry rides with the manifest to EVERY target; the machine-local overlay
3991
+ # (a runner daemon's view of THIS Mac) never leaves. Without a registry file nothing
3992
+ # about it is pushed, so a target's own registry is never blanked.
3993
+ grep -q "mcp-servers" "$SSHLOG" \
3994
+ && t_fail "registry push without a registry" "pushed a registry that does not exist: $(grep mcp-servers "$SSHLOG")" \
3995
+ || t_ok "no registry file => no registry push"
3996
+ printf '{"version":1,"mcpServers":{"regsync":{"type":"stdio","command":"echo","args":["hi"],"env":{}}},"retired":[],"projects":{}}\n' \
3997
+ > "$ACC/mcp-servers.json"
3998
+ printf '{"version":1,"owners":{"panel":{"mcpServers":{"local-only":{"type":"stdio","command":"true","args":[],"env":{}}},"retired":[]}}}\n' \
3999
+ > "$ACC/mcp-servers.local.json"
4000
+ : > "$SSHLOG"
4001
+ out="$(PATH="$SSHFAKE:$PATH" CLAUDE_MULTIACC_NO_SYNC=0 claude-accounts sync 2>&1)"
4002
+ rc=$?
4003
+ [ "$rc" = "0" ] && t_ok "sync with a registry succeeds" || t_fail "registry sync" "rc=$rc: $out"
4004
+ grep -q "rsync.*mcp-servers.json root@203.0.113.1:/root/.claude-accounts/mcp-servers.json" "$SSHLOG" \
4005
+ && grep -q "rsync.*mcp-servers.json gas@peer1:/Users/gas/.claude-accounts/mcp-servers.json" "$SSHLOG" \
4006
+ && t_ok "the MCP registry is pushed to BOTH targets" \
4007
+ || t_fail "registry push" "$(grep mcp-servers "$SSHLOG")"
4008
+ grep -q "mcp-servers.local.json" "$SSHLOG" \
4009
+ && t_fail "overlay push" "the machine-local overlay was pushed: $(grep local.json "$SSHLOG")" \
4010
+ || t_ok "the machine-local overlay is never pushed"
4011
+ rm -f "$ACC/mcp-servers.json" "$ACC/mcp-servers.local.json"
4012
+ for _d in "$ACC"/acct-*; do rm -f "$_d/.mcp-applied"; done
3934
4013
  # a primary failure aborts the WHOLE sync: the peer is never contacted
3935
4014
  : > "$SSHLOG"
3936
4015
  out="$(PATH="$SSHFAKE:$PATH" SSH_FAIL_HOST="root@203.0.113.1" CLAUDE_MULTIACC_NO_SYNC=0 claude-accounts sync 2>&1)"
@@ -4073,6 +4152,26 @@ dp_drain
4073
4152
  && t_ok "only an exact 'replica' value suppresses the telemetry push" \
4074
4153
  || t_fail "replica anchor (distribute)" "'not-replica' suppressed the push"
4075
4154
  rm -f "$DPOOL/sync-role"
4155
+ # The MCP registry rides along with the telemetry, so a server the shim mirrored from a
4156
+ # stock 'claude mcp add' reaches every peer within one limits cadence. The overlay does not.
4157
+ printf '{"version":1,"mcpServers":{"ride":{"type":"stdio","command":"echo","args":["hi"],"env":{}}},"retired":[],"projects":{}}\n' \
4158
+ > "$DPOOL/mcp-servers.json"
4159
+ printf '{"version":1,"owners":{"panel":{"mcpServers":{},"retired":[]}}}\n' > "$DPOOL/mcp-servers.local.json"
4160
+ : > "$RSLOG"
4161
+ PATH="$RSFAKE:$PATH" CLAUDE_ACCOUNTS_ROOT="$DPOOL" \
4162
+ CLAUDE_MULTIACC_USAGE_URL="file://$WORK/usage-low.json" claude-accounts limits --force --quiet
4163
+ waited=0
4164
+ while [ "$waited" -lt 50 ] && [ "$(grep -c '^RSYNC ' "$RSLOG" 2>/dev/null)" -lt 3 ]; do
4165
+ sleep 0.2; waited=$((waited + 1))
4166
+ done
4167
+ dp_drain
4168
+ [ "$(grep -c '^FILE mcp-servers.json$' "$RSLOG")" = "3" ] \
4169
+ && t_ok "the MCP registry rides along with the telemetry push to all three targets" \
4170
+ || t_fail "registry in limits push" "mcp-servers.json listed $(grep -c '^FILE mcp-servers.json$' "$RSLOG")x, want 3: $(grep FILE "$RSLOG" | tr '\n' '|')"
4171
+ grep -q '^FILE mcp-servers.local.json$' "$RSLOG" \
4172
+ && t_fail "overlay in limits push" "the machine-local overlay was pushed" \
4173
+ || t_ok "the machine-local overlay never rides along"
4174
+ rm -f "$DPOOL/mcp-servers.json" "$DPOOL/mcp-servers.local.json"
4076
4175
 
4077
4176
  # ---- 16d-lock. a distribute lock is honored while it is alive, broken once it is not -
4078
4177
  # The push serializes on a lock DIRECTORY, and a detached push that is killed (logout,
@@ -4148,6 +4247,253 @@ case "$out" in
4148
4247
  esac
4149
4248
  rm -f "$WORK/ctl-brokencli"
4150
4249
 
4250
+ # ---- 16e. MCP servers for every account ---------------------------------------------
4251
+ # Claude Code keeps MCP servers in the CONFIG DIR, so under the pool a stock `claude mcp
4252
+ # add` used to land in ONE random account — appinspire was connected in some sessions
4253
+ # and not in others (2026-09-22). The pool's registry (mcp-servers.json) is reconciled
4254
+ # into every account at seed time and by the shim right before exec, and a stock add
4255
+ # under the shim is mirrored into the registry. Own pool root, so nothing here leaks
4256
+ # into the sections above or below.
4257
+ MPOOL="$WORK/mcp-pool"
4258
+ mkdir -p "$MPOOL/acct-01" "$MPOOL/acct-02" "$MPOOL/tmp"
4259
+ : > "$MPOOL/.limits-kick"
4260
+ cat > "$MPOOL/accounts.json" <<'EOF'
4261
+ {"version":1,"server":"none","threshold":90,
4262
+ "accounts":[
4263
+ {"id":"acct-01","email":"m1@test","home":"mac","added_at":"2026-07-13T00:00:00Z"},
4264
+ {"id":"acct-02","email":"m2@test","home":"mac","added_at":"2026-07-13T00:00:00Z"}]}
4265
+ EOF
4266
+ for i in 01 02; do
4267
+ printf '{"claudeAiOauth":{"accessToken":"sk-ant-oat01-mcp%s","refreshToken":"r","expiresAt":9999999999999,"refreshTokenExpiresAt":9999999999999}}' \
4268
+ "$i" > "$MPOOL/acct-$i/.credentials.json"
4269
+ printf '{"mcpServers":{"own-%s":{"type":"stdio","command":"true","args":[],"env":{}}},"numStartups":1}\n' "$i" \
4270
+ > "$MPOOL/acct-$i/.claude.json"
4271
+ done
4272
+ # A python that LOGS every invocation, then runs the real python3: proves whether the
4273
+ # shim spent a python start-up on a launch, which the stamp exists to avoid.
4274
+ PYLOG="$WORK/mcp-pylog"
4275
+ cat > "$WORK/pylog.sh" <<EOF
4276
+ #!/usr/bin/env bash
4277
+ printf 'PY %s\n' "\$*" >> "$PYLOG"
4278
+ exec python3 "\$@"
4279
+ EOF
4280
+ chmod +x "$WORK/pylog.sh"
4281
+ mcp_has() { # mcp_has <acct> <name> [<project>] -> 0 when the account carries the server
4282
+ python3 - "$MPOOL/$1/.claude.json" "$2" "${3:-}" <<'PY'
4283
+ import json, sys
4284
+ doc = json.load(open(sys.argv[1]))
4285
+ name, project = sys.argv[2], sys.argv[3]
4286
+ if project:
4287
+ sys.exit(0 if name in (((doc.get("projects") or {}).get(project) or {}).get("mcpServers") or {}) else 1)
4288
+ sys.exit(0 if name in (doc.get("mcpServers") or {}) else 1)
4289
+ PY
4290
+ }
4291
+ # (iv) the CLI: add for THIS provider only, list, remove (retires), apply repairs drift
4292
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp add --provider claude reg1 -e K=v -- echo hi 2>&1)"
4293
+ rc=$?
4294
+ [ "$rc" = "0" ] && t_ok "mcp add (claude only) exits 0" || t_fail "mcp add rc" "rc=$rc: $out"
4295
+ [ -f "$MPOOL/mcp-servers.json" ] && t_ok "mcp add writes the pool registry" || t_fail "registry file" "missing"
4296
+ mcp_has acct-01 reg1 && mcp_has acct-02 reg1 && t_ok "mcp add reconciles EVERY account" \
4297
+ || t_fail "mcp add reconcile" "$(cat "$MPOOL/acct-01/.claude.json" "$MPOOL/acct-02/.claude.json")"
4298
+ mcp_has acct-01 own-01 && mcp_has acct-02 own-02 && t_ok "an account's own extra servers survive the reconcile" \
4299
+ || t_fail "extras preserved" "$(cat "$MPOOL/acct-01/.claude.json")"
4300
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp list 2>&1)"
4301
+ check "mcp list names the server" "reg1" "$out"
4302
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp list --json 2>&1)"
4303
+ python3 -c 'import json,sys; d=json.loads(sys.argv[1]); sys.exit(0 if "reg1" in d.get("mcpServers",{}) else 1)' "$out" \
4304
+ && t_ok "mcp list --json carries mcpServers" || t_fail "mcp list --json" "$out"
4305
+ # drift: the client rewrote the file without the server (its in-memory state was older)
4306
+ python3 - "$MPOOL/acct-02/.claude.json" <<'PY'
4307
+ import json, os, sys
4308
+ p = sys.argv[1]; d = json.load(open(p)); d["mcpServers"].pop("reg1", None)
4309
+ json.dump(d, open(p + ".tmp", "w")); os.replace(p + ".tmp", p)
4310
+ PY
4311
+ mcp_has acct-02 reg1 && t_fail "drift fixture" "reg1 still present" || t_ok "drift fixture: acct-02 lost reg1"
4312
+ # (i) a launch reconciles the picked account before exec, and stamps it
4313
+ : > "$PYLOG"
4314
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_ACCOUNT=acct-02 CLAUDE_MULTIACC_PYTHON="$WORK/pylog.sh" claude 2>&1)"
4315
+ check "launch runs on the pinned account" "CFG=acct-02" "$out"
4316
+ mcp_has acct-02 reg1 && t_ok "the shim reconciles the picked account before exec" \
4317
+ || t_fail "shim reconcile" "$(cat "$MPOOL/acct-02/.claude.json")"
4318
+ [ -s "$MPOOL/acct-02/.mcp-applied" ] && t_ok "the reconcile leaves a stamp" || t_fail "stamp" "no .mcp-applied"
4319
+ grep -q "apply" "$PYLOG" && t_ok "python ran once for the drifted account" || t_fail "python ran" "$(cat "$PYLOG")"
4320
+ # (ii) the stamp short-circuits: nothing changed => no python at all; python gone => still launches
4321
+ : > "$PYLOG"
4322
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_ACCOUNT=acct-02 CLAUDE_MULTIACC_PYTHON="$WORK/pylog.sh" claude 2>&1)"
4323
+ check "second launch still runs" "CFG=acct-02" "$out"
4324
+ [ ! -s "$PYLOG" ] && t_ok "an unchanged registry + config spends no python start-up" \
4325
+ || t_fail "stamp short-circuit" "python ran: $(cat "$PYLOG")"
4326
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_ACCOUNT=acct-02 CLAUDE_MULTIACC_PYTHON=/nonexistent/python3 claude 2>&1)"
4327
+ rc=$?
4328
+ [ "$rc" = "0" ] && check "a missing python never breaks a launch" "CFG=acct-02" "$out" \
4329
+ || t_fail "fail-open python" "rc=$rc: $out"
4330
+ touch "$MPOOL/mcp-servers.json"
4331
+ sleep 1; touch "$MPOOL/mcp-servers.json"
4332
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_ACCOUNT=acct-02 CLAUDE_MULTIACC_PYTHON=/nonexistent/python3 claude 2>&1)"
4333
+ rc=$?
4334
+ [ "$rc" = "0" ] && check "a changed registry with no python still launches (fail-open)" "CFG=acct-02" "$out" \
4335
+ || t_fail "fail-open changed registry" "rc=$rc: $out"
4336
+ : > "$PYLOG"
4337
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_ACCOUNT=acct-02 CLAUDE_MULTIACC_PYTHON="$WORK/pylog.sh" claude 2>&1)"
4338
+ grep -q "apply" "$PYLOG" && t_ok "a touched registry re-runs the reconcile" || t_fail "registry mtime" "no python run"
4339
+ # (vi) the kill switch: no reconcile at launch
4340
+ python3 - "$MPOOL/acct-01/.claude.json" <<'PY'
4341
+ import json, os, sys
4342
+ p = sys.argv[1]; d = json.load(open(p)); d["mcpServers"].pop("reg1", None)
4343
+ json.dump(d, open(p + ".tmp", "w")); os.replace(p + ".tmp", p)
4344
+ PY
4345
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_ACCOUNT=acct-01 CLAUDE_MULTIACC_MCP=0 claude 2>&1)"
4346
+ check "kill switch: launch still runs" "CFG=acct-01" "$out"
4347
+ mcp_has acct-01 reg1 && t_fail "kill switch" "CLAUDE_MULTIACC_MCP=0 still reconciled" \
4348
+ || t_ok "CLAUDE_MULTIACC_MCP=0 leaves the account alone"
4349
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp apply 2>&1)"
4350
+ mcp_has acct-01 reg1 && t_ok "mcp apply repairs the drifted account" || t_fail "mcp apply" "$out"
4351
+ # (iii) learn-from-write: a stock 'claude mcp add' under the shim reaches EVERY account
4352
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude mcp add -s user x -- echo hi 2>&1)"
4353
+ rc=$?
4354
+ [ "$rc" = "0" ] && check "stock 'claude mcp add' still answers like the real client" "Added stdio MCP server x" "$out" \
4355
+ || t_fail "stock mcp add rc" "rc=$rc: $out"
4356
+ mcp_has acct-01 x && mcp_has acct-02 x && t_ok "a stock 'claude mcp add -s user' lands in EVERY account" \
4357
+ || t_fail "mirror add" "$(cat "$MPOOL/acct-01/.claude.json" "$MPOOL/acct-02/.claude.json")"
4358
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp list --json 2>&1)"
4359
+ python3 -c 'import json,sys; d=json.loads(sys.argv[1]); s=d["mcpServers"]["x"]; sys.exit(0 if s.get("command")=="echo" and s.get("args")==["hi"] else 1)' "$out" \
4360
+ && t_ok "the mirrored server is in the registry with the client's own block" || t_fail "mirror registry" "$out"
4361
+ mkdir -p "$WORK/mcp-proj"
4362
+ # the client keys the project by its normalized cwd, so ask the shell for that spelling
4363
+ MPROJ="$(cd "$WORK/mcp-proj" && pwd)"
4364
+ out="$(cd "$MPROJ" && CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude mcp add y -- echo yo 2>&1)"
4365
+ mcp_has acct-01 y "$MPROJ" && mcp_has acct-02 y "$MPROJ" \
4366
+ && t_ok "a stock project-local add is mirrored for that project in EVERY account" \
4367
+ || t_fail "mirror local add" "$out $(cat "$MPOOL/acct-01/.claude.json")"
4368
+ mcp_has acct-01 y && t_fail "local scope leak" "y landed in user scope" || t_ok "a project-local add stays project-local"
4369
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude mcp remove -s user x 2>&1)"
4370
+ rc=$?
4371
+ [ "$rc" = "0" ] && t_ok "stock 'claude mcp remove' exits 0" || t_fail "stock mcp remove rc" "rc=$rc: $out"
4372
+ mcp_has acct-01 x || mcp_has acct-02 x \
4373
+ && t_fail "mirror remove" "x survives in an account" || t_ok "a stock 'claude mcp remove' drops the server from EVERY account"
4374
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp list --json 2>&1)"
4375
+ python3 -c 'import json,sys; d=json.loads(sys.argv[1]); sys.exit(0 if "x" in d.get("retiredUser",[]) and "x" not in d.get("mcpServers",{}) else 1)' "$out" \
4376
+ && t_ok "a learned user-scope remove is a USER-SCOPE tombstone (retiredUser) in the registry" || t_fail "tombstone" "$out"
4377
+ # the kill switch also disables the mirror: the stock add lands in one account only
4378
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_ACCOUNT=acct-01 CLAUDE_MULTIACC_MCP=0 claude mcp add -s user solo -- echo solo 2>&1)"
4379
+ mcp_has acct-01 solo && ! mcp_has acct-02 solo && t_ok "CLAUDE_MULTIACC_MCP=0: a stock add stays in one account" \
4380
+ || t_fail "kill switch mirror" "$out"
4381
+ # a failing real command is never mirrored, and its exit status passes through
4382
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_ACCOUNT=acct-02 claude mcp remove -s user never-there 2>&1)"
4383
+ rc=$?
4384
+ [ "$rc" != "0" ] && t_ok "a failed 'claude mcp remove' keeps its exit status" || t_fail "mcp remove passthrough rc" "rc=0: $out"
4385
+ # ...even when the client wrote its file BEFORE failing: the write stays in that one
4386
+ # account, the registry learns nothing, and the other account is untouched
4387
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_ACCOUNT=acct-02 FAKE_MCP_WRITE_THEN_FAIL=1 claude mcp add -s user wtf -- echo w 2>&1)"
4388
+ rc=$?
4389
+ [ "$rc" != "0" ] && t_ok "a client that writes and then fails still fails the shim run" || t_fail "write-then-fail rc" "rc=0: $out"
4390
+ mcp_has acct-02 wtf && ! mcp_has acct-01 wtf && t_ok "the failed add stays in the one account that wrote it" \
4391
+ || t_fail "write-then-fail leak" "$(cat "$MPOOL/acct-01/.claude.json")"
4392
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp list --json 2>&1)"
4393
+ python3 -c 'import json,sys; d=json.loads(sys.argv[1]); sys.exit(0 if "wtf" not in d.get("mcpServers",{}) and "wtf" not in d.get("retired",[]) and "wtf" not in d.get("retiredUser",[]) else 1)' "$out" \
4394
+ && t_ok "a failed stock add is never mirrored into the registry" || t_fail "failed mirror add" "$out"
4395
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_ACCOUNT=acct-02 FAKE_MCP_WRITE_THEN_FAIL=1 claude mcp remove -s user wtf 2>&1)"
4396
+ rc=$?
4397
+ [ "$rc" != "0" ] && t_ok "a client that removes and then fails still fails the shim run" || t_fail "write-then-fail remove rc" "rc=0: $out"
4398
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp list --json 2>&1)"
4399
+ python3 -c 'import json,sys; d=json.loads(sys.argv[1]); sys.exit(0 if "wtf" not in d.get("retired",[]) and "wtf" not in d.get("retiredUser",[]) else 1)' "$out" \
4400
+ && t_ok "a failed stock remove leaves no tombstone" || t_fail "failed mirror remove" "$out"
4401
+ # NESTED session: a pooled session exports CLAUDE_CONFIG_DIR=<acct> + CLAUDE_SHIM_ACTIVE=1
4402
+ # and its children inherit both, so an agent's `claude mcp add` reaches the passthrough
4403
+ # with the account already chosen — it must be mirrored exactly like a top-level one
4404
+ : > "$PYLOG"
4405
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_CONFIG_DIR="$MPOOL/acct-01" CLAUDE_SHIM_ACTIVE=1 CLAUDE_MULTIACC_PYTHON="$WORK/pylog.sh" claude mcp add -s user nested -- echo n 2>&1)"
4406
+ rc=$?
4407
+ [ "$rc" = "0" ] && t_ok "a nested (in-session) 'claude mcp add' exits 0" || t_fail "nested add rc" "rc=$rc: $out"
4408
+ mcp_has acct-01 nested && mcp_has acct-02 nested && t_ok "a nested add is mirrored into EVERY account" \
4409
+ || t_fail "nested mirror" "$out $(cat "$MPOOL/acct-02/.claude.json")"
4410
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp list --json 2>&1)"
4411
+ python3 -c 'import json,sys; d=json.loads(sys.argv[1]); sys.exit(0 if "nested" in d.get("mcpServers",{}) else 1)' "$out" \
4412
+ && t_ok "a nested add reaches the registry" || t_fail "nested registry" "$out"
4413
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_CONFIG_DIR="$MPOOL/acct-01" CLAUDE_SHIM_ACTIVE=1 claude mcp remove -s user nested 2>&1)"
4414
+ rc=$?
4415
+ [ "$rc" = "0" ] && t_ok "a nested 'claude mcp remove' exits 0" || t_fail "nested remove rc" "rc=$rc: $out"
4416
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp list --json 2>&1)"
4417
+ python3 -c 'import json,sys; d=json.loads(sys.argv[1]); sys.exit(0 if "nested" in d.get("retiredUser",[]) and "nested" not in d.get("mcpServers",{}) else 1)' "$out" \
4418
+ && t_ok "a nested remove leaves a user-scope tombstone" || t_fail "nested tombstone" "$out"
4419
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_ACCOUNT=acct-01 claude 2>&1)"
4420
+ check "the next launch on that account still runs" "CFG=acct-01" "$out"
4421
+ mcp_has acct-01 nested && t_fail "nested remove undone" "the next launch re-added nested" \
4422
+ || t_ok "a nested remove is not undone by the next launch"
4423
+ # a CLAUDE_CONFIG_DIR OUTSIDE the pool is plain passthrough: no python, no mirror
4424
+ mkdir -p "$WORK/mcp-outside"
4425
+ : > "$PYLOG"
4426
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_CONFIG_DIR="$WORK/mcp-outside" CLAUDE_MULTIACC_PYTHON="$WORK/pylog.sh" claude mcp add -s user outside -- echo o 2>&1)"
4427
+ [ "$?" = "0" ] && [ ! -s "$PYLOG" ] && ! mcp_has acct-01 outside \
4428
+ && t_ok "a config dir outside the pool is untouched passthrough (no python, no mirror)" \
4429
+ || t_fail "outside passthrough" "$out; pylog: $(cat "$PYLOG")"
4430
+ # the help pre-scan stops at `--`: -h in the SERVER's command line is not our --help
4431
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp add --provider claude hsvc -- node s.js -h 127.0.0.1 2>&1)"
4432
+ rc=$?
4433
+ case "$out" in *USAGE*) t_fail "help scan" "-h after -- printed usage" ;; *) [ "$rc" = "0" ] && t_ok "-h after -- is the server's own flag, not --help" || t_fail "help scan rc" "rc=$rc: $out" ;; esac
4434
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp list --json 2>&1)"
4435
+ python3 -c 'import json,sys; d=json.loads(sys.argv[1]); sys.exit(0 if d["mcpServers"]["hsvc"]["args"]==["s.js","-h","127.0.0.1"] else 1)' "$out" \
4436
+ && t_ok "the server keeps its -h argument" || t_fail "help scan args" "$out"
4437
+ # retirement reaches a server that was only ever hand-added to ONE account
4438
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp remove --provider claude own-01 2>&1)"
4439
+ mcp_has acct-01 own-01 && t_fail "retire hand-added" "own-01 survives" \
4440
+ || t_ok "mcp remove retires a server that was hand-added to a single account"
4441
+ # --provider both: the change reaches the codex pool beside this one
4442
+ CXM="$WORK/mcp-codex-pool"
4443
+ mkdir -p "$CXM/acct-01" "$CXM/tmp"
4444
+ cat > "$CXM/accounts.json" <<'EOF'
4445
+ {"version":1,"server":"none","threshold":90,
4446
+ "accounts":[{"id":"acct-01","email":"cm1@cx","home":"mac","added_at":"2026-08-21T00:00:00Z"}]}
4447
+ EOF
4448
+ printf 'model = "gpt-5"\n\n[mcp_servers.keep]\ncommand = "true"\nargs = []\n' > "$CXM/acct-01/config.toml"
4449
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CODEX_ACCOUNTS_DIR="$CXM" CODEX_MULTIACC_NO_SYNC=1 claude-accounts mcp add shared -- echo both 2>&1)"
4450
+ rc=$?
4451
+ [ "$rc" = "0" ] && t_ok "mcp add --provider both (default) exits 0" || t_fail "mcp add both rc" "rc=$rc: $out"
4452
+ mcp_has acct-01 shared && t_ok "--provider both: the claude pool has the server" || t_fail "both claude" "$out"
4453
+ grep -q '^\[mcp_servers.shared\]' "$CXM/acct-01/config.toml" \
4454
+ && t_ok "--provider both: the codex pool beside it has [mcp_servers.shared]" \
4455
+ || t_fail "both codex" "$out $(cat "$CXM/acct-01/config.toml")"
4456
+ grep -q '^model = "gpt-5"' "$CXM/acct-01/config.toml" && grep -q '^\[mcp_servers.keep\]' "$CXM/acct-01/config.toml" \
4457
+ && t_ok "the codex config keeps its unrelated content" || t_fail "codex lossless" "$(cat "$CXM/acct-01/config.toml")"
4458
+ [ -f "$CXM/mcp-servers.json" ] && t_ok "the codex pool got its own registry" || t_fail "codex registry" "missing"
4459
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CODEX_ACCOUNTS_DIR="$WORK/no-such-codex-pool" claude-accounts mcp add lonely -- echo one 2>&1)"
4460
+ rc=$?
4461
+ [ "$rc" = "0" ] && t_ok "no sibling pool: the mirror is silently skipped" || t_fail "no sibling" "rc=$rc: $out"
4462
+ case "$out" in *"no manifest"*|*"warning"*) t_fail "no sibling silence" "$out" ;; *) t_ok "no sibling pool: no warning" ;; esac
4463
+ # an EXPLICIT --provider codex with no codex pool is a loud error, not a silent no-op
4464
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CODEX_ACCOUNTS_DIR="$WORK/no-such-codex-pool" claude-accounts mcp add --provider codex q -- echo q 2>&1)"
4465
+ rc=$?
4466
+ [ "$rc" != "0" ] && check "an explicit --provider codex without a codex pool fails loudly" "no codex pool" "$out" \
4467
+ || t_fail "explicit provider" "rc=0: $out"
4468
+ # rc 3: the registry is saved, the healthy accounts are reconciled, the broken one is
4469
+ # reported, and the change STILL reaches the sibling pool (and would still sync)
4470
+ cp "$MPOOL/acct-02/.claude.json" "$WORK/mcp-acct-02.bak"
4471
+ printf '{not json' > "$MPOOL/acct-02/.claude.json"
4472
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CODEX_ACCOUNTS_DIR="$CXM" CODEX_MULTIACC_NO_SYNC=1 claude-accounts mcp add partial -- echo p 2>&1)"
4473
+ rc=$?
4474
+ [ "$rc" = "3" ] && t_ok "a corrupt account makes mcp add exit 3 (saved, partially applied)" || t_fail "partial rc" "rc=$rc: $out"
4475
+ check "the partial add says how to repair" "mcp apply" "$out"
4476
+ mcp_has acct-01 partial && t_ok "the healthy account was reconciled despite the corrupt one" || t_fail "partial healthy" "$out"
4477
+ grep -q '^\[mcp_servers.partial\]' "$CXM/acct-01/config.toml" \
4478
+ && t_ok "the partial add still reached the codex pool beside it" || t_fail "partial sibling" "$(cat "$CXM/acct-01/config.toml")"
4479
+ cp "$WORK/mcp-acct-02.bak" "$MPOOL/acct-02/.claude.json"
4480
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp apply 2>&1)"
4481
+ mcp_has acct-02 partial && t_ok "mcp apply repairs the account once its config is fixed" || t_fail "partial repair" "$out"
4482
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp add --provider mars z -- echo z 2>&1)"
4483
+ rc=$?
4484
+ [ "$rc" != "0" ] && check "an unknown --provider is refused" "must be claude, codex or both" "$out" \
4485
+ || t_fail "provider validation" "rc=0"
4486
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp bogus 2>&1)"
4487
+ rc=$?
4488
+ [ "$rc" != "0" ] && check "an unknown mcp subcommand is refused" "unknown mcp subcommand" "$out" \
4489
+ || t_fail "subcommand validation" "rc=0"
4490
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp --help 2>&1)"
4491
+ check "mcp --help prints usage" "claude-accounts mcp add" "$out"
4492
+ # seeding a NEW account applies the registry (import goes through seed_account_dir)
4493
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts import m3@test --id acct-03 --no-sync 2>&1)"
4494
+ mcp_has acct-03 reg1 && mcp_has acct-03 shared && t_ok "a newly seeded account gets every registered server" \
4495
+ || t_fail "seed applies registry" "$out $(cat "$MPOOL/acct-03/.claude.json" 2>/dev/null)"
4496
+
4151
4497
  # ---- 17b. verify authenticates the way the SHIM would ---------------------------------
4152
4498
  # A dead credential next to a live portable token: the shim runs that account with the
4153
4499
  # TOKEN, so verify must too — testing it with the dead credential would fail a healthy
@@ -4446,11 +4792,65 @@ export MKAUTH
4446
4792
  mk_cx_auth() { python3 "$MKAUTH" "$@"; }
4447
4793
  FUTURE_EXP=$((now + 864000))
4448
4794
 
4795
+ # A stand-in for the real `codex mcp add|remove`: appends / strips a
4796
+ # `[mcp_servers.NAME]` table in $CODEX_HOME/config.toml, where the real CLI keeps them.
4797
+ # A separate file because the fake codex below is an UNQUOTED heredoc.
4798
+ FAKE_CODEX_MCP="$WORK/fake_codex_mcp.py"
4799
+ cat > "$FAKE_CODEX_MCP" <<'EOF'
4800
+ import os, re, sys
4801
+ path, argv = sys.argv[1], sys.argv[2:]
4802
+ verb, rest = argv[1], argv[2:]
4803
+ name, cmd, envs = None, [], {}
4804
+ i = 0
4805
+ while i < len(rest):
4806
+ a = rest[i]
4807
+ if a == "--env":
4808
+ k, v = rest[i + 1].split("=", 1); envs[k] = v; i += 2; continue
4809
+ if a == "--":
4810
+ cmd = rest[i + 1:]; break
4811
+ if name is None:
4812
+ name = a
4813
+ i += 1
4814
+ src = open(path).read() if os.path.exists(path) else ""
4815
+ out, skip, found = [], False, False
4816
+ for line in src.splitlines(keepends=True):
4817
+ st = line.strip()
4818
+ if st.startswith("["):
4819
+ skip = bool(re.match(r"\[mcp_servers\." + re.escape(name) + r"(\.|\])", st))
4820
+ found = found or skip
4821
+ if not skip:
4822
+ out.append(line)
4823
+ src = "".join(out)
4824
+ def q(v):
4825
+ return '"' + v.replace("\\", "\\\\").replace('"', '\\"') + '"'
4826
+ if verb == "add":
4827
+ if src and not src.endswith("\n"):
4828
+ src += "\n"
4829
+ src += "\n[mcp_servers.%s]\ncommand = %s\nargs = [%s]\n" % (name, q(cmd[0]), ", ".join(q(a) for a in cmd[1:]))
4830
+ if envs:
4831
+ src += "env = { %s }\n" % ", ".join("%s = %s" % (k, q(v)) for k, v in envs.items())
4832
+ print("Added global MCP server '%s'." % name)
4833
+ elif not found:
4834
+ print("No MCP server named '%s' found." % name, file=sys.stderr); sys.exit(1)
4835
+ else:
4836
+ print("Removed global MCP server '%s'." % name)
4837
+ with open(path + ".tmp", "w") as f:
4838
+ f.write(src)
4839
+ os.replace(path + ".tmp", path)
4840
+ EOF
4841
+
4449
4842
  # Fake "real" codex: prints which CODEX_HOME it ran under; scriptable login +
4450
4843
  # failures via a control file. (Note: must not contain the hyphenated shim marker.)
4451
4844
  cat > "$FAKEBIN/codex" <<EOF
4452
4845
  #!/usr/bin/env bash
4453
4846
  # fake real codex for tests (not a shim)
4847
+ if [ "\${1:-}" = "mcp" ] && [ -n "\${CODEX_HOME:-}" ]; then
4848
+ python3 "$FAKE_CODEX_MCP" "\$CODEX_HOME/config.toml" "\$@"
4849
+ rc=\$?
4850
+ # FAKE_MCP_WRITE_THEN_FAIL=1: wrote the table, then died — nothing may be mirrored.
4851
+ [ "\$rc" = 0 ] && [ -n "\${FAKE_MCP_WRITE_THEN_FAIL:-}" ] && exit 1
4852
+ exit \$rc
4853
+ fi
4454
4854
  if [ "\${1:-}" = "login" ]; then
4455
4855
  shift
4456
4856
  # record the flags the CLI chose (device-auth default vs --browser opt-out)
@@ -6501,6 +6901,184 @@ else
6501
6901
  t_ok "codex npm-layer tests skipped (node not installed)"
6502
6902
  fi
6503
6903
 
6904
+ # ---- C19. MCP servers for every account (codex parity) ------------------------------
6905
+ # Same contract as 16e over config.toml: the registry is reconciled into the picked
6906
+ # account before exec (stamped, no python when nothing changed), a stock `codex mcp
6907
+ # add|remove` under the shim is mirrored into every account, and `codex-accounts mcp`
6908
+ # manages the registry — for this pool alone with --provider codex.
6909
+ CXP="$WORK/cx-mcp-pool"
6910
+ mkdir -p "$CXP/acct-01" "$CXP/acct-02" "$CXP/tmp"
6911
+ : > "$CXP/.limits-kick"
6912
+ cat > "$CXP/accounts.json" <<'EOF'
6913
+ {"version":1,"server":"none","threshold":90,
6914
+ "accounts":[
6915
+ {"id":"acct-01","email":"p1@cx","home":"mac","added_at":"2026-08-21T00:00:00Z"},
6916
+ {"id":"acct-02","email":"p2@cx","home":"mac","added_at":"2026-08-21T00:00:00Z"}]}
6917
+ EOF
6918
+ mk_cx_auth "$CXP/acct-01/auth.json" p1@cx "$FUTURE_EXP"
6919
+ mk_cx_auth "$CXP/acct-02/auth.json" p2@cx "$FUTURE_EXP"
6920
+ for i in 01 02; do
6921
+ printf 'model = "gpt-5"\n\n[mcp_servers.own-%s]\ncommand = "true"\nargs = []\n' "$i" > "$CXP/acct-$i/config.toml"
6922
+ done
6923
+ cx_has() { grep -q "^\[mcp_servers\.$2\]" "$CXP/$1/config.toml" 2>/dev/null; }
6924
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts mcp add --provider codex reg1 -- echo hi 2>&1)"
6925
+ rc=$?
6926
+ [ "$rc" = "0" ] && t_ok "codex: mcp add (codex only) exits 0" || t_fail "codex mcp add rc" "rc=$rc: $out"
6927
+ cx_has acct-01 reg1 && cx_has acct-02 reg1 && t_ok "codex: mcp add reconciles EVERY account" \
6928
+ || t_fail "codex mcp add reconcile" "$(cat "$CXP/acct-01/config.toml" "$CXP/acct-02/config.toml")"
6929
+ cx_has acct-01 own-01 && grep -q '^model = "gpt-5"' "$CXP/acct-01/config.toml" \
6930
+ && t_ok "codex: an account's own servers and settings survive the reconcile" \
6931
+ || t_fail "codex extras preserved" "$(cat "$CXP/acct-01/config.toml")"
6932
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts mcp list 2>&1)"
6933
+ check "codex: mcp list names the server" "reg1" "$out"
6934
+ # drift + launch: the picked account is reconciled before exec, and stamped
6935
+ python3 - "$CXP/acct-02/config.toml" <<'PY'
6936
+ import re, sys
6937
+ p = sys.argv[1]; src = open(p).read()
6938
+ out, skip = [], False
6939
+ for line in src.splitlines(keepends=True):
6940
+ if line.lstrip().startswith("["):
6941
+ skip = line.strip().startswith("[mcp_servers.reg1")
6942
+ if not skip: out.append(line)
6943
+ open(p, "w").write("".join(out))
6944
+ PY
6945
+ cx_has acct-02 reg1 && t_fail "codex drift fixture" "reg1 still present" || t_ok "codex drift fixture: acct-02 lost reg1"
6946
+ : > "$PYLOG"
6947
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_ACCOUNT=acct-02 CLAUDE_MULTIACC_PYTHON="$WORK/pylog.sh" codex 2>&1)"
6948
+ check "codex: launch runs on the pinned account" "CFG=acct-02" "$out"
6949
+ cx_has acct-02 reg1 && t_ok "codex: the shim reconciles the picked account before exec" \
6950
+ || t_fail "codex shim reconcile" "$(cat "$CXP/acct-02/config.toml")"
6951
+ [ -s "$CXP/acct-02/.mcp-applied" ] && t_ok "codex: the reconcile leaves a stamp" || t_fail "codex stamp" "no .mcp-applied"
6952
+ : > "$PYLOG"
6953
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_ACCOUNT=acct-02 CLAUDE_MULTIACC_PYTHON="$WORK/pylog.sh" codex 2>&1)"
6954
+ check "codex: second launch still runs" "CFG=acct-02" "$out"
6955
+ [ ! -s "$PYLOG" ] && t_ok "codex: an unchanged registry + config spends no python start-up" \
6956
+ || t_fail "codex stamp short-circuit" "python ran: $(cat "$PYLOG")"
6957
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_ACCOUNT=acct-02 CLAUDE_MULTIACC_PYTHON=/nonexistent/python3 codex 2>&1)"
6958
+ rc=$?
6959
+ [ "$rc" = "0" ] && check "codex: a missing python never breaks a launch" "CFG=acct-02" "$out" \
6960
+ || t_fail "codex fail-open python" "rc=$rc: $out"
6961
+ # kill switch
6962
+ python3 - "$CXP/acct-01/config.toml" <<'PY'
6963
+ import sys
6964
+ p = sys.argv[1]; src = open(p).read()
6965
+ out, skip = [], False
6966
+ for line in src.splitlines(keepends=True):
6967
+ if line.lstrip().startswith("["):
6968
+ skip = line.strip().startswith("[mcp_servers.reg1")
6969
+ if not skip: out.append(line)
6970
+ open(p, "w").write("".join(out))
6971
+ PY
6972
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_ACCOUNT=acct-01 CODEX_MULTIACC_MCP=0 codex 2>&1)"
6973
+ check "codex: kill switch: launch still runs" "CFG=acct-01" "$out"
6974
+ cx_has acct-01 reg1 && t_fail "codex kill switch" "CODEX_MULTIACC_MCP=0 still reconciled" \
6975
+ || t_ok "codex: CODEX_MULTIACC_MCP=0 leaves the account alone"
6976
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts mcp apply 2>&1)"
6977
+ cx_has acct-01 reg1 && t_ok "codex: mcp apply repairs the drifted account" || t_fail "codex mcp apply" "$out"
6978
+ # learn-from-write: a stock 'codex mcp add' under the shim reaches EVERY account
6979
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex mcp add x -- echo hi 2>&1)"
6980
+ rc=$?
6981
+ [ "$rc" = "0" ] && check "codex: stock 'codex mcp add' still answers like the real client" "Added global MCP server 'x'" "$out" \
6982
+ || t_fail "codex stock mcp add rc" "rc=$rc: $out"
6983
+ cx_has acct-01 x && cx_has acct-02 x && t_ok "codex: a stock 'codex mcp add' lands in EVERY account" \
6984
+ || t_fail "codex mirror add" "$(cat "$CXP/acct-01/config.toml" "$CXP/acct-02/config.toml")"
6985
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts mcp list --json 2>&1)"
6986
+ python3 -c 'import json,sys; d=json.loads(sys.argv[1]); s=d["mcpServers"]["x"]; sys.exit(0 if s.get("command")=="echo" and s.get("args")==["hi"] else 1)' "$out" \
6987
+ && t_ok "codex: the mirrored server is in the registry as a Claude-style block" || t_fail "codex mirror registry" "$out"
6988
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex mcp remove x 2>&1)"
6989
+ rc=$?
6990
+ [ "$rc" = "0" ] && t_ok "codex: stock 'codex mcp remove' exits 0" || t_fail "codex stock mcp remove rc" "rc=$rc: $out"
6991
+ cx_has acct-01 x || cx_has acct-02 x \
6992
+ && t_fail "codex mirror remove" "x survives in an account" || t_ok "codex: a stock 'codex mcp remove' drops the server from EVERY account"
6993
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts mcp list --json 2>&1)"
6994
+ python3 -c 'import json,sys; d=json.loads(sys.argv[1]); sys.exit(0 if ("x" in d.get("retiredUser",[]) or "x" in d.get("retired",[])) and "x" not in d.get("mcpServers",{}) else 1)' "$out" \
6995
+ && t_ok "codex: a removed server is retired in the registry" || t_fail "codex tombstone" "$out"
6996
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_ACCOUNT=acct-01 CODEX_MULTIACC_MCP=0 codex mcp add solo -- echo solo 2>&1)"
6997
+ cx_has acct-01 solo && ! cx_has acct-02 solo && t_ok "codex: CODEX_MULTIACC_MCP=0: a stock add stays in one account" \
6998
+ || t_fail "codex kill switch mirror" "$out"
6999
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_ACCOUNT=acct-02 codex mcp remove never-there 2>&1)"
7000
+ rc=$?
7001
+ [ "$rc" != "0" ] && t_ok "codex: a failed 'codex mcp remove' keeps its exit status" || t_fail "codex mcp remove rc" "rc=0: $out"
7002
+ # a client that wrote its table and then died: nothing is mirrored, the failure passes
7003
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_ACCOUNT=acct-02 FAKE_MCP_WRITE_THEN_FAIL=1 codex mcp add wtf -- echo w 2>&1)"
7004
+ rc=$?
7005
+ [ "$rc" != "0" ] && t_ok "codex: a client that writes and then fails still fails the shim run" || t_fail "codex write-then-fail rc" "rc=0: $out"
7006
+ cx_has acct-02 wtf && ! cx_has acct-01 wtf && t_ok "codex: the failed add stays in the one account that wrote it" \
7007
+ || t_fail "codex write-then-fail leak" "$(cat "$CXP/acct-01/config.toml")"
7008
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts mcp list --json 2>&1)"
7009
+ python3 -c 'import json,sys; d=json.loads(sys.argv[1]); sys.exit(0 if "wtf" not in d.get("mcpServers",{}) and "wtf" not in d.get("retired",[]) and "wtf" not in d.get("retiredUser",[]) else 1)' "$out" \
7010
+ && t_ok "codex: a failed stock add is never mirrored into the registry" || t_fail "codex failed mirror add" "$out"
7011
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_ACCOUNT=acct-02 FAKE_MCP_WRITE_THEN_FAIL=1 codex mcp remove wtf 2>&1)"
7012
+ rc=$?
7013
+ [ "$rc" != "0" ] && t_ok "codex: a client that removes and then fails still fails the shim run" || t_fail "codex write-then-fail remove rc" "rc=0: $out"
7014
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts mcp list --json 2>&1)"
7015
+ python3 -c 'import json,sys; d=json.loads(sys.argv[1]); sys.exit(0 if "wtf" not in d.get("retired",[]) and "wtf" not in d.get("retiredUser",[]) else 1)' "$out" \
7016
+ && t_ok "codex: a failed stock remove leaves no tombstone" || t_fail "codex failed mirror remove" "$out"
7017
+ # NESTED session: CODEX_HOME=<acct> + CODEX_SHIM_ACTIVE=1 is what every pooled codex
7018
+ # session's children inherit — the passthrough must mirror the write all the same
7019
+ : > "$PYLOG"
7020
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_HOME="$CXP/acct-01" CODEX_SHIM_ACTIVE=1 CLAUDE_MULTIACC_PYTHON="$WORK/pylog.sh" codex mcp add nested -- echo n 2>&1)"
7021
+ rc=$?
7022
+ [ "$rc" = "0" ] && t_ok "codex: a nested (in-session) 'codex mcp add' exits 0" || t_fail "codex nested add rc" "rc=$rc: $out"
7023
+ cx_has acct-01 nested && cx_has acct-02 nested && t_ok "codex: a nested add is mirrored into EVERY account" \
7024
+ || t_fail "codex nested mirror" "$out $(cat "$CXP/acct-02/config.toml")"
7025
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_HOME="$CXP/acct-01" CODEX_SHIM_ACTIVE=1 codex mcp remove nested 2>&1)"
7026
+ rc=$?
7027
+ [ "$rc" = "0" ] && t_ok "codex: a nested 'codex mcp remove' exits 0" || t_fail "codex nested remove rc" "rc=$rc: $out"
7028
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts mcp list --json 2>&1)"
7029
+ python3 -c 'import json,sys; d=json.loads(sys.argv[1]); sys.exit(0 if ("nested" in d.get("retiredUser",[]) or "nested" in d.get("retired",[])) and "nested" not in d.get("mcpServers",{}) else 1)' "$out" \
7030
+ && t_ok "codex: a nested remove leaves a tombstone" || t_fail "codex nested tombstone" "$out"
7031
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_ACCOUNT=acct-01 codex 2>&1)"
7032
+ check "codex: the next launch on that account still runs" "CFG=acct-01" "$out"
7033
+ cx_has acct-01 nested && t_fail "codex nested remove undone" "the next launch re-added nested" \
7034
+ || t_ok "codex: a nested remove is not undone by the next launch"
7035
+ mkdir -p "$WORK/cx-outside"
7036
+ : > "$PYLOG"
7037
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_HOME="$WORK/cx-outside" CLAUDE_MULTIACC_PYTHON="$WORK/pylog.sh" codex mcp add outside -- echo o 2>&1)"
7038
+ [ "$?" = "0" ] && [ ! -s "$PYLOG" ] && ! cx_has acct-01 outside \
7039
+ && t_ok "codex: a CODEX_HOME outside the pool is untouched passthrough (no python, no mirror)" \
7040
+ || t_fail "codex outside passthrough" "$out; pylog: $(cat "$PYLOG")"
7041
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts mcp add --provider codex hsvc -- node s.js -h 127.0.0.1 2>&1)"
7042
+ rc=$?
7043
+ case "$out" in *USAGE*) t_fail "codex help scan" "-h after -- printed usage" ;; *) [ "$rc" = "0" ] && t_ok "codex: -h after -- is the server's own flag, not --help" || t_fail "codex help scan rc" "rc=$rc: $out" ;; esac
7044
+ grep -q '^args = \["s.js", "-h", "127.0.0.1"\]' "$CXP/acct-01/config.toml" \
7045
+ && t_ok "codex: the server keeps its -h argument" || t_fail "codex help scan args" "$(cat "$CXP/acct-01/config.toml")"
7046
+ # --provider both from the codex side reaches the claude pool beside it
7047
+ CLM="$WORK/mcp-claude-pool"
7048
+ mkdir -p "$CLM/acct-01" "$CLM/tmp"
7049
+ cat > "$CLM/accounts.json" <<'EOF'
7050
+ {"version":1,"server":"none","threshold":90,
7051
+ "accounts":[{"id":"acct-01","email":"cl1@test","home":"mac","added_at":"2026-07-13T00:00:00Z"}]}
7052
+ EOF
7053
+ printf '{"mcpServers":{}}\n' > "$CLM/acct-01/.claude.json"
7054
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CLAUDE_ACCOUNTS_ROOT="$CLM" codex-accounts mcp add shared -- echo both 2>&1)"
7055
+ rc=$?
7056
+ [ "$rc" = "0" ] && t_ok "codex: mcp add --provider both (default) exits 0" || t_fail "codex mcp add both rc" "rc=$rc: $out"
7057
+ cx_has acct-01 shared && t_ok "codex: --provider both: the codex pool has the server" || t_fail "codex both codex" "$out"
7058
+ python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); sys.exit(0 if "shared" in d.get("mcpServers",{}) else 1)' "$CLM/acct-01/.claude.json" \
7059
+ && t_ok "codex: --provider both: the claude pool beside it has the server" \
7060
+ || t_fail "codex both claude" "$(cat "$CLM/acct-01/.claude.json")"
7061
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts mcp --help 2>&1)"
7062
+ check "codex: mcp --help prints usage" "codex-accounts mcp add" "$out"
7063
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CLAUDE_ACCOUNTS_ROOT="$WORK/no-such-claude-pool" codex-accounts mcp add --provider claude q -- echo q 2>&1)"
7064
+ rc=$?
7065
+ [ "$rc" != "0" ] && check "codex: an explicit --provider claude without a claude pool fails loudly" "no claude pool" "$out" \
7066
+ || t_fail "codex explicit provider" "rc=0: $out"
7067
+ cp "$CXP/acct-02/config.toml" "$WORK/cx-acct-02.bak"
7068
+ printf '[mcp_servers.broken\ncommand = "x"\n' > "$CXP/acct-02/config.toml"
7069
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CLAUDE_ACCOUNTS_ROOT="$CLM" codex-accounts mcp add partial -- echo p 2>&1)"
7070
+ rc=$?
7071
+ [ "$rc" = "3" ] && t_ok "codex: a corrupt config.toml makes mcp add exit 3 (saved, partially applied)" || t_fail "codex partial rc" "rc=$rc: $out"
7072
+ cx_has acct-01 partial && t_ok "codex: the healthy account was reconciled despite the corrupt one" || t_fail "codex partial healthy" "$out"
7073
+ python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); sys.exit(0 if "partial" in d.get("mcpServers",{}) else 1)' "$CLM/acct-01/.claude.json" \
7074
+ && t_ok "codex: the partial add still reached the claude pool beside it" || t_fail "codex partial sibling" "$(cat "$CLM/acct-01/.claude.json")"
7075
+ cp "$WORK/cx-acct-02.bak" "$CXP/acct-02/config.toml"
7076
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts mcp apply 2>&1)"
7077
+ cx_has acct-02 partial && t_ok "codex: mcp apply repairs the account once its config is fixed" || t_fail "codex partial repair" "$out"
7078
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts import p3@cx --id acct-03 --no-sync 2>&1)"
7079
+ cx_has acct-03 reg1 && cx_has acct-03 shared && t_ok "codex: a newly seeded account gets every registered server" \
7080
+ || t_fail "codex seed applies registry" "$out $(cat "$CXP/acct-03/config.toml" 2>/dev/null)"
7081
+
6504
7082
  # ---- 20. panel pools: --json, credential transfer, instance roots, local sync -------
6505
7083
  # Everything here runs against pools under $WORK/panel, addressed with the NEW
6506
7084
  # CLAUDE_ACCOUNTS_ROOT/CODEX_ACCOUNTS_ROOT env — while CLAUDE_ACCOUNTS_DIR (the legacy
@@ -7303,6 +7881,16 @@ else
7303
7881
  fi
7304
7882
  # ...and its claude twin: the cedar_ember status rides on the usage response, the claim
7305
7883
  # is idempotent across the fleet, and a confirmed reset lifts even a weekly client park.
7884
+ # The claude half of the MCP registry must run on STOCK macOS python (3.9): the shim
7885
+ # reconciles with whatever `python3` is on PATH, and a runner Mac's is /usr/bin/python3.
7886
+ # Skipped silently where that interpreter is absent or already 3.11+ (CI is ubuntu).
7887
+ if [ -x /usr/bin/python3 ] && /usr/bin/python3 -c 'import sys; sys.exit(0 if sys.version_info < (3, 11) else 1)' 2>/dev/null; then
7888
+ if /usr/bin/python3 "$REPO_DIR/tests/test_mcp_registry.py" >/dev/null 2>&1; then
7889
+ t_ok "MCP registry unit suite on stock python $(/usr/bin/python3 -c 'import sys;print("%d.%d"%sys.version_info[:2])')"
7890
+ else
7891
+ t_fail "MCP registry unit suite on stock python" "/usr/bin/python3 tests/test_mcp_registry.py failed"
7892
+ fi
7893
+ fi
7306
7894
  if CLAUDE_MULTIACC_AUTO_RESET=1 python3 -m unittest discover -s "$REPO_DIR/tests" -p 'test_claude_reset*.py'; then
7307
7895
  t_ok "claude: automatic limit-reset integration suite"
7308
7896
  else