claude-multiacc 2.0.38 → 2.0.40

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
@@ -3852,15 +3908,20 @@ EOF
3852
3908
  SSHFAKE="$WORK/sshfake"
3853
3909
  SSHLOG="$WORK/sshfake.log"
3854
3910
  mkdir -p "$SSHFAKE"
3911
+ # Both fakes DRAIN stdin like the real tools do: ssh forwards it to the remote
3912
+ # command and rsync's transport reads it. A fake that left stdin alone hid the
3913
+ # bug where the peer loop fed them its own peer list.
3855
3914
  cat > "$SSHFAKE/ssh" <<'EOF'
3856
3915
  #!/usr/bin/env bash
3857
3916
  printf 'ssh %s\n' "$*" >> "${SSHLOG:?}"
3917
+ [ -t 0 ] || cat >/dev/null
3858
3918
  case "$*" in *"${SSH_FAIL_HOST:-@@none@@}"*) exit 1 ;; esac
3859
3919
  exit 0
3860
3920
  EOF
3861
3921
  cat > "$SSHFAKE/rsync" <<'EOF'
3862
3922
  #!/usr/bin/env bash
3863
3923
  printf 'rsync %s\n' "$*" >> "${SSHLOG:?}"
3924
+ [ -t 0 ] || cat >/dev/null
3864
3925
  exit 0
3865
3926
  EOF
3866
3927
  chmod +x "$SSHFAKE/ssh" "$SSHFAKE/rsync"
@@ -3910,14 +3971,22 @@ EOF
3910
3971
  python3 - "$ACC/accounts.json" <<'EOF'
3911
3972
  import json, os, sys
3912
3973
  d = json.load(open(sys.argv[1]))
3913
- d['peers'] = [{'target': 'gas@peer1', 'root': '/Users/gas/.claude-accounts', 'repo': '/Users/gas/claude-multiacc'}]
3974
+ d['peers'] = [{'target': 'gas@peer1', 'root': '/Users/gas/.claude-accounts', 'repo': '/Users/gas/claude-multiacc'},
3975
+ {'target': 'gas@peer2', 'root': '/Users/gas/.claude-accounts', 'repo': '/Users/gas/claude-multiacc'},
3976
+ {'target': 'gas@peer3', 'root': '/Users/gas/.claude-accounts', 'repo': '/Users/gas/claude-multiacc'}]
3914
3977
  json.dump(d, open(sys.argv[1] + '.tmp', 'w'), indent=2); os.replace(sys.argv[1] + '.tmp', sys.argv[1])
3915
3978
  EOF
3916
3979
  : > "$SSHLOG"
3917
3980
  out="$(PATH="$SSHFAKE:$PATH" CLAUDE_MULTIACC_NO_SYNC=0 claude-accounts sync 2>&1)"
3918
3981
  rc=$?
3919
3982
  [ "$rc" = "0" ] && t_ok "multi-target sync succeeds" || t_fail "multi-target sync" "rc=$rc: $out"
3920
- check "multi-target sync reports the peer" "+ 1 peer(s)" "$out"
3983
+ check "multi-target sync reports every peer" "+ 3 peer(s)" "$out"
3984
+ # EVERY peer is pushed, not just the first: the ssh/rsync inside the loop must not
3985
+ # be allowed to eat the peer list off stdin (mini-3..8 got nothing for 18 days).
3986
+ grep -q "rsync.*gas@peer2:/Users/gas/.claude-accounts/accounts.json" "$SSHLOG" \
3987
+ && grep -q "rsync.*gas@peer3:/Users/gas/.claude-accounts/accounts.json" "$SSHLOG" \
3988
+ && t_ok "the second and third peers are pushed too (stdin not eaten by the first)" \
3989
+ || t_fail "later peers skipped" "$(grep -c 'accounts.json' "$SSHLOG") manifest pushes: $(grep accounts.json "$SSHLOG" | tr '\n' '|')"
3921
3990
  grep -q "rsync.*root@203.0.113.1:/root/.claude-accounts/accounts.json" "$SSHLOG" \
3922
3991
  && grep -q "rsync.*gas@peer1:/Users/gas/.claude-accounts/accounts.json" "$SSHLOG" \
3923
3992
  && t_ok "manifest pushed to BOTH targets" \
@@ -3928,9 +3997,32 @@ EOF
3928
3997
  && t_ok "primary target pushed before the peer" \
3929
3998
  || t_fail "target order" "primary=$first_primary peer=$first_peer"
3930
3999
  n_remov="$(grep -c "for dd in acct-" "$SSHLOG")"
3931
- [ "$n_remov" = "2" ] && t_ok "removal propagation ran on each target" \
3932
- || t_fail "per-target removal" "expected 2 removal sweeps, saw $n_remov"
4000
+ [ "$n_remov" = "4" ] && t_ok "removal propagation ran on each target" \
4001
+ || t_fail "per-target removal" "expected 4 removal sweeps, saw $n_remov"
3933
4002
  grep -q "post-sync" "$SSHLOG" && t_ok "post-sync hooks invoked" || t_fail "post-sync hooks" "none recorded"
4003
+ # The MCP registry rides with the manifest to EVERY target; the machine-local overlay
4004
+ # (a runner daemon's view of THIS Mac) never leaves. Without a registry file nothing
4005
+ # about it is pushed, so a target's own registry is never blanked.
4006
+ grep -q "mcp-servers" "$SSHLOG" \
4007
+ && t_fail "registry push without a registry" "pushed a registry that does not exist: $(grep mcp-servers "$SSHLOG")" \
4008
+ || t_ok "no registry file => no registry push"
4009
+ printf '{"version":1,"mcpServers":{"regsync":{"type":"stdio","command":"echo","args":["hi"],"env":{}}},"retired":[],"projects":{}}\n' \
4010
+ > "$ACC/mcp-servers.json"
4011
+ printf '{"version":1,"owners":{"panel":{"mcpServers":{"local-only":{"type":"stdio","command":"true","args":[],"env":{}}},"retired":[]}}}\n' \
4012
+ > "$ACC/mcp-servers.local.json"
4013
+ : > "$SSHLOG"
4014
+ out="$(PATH="$SSHFAKE:$PATH" CLAUDE_MULTIACC_NO_SYNC=0 claude-accounts sync 2>&1)"
4015
+ rc=$?
4016
+ [ "$rc" = "0" ] && t_ok "sync with a registry succeeds" || t_fail "registry sync" "rc=$rc: $out"
4017
+ grep -q "rsync.*mcp-servers.json root@203.0.113.1:/root/.claude-accounts/mcp-servers.json" "$SSHLOG" \
4018
+ && grep -q "rsync.*mcp-servers.json gas@peer1:/Users/gas/.claude-accounts/mcp-servers.json" "$SSHLOG" \
4019
+ && t_ok "the MCP registry is pushed to BOTH targets" \
4020
+ || t_fail "registry push" "$(grep mcp-servers "$SSHLOG")"
4021
+ grep -q "mcp-servers.local.json" "$SSHLOG" \
4022
+ && t_fail "overlay push" "the machine-local overlay was pushed: $(grep local.json "$SSHLOG")" \
4023
+ || t_ok "the machine-local overlay is never pushed"
4024
+ rm -f "$ACC/mcp-servers.json" "$ACC/mcp-servers.local.json"
4025
+ for _d in "$ACC"/acct-*; do rm -f "$_d/.mcp-applied"; done
3934
4026
  # a primary failure aborts the WHOLE sync: the peer is never contacted
3935
4027
  : > "$SSHLOG"
3936
4028
  out="$(PATH="$SSHFAKE:$PATH" SSH_FAIL_HOST="root@203.0.113.1" CLAUDE_MULTIACC_NO_SYNC=0 claude-accounts sync 2>&1)"
@@ -4073,6 +4165,26 @@ dp_drain
4073
4165
  && t_ok "only an exact 'replica' value suppresses the telemetry push" \
4074
4166
  || t_fail "replica anchor (distribute)" "'not-replica' suppressed the push"
4075
4167
  rm -f "$DPOOL/sync-role"
4168
+ # The MCP registry rides along with the telemetry, so a server the shim mirrored from a
4169
+ # stock 'claude mcp add' reaches every peer within one limits cadence. The overlay does not.
4170
+ printf '{"version":1,"mcpServers":{"ride":{"type":"stdio","command":"echo","args":["hi"],"env":{}}},"retired":[],"projects":{}}\n' \
4171
+ > "$DPOOL/mcp-servers.json"
4172
+ printf '{"version":1,"owners":{"panel":{"mcpServers":{},"retired":[]}}}\n' > "$DPOOL/mcp-servers.local.json"
4173
+ : > "$RSLOG"
4174
+ PATH="$RSFAKE:$PATH" CLAUDE_ACCOUNTS_ROOT="$DPOOL" \
4175
+ CLAUDE_MULTIACC_USAGE_URL="file://$WORK/usage-low.json" claude-accounts limits --force --quiet
4176
+ waited=0
4177
+ while [ "$waited" -lt 50 ] && [ "$(grep -c '^RSYNC ' "$RSLOG" 2>/dev/null)" -lt 3 ]; do
4178
+ sleep 0.2; waited=$((waited + 1))
4179
+ done
4180
+ dp_drain
4181
+ [ "$(grep -c '^FILE mcp-servers.json$' "$RSLOG")" = "3" ] \
4182
+ && t_ok "the MCP registry rides along with the telemetry push to all three targets" \
4183
+ || 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' '|')"
4184
+ grep -q '^FILE mcp-servers.local.json$' "$RSLOG" \
4185
+ && t_fail "overlay in limits push" "the machine-local overlay was pushed" \
4186
+ || t_ok "the machine-local overlay never rides along"
4187
+ rm -f "$DPOOL/mcp-servers.json" "$DPOOL/mcp-servers.local.json"
4076
4188
 
4077
4189
  # ---- 16d-lock. a distribute lock is honored while it is alive, broken once it is not -
4078
4190
  # The push serializes on a lock DIRECTORY, and a detached push that is killed (logout,
@@ -4148,6 +4260,253 @@ case "$out" in
4148
4260
  esac
4149
4261
  rm -f "$WORK/ctl-brokencli"
4150
4262
 
4263
+ # ---- 16e. MCP servers for every account ---------------------------------------------
4264
+ # Claude Code keeps MCP servers in the CONFIG DIR, so under the pool a stock `claude mcp
4265
+ # add` used to land in ONE random account — appinspire was connected in some sessions
4266
+ # and not in others (2026-09-22). The pool's registry (mcp-servers.json) is reconciled
4267
+ # into every account at seed time and by the shim right before exec, and a stock add
4268
+ # under the shim is mirrored into the registry. Own pool root, so nothing here leaks
4269
+ # into the sections above or below.
4270
+ MPOOL="$WORK/mcp-pool"
4271
+ mkdir -p "$MPOOL/acct-01" "$MPOOL/acct-02" "$MPOOL/tmp"
4272
+ : > "$MPOOL/.limits-kick"
4273
+ cat > "$MPOOL/accounts.json" <<'EOF'
4274
+ {"version":1,"server":"none","threshold":90,
4275
+ "accounts":[
4276
+ {"id":"acct-01","email":"m1@test","home":"mac","added_at":"2026-07-13T00:00:00Z"},
4277
+ {"id":"acct-02","email":"m2@test","home":"mac","added_at":"2026-07-13T00:00:00Z"}]}
4278
+ EOF
4279
+ for i in 01 02; do
4280
+ printf '{"claudeAiOauth":{"accessToken":"sk-ant-oat01-mcp%s","refreshToken":"r","expiresAt":9999999999999,"refreshTokenExpiresAt":9999999999999}}' \
4281
+ "$i" > "$MPOOL/acct-$i/.credentials.json"
4282
+ printf '{"mcpServers":{"own-%s":{"type":"stdio","command":"true","args":[],"env":{}}},"numStartups":1}\n' "$i" \
4283
+ > "$MPOOL/acct-$i/.claude.json"
4284
+ done
4285
+ # A python that LOGS every invocation, then runs the real python3: proves whether the
4286
+ # shim spent a python start-up on a launch, which the stamp exists to avoid.
4287
+ PYLOG="$WORK/mcp-pylog"
4288
+ cat > "$WORK/pylog.sh" <<EOF
4289
+ #!/usr/bin/env bash
4290
+ printf 'PY %s\n' "\$*" >> "$PYLOG"
4291
+ exec python3 "\$@"
4292
+ EOF
4293
+ chmod +x "$WORK/pylog.sh"
4294
+ mcp_has() { # mcp_has <acct> <name> [<project>] -> 0 when the account carries the server
4295
+ python3 - "$MPOOL/$1/.claude.json" "$2" "${3:-}" <<'PY'
4296
+ import json, sys
4297
+ doc = json.load(open(sys.argv[1]))
4298
+ name, project = sys.argv[2], sys.argv[3]
4299
+ if project:
4300
+ sys.exit(0 if name in (((doc.get("projects") or {}).get(project) or {}).get("mcpServers") or {}) else 1)
4301
+ sys.exit(0 if name in (doc.get("mcpServers") or {}) else 1)
4302
+ PY
4303
+ }
4304
+ # (iv) the CLI: add for THIS provider only, list, remove (retires), apply repairs drift
4305
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp add --provider claude reg1 -e K=v -- echo hi 2>&1)"
4306
+ rc=$?
4307
+ [ "$rc" = "0" ] && t_ok "mcp add (claude only) exits 0" || t_fail "mcp add rc" "rc=$rc: $out"
4308
+ [ -f "$MPOOL/mcp-servers.json" ] && t_ok "mcp add writes the pool registry" || t_fail "registry file" "missing"
4309
+ mcp_has acct-01 reg1 && mcp_has acct-02 reg1 && t_ok "mcp add reconciles EVERY account" \
4310
+ || t_fail "mcp add reconcile" "$(cat "$MPOOL/acct-01/.claude.json" "$MPOOL/acct-02/.claude.json")"
4311
+ mcp_has acct-01 own-01 && mcp_has acct-02 own-02 && t_ok "an account's own extra servers survive the reconcile" \
4312
+ || t_fail "extras preserved" "$(cat "$MPOOL/acct-01/.claude.json")"
4313
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp list 2>&1)"
4314
+ check "mcp list names the server" "reg1" "$out"
4315
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp list --json 2>&1)"
4316
+ python3 -c 'import json,sys; d=json.loads(sys.argv[1]); sys.exit(0 if "reg1" in d.get("mcpServers",{}) else 1)' "$out" \
4317
+ && t_ok "mcp list --json carries mcpServers" || t_fail "mcp list --json" "$out"
4318
+ # drift: the client rewrote the file without the server (its in-memory state was older)
4319
+ python3 - "$MPOOL/acct-02/.claude.json" <<'PY'
4320
+ import json, os, sys
4321
+ p = sys.argv[1]; d = json.load(open(p)); d["mcpServers"].pop("reg1", None)
4322
+ json.dump(d, open(p + ".tmp", "w")); os.replace(p + ".tmp", p)
4323
+ PY
4324
+ mcp_has acct-02 reg1 && t_fail "drift fixture" "reg1 still present" || t_ok "drift fixture: acct-02 lost reg1"
4325
+ # (i) a launch reconciles the picked account before exec, and stamps it
4326
+ : > "$PYLOG"
4327
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_ACCOUNT=acct-02 CLAUDE_MULTIACC_PYTHON="$WORK/pylog.sh" claude 2>&1)"
4328
+ check "launch runs on the pinned account" "CFG=acct-02" "$out"
4329
+ mcp_has acct-02 reg1 && t_ok "the shim reconciles the picked account before exec" \
4330
+ || t_fail "shim reconcile" "$(cat "$MPOOL/acct-02/.claude.json")"
4331
+ [ -s "$MPOOL/acct-02/.mcp-applied" ] && t_ok "the reconcile leaves a stamp" || t_fail "stamp" "no .mcp-applied"
4332
+ grep -q "apply" "$PYLOG" && t_ok "python ran once for the drifted account" || t_fail "python ran" "$(cat "$PYLOG")"
4333
+ # (ii) the stamp short-circuits: nothing changed => no python at all; python gone => still launches
4334
+ : > "$PYLOG"
4335
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_ACCOUNT=acct-02 CLAUDE_MULTIACC_PYTHON="$WORK/pylog.sh" claude 2>&1)"
4336
+ check "second launch still runs" "CFG=acct-02" "$out"
4337
+ [ ! -s "$PYLOG" ] && t_ok "an unchanged registry + config spends no python start-up" \
4338
+ || t_fail "stamp short-circuit" "python ran: $(cat "$PYLOG")"
4339
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_ACCOUNT=acct-02 CLAUDE_MULTIACC_PYTHON=/nonexistent/python3 claude 2>&1)"
4340
+ rc=$?
4341
+ [ "$rc" = "0" ] && check "a missing python never breaks a launch" "CFG=acct-02" "$out" \
4342
+ || t_fail "fail-open python" "rc=$rc: $out"
4343
+ touch "$MPOOL/mcp-servers.json"
4344
+ sleep 1; touch "$MPOOL/mcp-servers.json"
4345
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_ACCOUNT=acct-02 CLAUDE_MULTIACC_PYTHON=/nonexistent/python3 claude 2>&1)"
4346
+ rc=$?
4347
+ [ "$rc" = "0" ] && check "a changed registry with no python still launches (fail-open)" "CFG=acct-02" "$out" \
4348
+ || t_fail "fail-open changed registry" "rc=$rc: $out"
4349
+ : > "$PYLOG"
4350
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_ACCOUNT=acct-02 CLAUDE_MULTIACC_PYTHON="$WORK/pylog.sh" claude 2>&1)"
4351
+ grep -q "apply" "$PYLOG" && t_ok "a touched registry re-runs the reconcile" || t_fail "registry mtime" "no python run"
4352
+ # (vi) the kill switch: no reconcile at launch
4353
+ python3 - "$MPOOL/acct-01/.claude.json" <<'PY'
4354
+ import json, os, sys
4355
+ p = sys.argv[1]; d = json.load(open(p)); d["mcpServers"].pop("reg1", None)
4356
+ json.dump(d, open(p + ".tmp", "w")); os.replace(p + ".tmp", p)
4357
+ PY
4358
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_ACCOUNT=acct-01 CLAUDE_MULTIACC_MCP=0 claude 2>&1)"
4359
+ check "kill switch: launch still runs" "CFG=acct-01" "$out"
4360
+ mcp_has acct-01 reg1 && t_fail "kill switch" "CLAUDE_MULTIACC_MCP=0 still reconciled" \
4361
+ || t_ok "CLAUDE_MULTIACC_MCP=0 leaves the account alone"
4362
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp apply 2>&1)"
4363
+ mcp_has acct-01 reg1 && t_ok "mcp apply repairs the drifted account" || t_fail "mcp apply" "$out"
4364
+ # (iii) learn-from-write: a stock 'claude mcp add' under the shim reaches EVERY account
4365
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude mcp add -s user x -- echo hi 2>&1)"
4366
+ rc=$?
4367
+ [ "$rc" = "0" ] && check "stock 'claude mcp add' still answers like the real client" "Added stdio MCP server x" "$out" \
4368
+ || t_fail "stock mcp add rc" "rc=$rc: $out"
4369
+ mcp_has acct-01 x && mcp_has acct-02 x && t_ok "a stock 'claude mcp add -s user' lands in EVERY account" \
4370
+ || t_fail "mirror add" "$(cat "$MPOOL/acct-01/.claude.json" "$MPOOL/acct-02/.claude.json")"
4371
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp list --json 2>&1)"
4372
+ 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" \
4373
+ && t_ok "the mirrored server is in the registry with the client's own block" || t_fail "mirror registry" "$out"
4374
+ mkdir -p "$WORK/mcp-proj"
4375
+ # the client keys the project by its normalized cwd, so ask the shell for that spelling
4376
+ MPROJ="$(cd "$WORK/mcp-proj" && pwd)"
4377
+ out="$(cd "$MPROJ" && CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude mcp add y -- echo yo 2>&1)"
4378
+ mcp_has acct-01 y "$MPROJ" && mcp_has acct-02 y "$MPROJ" \
4379
+ && t_ok "a stock project-local add is mirrored for that project in EVERY account" \
4380
+ || t_fail "mirror local add" "$out $(cat "$MPOOL/acct-01/.claude.json")"
4381
+ mcp_has acct-01 y && t_fail "local scope leak" "y landed in user scope" || t_ok "a project-local add stays project-local"
4382
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude mcp remove -s user x 2>&1)"
4383
+ rc=$?
4384
+ [ "$rc" = "0" ] && t_ok "stock 'claude mcp remove' exits 0" || t_fail "stock mcp remove rc" "rc=$rc: $out"
4385
+ mcp_has acct-01 x || mcp_has acct-02 x \
4386
+ && t_fail "mirror remove" "x survives in an account" || t_ok "a stock 'claude mcp remove' drops the server from EVERY account"
4387
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp list --json 2>&1)"
4388
+ 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" \
4389
+ && t_ok "a learned user-scope remove is a USER-SCOPE tombstone (retiredUser) in the registry" || t_fail "tombstone" "$out"
4390
+ # the kill switch also disables the mirror: the stock add lands in one account only
4391
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_ACCOUNT=acct-01 CLAUDE_MULTIACC_MCP=0 claude mcp add -s user solo -- echo solo 2>&1)"
4392
+ mcp_has acct-01 solo && ! mcp_has acct-02 solo && t_ok "CLAUDE_MULTIACC_MCP=0: a stock add stays in one account" \
4393
+ || t_fail "kill switch mirror" "$out"
4394
+ # a failing real command is never mirrored, and its exit status passes through
4395
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_ACCOUNT=acct-02 claude mcp remove -s user never-there 2>&1)"
4396
+ rc=$?
4397
+ [ "$rc" != "0" ] && t_ok "a failed 'claude mcp remove' keeps its exit status" || t_fail "mcp remove passthrough rc" "rc=0: $out"
4398
+ # ...even when the client wrote its file BEFORE failing: the write stays in that one
4399
+ # account, the registry learns nothing, and the other account is untouched
4400
+ 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)"
4401
+ rc=$?
4402
+ [ "$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"
4403
+ mcp_has acct-02 wtf && ! mcp_has acct-01 wtf && t_ok "the failed add stays in the one account that wrote it" \
4404
+ || t_fail "write-then-fail leak" "$(cat "$MPOOL/acct-01/.claude.json")"
4405
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp list --json 2>&1)"
4406
+ 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" \
4407
+ && t_ok "a failed stock add is never mirrored into the registry" || t_fail "failed mirror add" "$out"
4408
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_ACCOUNT=acct-02 FAKE_MCP_WRITE_THEN_FAIL=1 claude mcp remove -s user wtf 2>&1)"
4409
+ rc=$?
4410
+ [ "$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"
4411
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp list --json 2>&1)"
4412
+ 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" \
4413
+ && t_ok "a failed stock remove leaves no tombstone" || t_fail "failed mirror remove" "$out"
4414
+ # NESTED session: a pooled session exports CLAUDE_CONFIG_DIR=<acct> + CLAUDE_SHIM_ACTIVE=1
4415
+ # and its children inherit both, so an agent's `claude mcp add` reaches the passthrough
4416
+ # with the account already chosen — it must be mirrored exactly like a top-level one
4417
+ : > "$PYLOG"
4418
+ 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)"
4419
+ rc=$?
4420
+ [ "$rc" = "0" ] && t_ok "a nested (in-session) 'claude mcp add' exits 0" || t_fail "nested add rc" "rc=$rc: $out"
4421
+ mcp_has acct-01 nested && mcp_has acct-02 nested && t_ok "a nested add is mirrored into EVERY account" \
4422
+ || t_fail "nested mirror" "$out $(cat "$MPOOL/acct-02/.claude.json")"
4423
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp list --json 2>&1)"
4424
+ python3 -c 'import json,sys; d=json.loads(sys.argv[1]); sys.exit(0 if "nested" in d.get("mcpServers",{}) else 1)' "$out" \
4425
+ && t_ok "a nested add reaches the registry" || t_fail "nested registry" "$out"
4426
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_CONFIG_DIR="$MPOOL/acct-01" CLAUDE_SHIM_ACTIVE=1 claude mcp remove -s user nested 2>&1)"
4427
+ rc=$?
4428
+ [ "$rc" = "0" ] && t_ok "a nested 'claude mcp remove' exits 0" || t_fail "nested remove rc" "rc=$rc: $out"
4429
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp list --json 2>&1)"
4430
+ 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" \
4431
+ && t_ok "a nested remove leaves a user-scope tombstone" || t_fail "nested tombstone" "$out"
4432
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CLAUDE_ACCOUNT=acct-01 claude 2>&1)"
4433
+ check "the next launch on that account still runs" "CFG=acct-01" "$out"
4434
+ mcp_has acct-01 nested && t_fail "nested remove undone" "the next launch re-added nested" \
4435
+ || t_ok "a nested remove is not undone by the next launch"
4436
+ # a CLAUDE_CONFIG_DIR OUTSIDE the pool is plain passthrough: no python, no mirror
4437
+ mkdir -p "$WORK/mcp-outside"
4438
+ : > "$PYLOG"
4439
+ 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)"
4440
+ [ "$?" = "0" ] && [ ! -s "$PYLOG" ] && ! mcp_has acct-01 outside \
4441
+ && t_ok "a config dir outside the pool is untouched passthrough (no python, no mirror)" \
4442
+ || t_fail "outside passthrough" "$out; pylog: $(cat "$PYLOG")"
4443
+ # the help pre-scan stops at `--`: -h in the SERVER's command line is not our --help
4444
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp add --provider claude hsvc -- node s.js -h 127.0.0.1 2>&1)"
4445
+ rc=$?
4446
+ 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
4447
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp list --json 2>&1)"
4448
+ 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" \
4449
+ && t_ok "the server keeps its -h argument" || t_fail "help scan args" "$out"
4450
+ # retirement reaches a server that was only ever hand-added to ONE account
4451
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp remove --provider claude own-01 2>&1)"
4452
+ mcp_has acct-01 own-01 && t_fail "retire hand-added" "own-01 survives" \
4453
+ || t_ok "mcp remove retires a server that was hand-added to a single account"
4454
+ # --provider both: the change reaches the codex pool beside this one
4455
+ CXM="$WORK/mcp-codex-pool"
4456
+ mkdir -p "$CXM/acct-01" "$CXM/tmp"
4457
+ cat > "$CXM/accounts.json" <<'EOF'
4458
+ {"version":1,"server":"none","threshold":90,
4459
+ "accounts":[{"id":"acct-01","email":"cm1@cx","home":"mac","added_at":"2026-08-21T00:00:00Z"}]}
4460
+ EOF
4461
+ printf 'model = "gpt-5"\n\n[mcp_servers.keep]\ncommand = "true"\nargs = []\n' > "$CXM/acct-01/config.toml"
4462
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CODEX_ACCOUNTS_DIR="$CXM" CODEX_MULTIACC_NO_SYNC=1 claude-accounts mcp add shared -- echo both 2>&1)"
4463
+ rc=$?
4464
+ [ "$rc" = "0" ] && t_ok "mcp add --provider both (default) exits 0" || t_fail "mcp add both rc" "rc=$rc: $out"
4465
+ mcp_has acct-01 shared && t_ok "--provider both: the claude pool has the server" || t_fail "both claude" "$out"
4466
+ grep -q '^\[mcp_servers.shared\]' "$CXM/acct-01/config.toml" \
4467
+ && t_ok "--provider both: the codex pool beside it has [mcp_servers.shared]" \
4468
+ || t_fail "both codex" "$out $(cat "$CXM/acct-01/config.toml")"
4469
+ grep -q '^model = "gpt-5"' "$CXM/acct-01/config.toml" && grep -q '^\[mcp_servers.keep\]' "$CXM/acct-01/config.toml" \
4470
+ && t_ok "the codex config keeps its unrelated content" || t_fail "codex lossless" "$(cat "$CXM/acct-01/config.toml")"
4471
+ [ -f "$CXM/mcp-servers.json" ] && t_ok "the codex pool got its own registry" || t_fail "codex registry" "missing"
4472
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CODEX_ACCOUNTS_DIR="$WORK/no-such-codex-pool" claude-accounts mcp add lonely -- echo one 2>&1)"
4473
+ rc=$?
4474
+ [ "$rc" = "0" ] && t_ok "no sibling pool: the mirror is silently skipped" || t_fail "no sibling" "rc=$rc: $out"
4475
+ case "$out" in *"no manifest"*|*"warning"*) t_fail "no sibling silence" "$out" ;; *) t_ok "no sibling pool: no warning" ;; esac
4476
+ # an EXPLICIT --provider codex with no codex pool is a loud error, not a silent no-op
4477
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CODEX_ACCOUNTS_DIR="$WORK/no-such-codex-pool" claude-accounts mcp add --provider codex q -- echo q 2>&1)"
4478
+ rc=$?
4479
+ [ "$rc" != "0" ] && check "an explicit --provider codex without a codex pool fails loudly" "no codex pool" "$out" \
4480
+ || t_fail "explicit provider" "rc=0: $out"
4481
+ # rc 3: the registry is saved, the healthy accounts are reconciled, the broken one is
4482
+ # reported, and the change STILL reaches the sibling pool (and would still sync)
4483
+ cp "$MPOOL/acct-02/.claude.json" "$WORK/mcp-acct-02.bak"
4484
+ printf '{not json' > "$MPOOL/acct-02/.claude.json"
4485
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" CODEX_ACCOUNTS_DIR="$CXM" CODEX_MULTIACC_NO_SYNC=1 claude-accounts mcp add partial -- echo p 2>&1)"
4486
+ rc=$?
4487
+ [ "$rc" = "3" ] && t_ok "a corrupt account makes mcp add exit 3 (saved, partially applied)" || t_fail "partial rc" "rc=$rc: $out"
4488
+ check "the partial add says how to repair" "mcp apply" "$out"
4489
+ mcp_has acct-01 partial && t_ok "the healthy account was reconciled despite the corrupt one" || t_fail "partial healthy" "$out"
4490
+ grep -q '^\[mcp_servers.partial\]' "$CXM/acct-01/config.toml" \
4491
+ && t_ok "the partial add still reached the codex pool beside it" || t_fail "partial sibling" "$(cat "$CXM/acct-01/config.toml")"
4492
+ cp "$WORK/mcp-acct-02.bak" "$MPOOL/acct-02/.claude.json"
4493
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp apply 2>&1)"
4494
+ mcp_has acct-02 partial && t_ok "mcp apply repairs the account once its config is fixed" || t_fail "partial repair" "$out"
4495
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp add --provider mars z -- echo z 2>&1)"
4496
+ rc=$?
4497
+ [ "$rc" != "0" ] && check "an unknown --provider is refused" "must be claude, codex or both" "$out" \
4498
+ || t_fail "provider validation" "rc=0"
4499
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp bogus 2>&1)"
4500
+ rc=$?
4501
+ [ "$rc" != "0" ] && check "an unknown mcp subcommand is refused" "unknown mcp subcommand" "$out" \
4502
+ || t_fail "subcommand validation" "rc=0"
4503
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts mcp --help 2>&1)"
4504
+ check "mcp --help prints usage" "claude-accounts mcp add" "$out"
4505
+ # seeding a NEW account applies the registry (import goes through seed_account_dir)
4506
+ out="$(CLAUDE_ACCOUNTS_ROOT="$MPOOL" claude-accounts import m3@test --id acct-03 --no-sync 2>&1)"
4507
+ mcp_has acct-03 reg1 && mcp_has acct-03 shared && t_ok "a newly seeded account gets every registered server" \
4508
+ || t_fail "seed applies registry" "$out $(cat "$MPOOL/acct-03/.claude.json" 2>/dev/null)"
4509
+
4151
4510
  # ---- 17b. verify authenticates the way the SHIM would ---------------------------------
4152
4511
  # A dead credential next to a live portable token: the shim runs that account with the
4153
4512
  # TOKEN, so verify must too — testing it with the dead credential would fail a healthy
@@ -4446,11 +4805,65 @@ export MKAUTH
4446
4805
  mk_cx_auth() { python3 "$MKAUTH" "$@"; }
4447
4806
  FUTURE_EXP=$((now + 864000))
4448
4807
 
4808
+ # A stand-in for the real `codex mcp add|remove`: appends / strips a
4809
+ # `[mcp_servers.NAME]` table in $CODEX_HOME/config.toml, where the real CLI keeps them.
4810
+ # A separate file because the fake codex below is an UNQUOTED heredoc.
4811
+ FAKE_CODEX_MCP="$WORK/fake_codex_mcp.py"
4812
+ cat > "$FAKE_CODEX_MCP" <<'EOF'
4813
+ import os, re, sys
4814
+ path, argv = sys.argv[1], sys.argv[2:]
4815
+ verb, rest = argv[1], argv[2:]
4816
+ name, cmd, envs = None, [], {}
4817
+ i = 0
4818
+ while i < len(rest):
4819
+ a = rest[i]
4820
+ if a == "--env":
4821
+ k, v = rest[i + 1].split("=", 1); envs[k] = v; i += 2; continue
4822
+ if a == "--":
4823
+ cmd = rest[i + 1:]; break
4824
+ if name is None:
4825
+ name = a
4826
+ i += 1
4827
+ src = open(path).read() if os.path.exists(path) else ""
4828
+ out, skip, found = [], False, False
4829
+ for line in src.splitlines(keepends=True):
4830
+ st = line.strip()
4831
+ if st.startswith("["):
4832
+ skip = bool(re.match(r"\[mcp_servers\." + re.escape(name) + r"(\.|\])", st))
4833
+ found = found or skip
4834
+ if not skip:
4835
+ out.append(line)
4836
+ src = "".join(out)
4837
+ def q(v):
4838
+ return '"' + v.replace("\\", "\\\\").replace('"', '\\"') + '"'
4839
+ if verb == "add":
4840
+ if src and not src.endswith("\n"):
4841
+ src += "\n"
4842
+ src += "\n[mcp_servers.%s]\ncommand = %s\nargs = [%s]\n" % (name, q(cmd[0]), ", ".join(q(a) for a in cmd[1:]))
4843
+ if envs:
4844
+ src += "env = { %s }\n" % ", ".join("%s = %s" % (k, q(v)) for k, v in envs.items())
4845
+ print("Added global MCP server '%s'." % name)
4846
+ elif not found:
4847
+ print("No MCP server named '%s' found." % name, file=sys.stderr); sys.exit(1)
4848
+ else:
4849
+ print("Removed global MCP server '%s'." % name)
4850
+ with open(path + ".tmp", "w") as f:
4851
+ f.write(src)
4852
+ os.replace(path + ".tmp", path)
4853
+ EOF
4854
+
4449
4855
  # Fake "real" codex: prints which CODEX_HOME it ran under; scriptable login +
4450
4856
  # failures via a control file. (Note: must not contain the hyphenated shim marker.)
4451
4857
  cat > "$FAKEBIN/codex" <<EOF
4452
4858
  #!/usr/bin/env bash
4453
4859
  # fake real codex for tests (not a shim)
4860
+ if [ "\${1:-}" = "mcp" ] && [ -n "\${CODEX_HOME:-}" ]; then
4861
+ python3 "$FAKE_CODEX_MCP" "\$CODEX_HOME/config.toml" "\$@"
4862
+ rc=\$?
4863
+ # FAKE_MCP_WRITE_THEN_FAIL=1: wrote the table, then died — nothing may be mirrored.
4864
+ [ "\$rc" = 0 ] && [ -n "\${FAKE_MCP_WRITE_THEN_FAIL:-}" ] && exit 1
4865
+ exit \$rc
4866
+ fi
4454
4867
  if [ "\${1:-}" = "login" ]; then
4455
4868
  shift
4456
4869
  # record the flags the CLI chose (device-auth default vs --browser opt-out)
@@ -6501,6 +6914,184 @@ else
6501
6914
  t_ok "codex npm-layer tests skipped (node not installed)"
6502
6915
  fi
6503
6916
 
6917
+ # ---- C19. MCP servers for every account (codex parity) ------------------------------
6918
+ # Same contract as 16e over config.toml: the registry is reconciled into the picked
6919
+ # account before exec (stamped, no python when nothing changed), a stock `codex mcp
6920
+ # add|remove` under the shim is mirrored into every account, and `codex-accounts mcp`
6921
+ # manages the registry — for this pool alone with --provider codex.
6922
+ CXP="$WORK/cx-mcp-pool"
6923
+ mkdir -p "$CXP/acct-01" "$CXP/acct-02" "$CXP/tmp"
6924
+ : > "$CXP/.limits-kick"
6925
+ cat > "$CXP/accounts.json" <<'EOF'
6926
+ {"version":1,"server":"none","threshold":90,
6927
+ "accounts":[
6928
+ {"id":"acct-01","email":"p1@cx","home":"mac","added_at":"2026-08-21T00:00:00Z"},
6929
+ {"id":"acct-02","email":"p2@cx","home":"mac","added_at":"2026-08-21T00:00:00Z"}]}
6930
+ EOF
6931
+ mk_cx_auth "$CXP/acct-01/auth.json" p1@cx "$FUTURE_EXP"
6932
+ mk_cx_auth "$CXP/acct-02/auth.json" p2@cx "$FUTURE_EXP"
6933
+ for i in 01 02; do
6934
+ printf 'model = "gpt-5"\n\n[mcp_servers.own-%s]\ncommand = "true"\nargs = []\n' "$i" > "$CXP/acct-$i/config.toml"
6935
+ done
6936
+ cx_has() { grep -q "^\[mcp_servers\.$2\]" "$CXP/$1/config.toml" 2>/dev/null; }
6937
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts mcp add --provider codex reg1 -- echo hi 2>&1)"
6938
+ rc=$?
6939
+ [ "$rc" = "0" ] && t_ok "codex: mcp add (codex only) exits 0" || t_fail "codex mcp add rc" "rc=$rc: $out"
6940
+ cx_has acct-01 reg1 && cx_has acct-02 reg1 && t_ok "codex: mcp add reconciles EVERY account" \
6941
+ || t_fail "codex mcp add reconcile" "$(cat "$CXP/acct-01/config.toml" "$CXP/acct-02/config.toml")"
6942
+ cx_has acct-01 own-01 && grep -q '^model = "gpt-5"' "$CXP/acct-01/config.toml" \
6943
+ && t_ok "codex: an account's own servers and settings survive the reconcile" \
6944
+ || t_fail "codex extras preserved" "$(cat "$CXP/acct-01/config.toml")"
6945
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts mcp list 2>&1)"
6946
+ check "codex: mcp list names the server" "reg1" "$out"
6947
+ # drift + launch: the picked account is reconciled before exec, and stamped
6948
+ python3 - "$CXP/acct-02/config.toml" <<'PY'
6949
+ import re, sys
6950
+ p = sys.argv[1]; src = open(p).read()
6951
+ out, skip = [], False
6952
+ for line in src.splitlines(keepends=True):
6953
+ if line.lstrip().startswith("["):
6954
+ skip = line.strip().startswith("[mcp_servers.reg1")
6955
+ if not skip: out.append(line)
6956
+ open(p, "w").write("".join(out))
6957
+ PY
6958
+ cx_has acct-02 reg1 && t_fail "codex drift fixture" "reg1 still present" || t_ok "codex drift fixture: acct-02 lost reg1"
6959
+ : > "$PYLOG"
6960
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_ACCOUNT=acct-02 CLAUDE_MULTIACC_PYTHON="$WORK/pylog.sh" codex 2>&1)"
6961
+ check "codex: launch runs on the pinned account" "CFG=acct-02" "$out"
6962
+ cx_has acct-02 reg1 && t_ok "codex: the shim reconciles the picked account before exec" \
6963
+ || t_fail "codex shim reconcile" "$(cat "$CXP/acct-02/config.toml")"
6964
+ [ -s "$CXP/acct-02/.mcp-applied" ] && t_ok "codex: the reconcile leaves a stamp" || t_fail "codex stamp" "no .mcp-applied"
6965
+ : > "$PYLOG"
6966
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_ACCOUNT=acct-02 CLAUDE_MULTIACC_PYTHON="$WORK/pylog.sh" codex 2>&1)"
6967
+ check "codex: second launch still runs" "CFG=acct-02" "$out"
6968
+ [ ! -s "$PYLOG" ] && t_ok "codex: an unchanged registry + config spends no python start-up" \
6969
+ || t_fail "codex stamp short-circuit" "python ran: $(cat "$PYLOG")"
6970
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_ACCOUNT=acct-02 CLAUDE_MULTIACC_PYTHON=/nonexistent/python3 codex 2>&1)"
6971
+ rc=$?
6972
+ [ "$rc" = "0" ] && check "codex: a missing python never breaks a launch" "CFG=acct-02" "$out" \
6973
+ || t_fail "codex fail-open python" "rc=$rc: $out"
6974
+ # kill switch
6975
+ python3 - "$CXP/acct-01/config.toml" <<'PY'
6976
+ import sys
6977
+ p = sys.argv[1]; src = open(p).read()
6978
+ out, skip = [], False
6979
+ for line in src.splitlines(keepends=True):
6980
+ if line.lstrip().startswith("["):
6981
+ skip = line.strip().startswith("[mcp_servers.reg1")
6982
+ if not skip: out.append(line)
6983
+ open(p, "w").write("".join(out))
6984
+ PY
6985
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_ACCOUNT=acct-01 CODEX_MULTIACC_MCP=0 codex 2>&1)"
6986
+ check "codex: kill switch: launch still runs" "CFG=acct-01" "$out"
6987
+ cx_has acct-01 reg1 && t_fail "codex kill switch" "CODEX_MULTIACC_MCP=0 still reconciled" \
6988
+ || t_ok "codex: CODEX_MULTIACC_MCP=0 leaves the account alone"
6989
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts mcp apply 2>&1)"
6990
+ cx_has acct-01 reg1 && t_ok "codex: mcp apply repairs the drifted account" || t_fail "codex mcp apply" "$out"
6991
+ # learn-from-write: a stock 'codex mcp add' under the shim reaches EVERY account
6992
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex mcp add x -- echo hi 2>&1)"
6993
+ rc=$?
6994
+ [ "$rc" = "0" ] && check "codex: stock 'codex mcp add' still answers like the real client" "Added global MCP server 'x'" "$out" \
6995
+ || t_fail "codex stock mcp add rc" "rc=$rc: $out"
6996
+ cx_has acct-01 x && cx_has acct-02 x && t_ok "codex: a stock 'codex mcp add' lands in EVERY account" \
6997
+ || t_fail "codex mirror add" "$(cat "$CXP/acct-01/config.toml" "$CXP/acct-02/config.toml")"
6998
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts mcp list --json 2>&1)"
6999
+ 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" \
7000
+ && t_ok "codex: the mirrored server is in the registry as a Claude-style block" || t_fail "codex mirror registry" "$out"
7001
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex mcp remove x 2>&1)"
7002
+ rc=$?
7003
+ [ "$rc" = "0" ] && t_ok "codex: stock 'codex mcp remove' exits 0" || t_fail "codex stock mcp remove rc" "rc=$rc: $out"
7004
+ cx_has acct-01 x || cx_has acct-02 x \
7005
+ && t_fail "codex mirror remove" "x survives in an account" || t_ok "codex: a stock 'codex mcp remove' drops the server from EVERY account"
7006
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts mcp list --json 2>&1)"
7007
+ 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" \
7008
+ && t_ok "codex: a removed server is retired in the registry" || t_fail "codex tombstone" "$out"
7009
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_ACCOUNT=acct-01 CODEX_MULTIACC_MCP=0 codex mcp add solo -- echo solo 2>&1)"
7010
+ cx_has acct-01 solo && ! cx_has acct-02 solo && t_ok "codex: CODEX_MULTIACC_MCP=0: a stock add stays in one account" \
7011
+ || t_fail "codex kill switch mirror" "$out"
7012
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_ACCOUNT=acct-02 codex mcp remove never-there 2>&1)"
7013
+ rc=$?
7014
+ [ "$rc" != "0" ] && t_ok "codex: a failed 'codex mcp remove' keeps its exit status" || t_fail "codex mcp remove rc" "rc=0: $out"
7015
+ # a client that wrote its table and then died: nothing is mirrored, the failure passes
7016
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_ACCOUNT=acct-02 FAKE_MCP_WRITE_THEN_FAIL=1 codex mcp add wtf -- echo w 2>&1)"
7017
+ rc=$?
7018
+ [ "$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"
7019
+ cx_has acct-02 wtf && ! cx_has acct-01 wtf && t_ok "codex: the failed add stays in the one account that wrote it" \
7020
+ || t_fail "codex write-then-fail leak" "$(cat "$CXP/acct-01/config.toml")"
7021
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts mcp list --json 2>&1)"
7022
+ 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" \
7023
+ && t_ok "codex: a failed stock add is never mirrored into the registry" || t_fail "codex failed mirror add" "$out"
7024
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_ACCOUNT=acct-02 FAKE_MCP_WRITE_THEN_FAIL=1 codex mcp remove wtf 2>&1)"
7025
+ rc=$?
7026
+ [ "$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"
7027
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts mcp list --json 2>&1)"
7028
+ 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" \
7029
+ && t_ok "codex: a failed stock remove leaves no tombstone" || t_fail "codex failed mirror remove" "$out"
7030
+ # NESTED session: CODEX_HOME=<acct> + CODEX_SHIM_ACTIVE=1 is what every pooled codex
7031
+ # session's children inherit — the passthrough must mirror the write all the same
7032
+ : > "$PYLOG"
7033
+ 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)"
7034
+ rc=$?
7035
+ [ "$rc" = "0" ] && t_ok "codex: a nested (in-session) 'codex mcp add' exits 0" || t_fail "codex nested add rc" "rc=$rc: $out"
7036
+ cx_has acct-01 nested && cx_has acct-02 nested && t_ok "codex: a nested add is mirrored into EVERY account" \
7037
+ || t_fail "codex nested mirror" "$out $(cat "$CXP/acct-02/config.toml")"
7038
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_HOME="$CXP/acct-01" CODEX_SHIM_ACTIVE=1 codex mcp remove nested 2>&1)"
7039
+ rc=$?
7040
+ [ "$rc" = "0" ] && t_ok "codex: a nested 'codex mcp remove' exits 0" || t_fail "codex nested remove rc" "rc=$rc: $out"
7041
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts mcp list --json 2>&1)"
7042
+ 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" \
7043
+ && t_ok "codex: a nested remove leaves a tombstone" || t_fail "codex nested tombstone" "$out"
7044
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_ACCOUNT=acct-01 codex 2>&1)"
7045
+ check "codex: the next launch on that account still runs" "CFG=acct-01" "$out"
7046
+ cx_has acct-01 nested && t_fail "codex nested remove undone" "the next launch re-added nested" \
7047
+ || t_ok "codex: a nested remove is not undone by the next launch"
7048
+ mkdir -p "$WORK/cx-outside"
7049
+ : > "$PYLOG"
7050
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CODEX_HOME="$WORK/cx-outside" CLAUDE_MULTIACC_PYTHON="$WORK/pylog.sh" codex mcp add outside -- echo o 2>&1)"
7051
+ [ "$?" = "0" ] && [ ! -s "$PYLOG" ] && ! cx_has acct-01 outside \
7052
+ && t_ok "codex: a CODEX_HOME outside the pool is untouched passthrough (no python, no mirror)" \
7053
+ || t_fail "codex outside passthrough" "$out; pylog: $(cat "$PYLOG")"
7054
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts mcp add --provider codex hsvc -- node s.js -h 127.0.0.1 2>&1)"
7055
+ rc=$?
7056
+ 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
7057
+ grep -q '^args = \["s.js", "-h", "127.0.0.1"\]' "$CXP/acct-01/config.toml" \
7058
+ && t_ok "codex: the server keeps its -h argument" || t_fail "codex help scan args" "$(cat "$CXP/acct-01/config.toml")"
7059
+ # --provider both from the codex side reaches the claude pool beside it
7060
+ CLM="$WORK/mcp-claude-pool"
7061
+ mkdir -p "$CLM/acct-01" "$CLM/tmp"
7062
+ cat > "$CLM/accounts.json" <<'EOF'
7063
+ {"version":1,"server":"none","threshold":90,
7064
+ "accounts":[{"id":"acct-01","email":"cl1@test","home":"mac","added_at":"2026-07-13T00:00:00Z"}]}
7065
+ EOF
7066
+ printf '{"mcpServers":{}}\n' > "$CLM/acct-01/.claude.json"
7067
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CLAUDE_ACCOUNTS_ROOT="$CLM" codex-accounts mcp add shared -- echo both 2>&1)"
7068
+ rc=$?
7069
+ [ "$rc" = "0" ] && t_ok "codex: mcp add --provider both (default) exits 0" || t_fail "codex mcp add both rc" "rc=$rc: $out"
7070
+ cx_has acct-01 shared && t_ok "codex: --provider both: the codex pool has the server" || t_fail "codex both codex" "$out"
7071
+ 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" \
7072
+ && t_ok "codex: --provider both: the claude pool beside it has the server" \
7073
+ || t_fail "codex both claude" "$(cat "$CLM/acct-01/.claude.json")"
7074
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts mcp --help 2>&1)"
7075
+ check "codex: mcp --help prints usage" "codex-accounts mcp add" "$out"
7076
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CLAUDE_ACCOUNTS_ROOT="$WORK/no-such-claude-pool" codex-accounts mcp add --provider claude q -- echo q 2>&1)"
7077
+ rc=$?
7078
+ [ "$rc" != "0" ] && check "codex: an explicit --provider claude without a claude pool fails loudly" "no claude pool" "$out" \
7079
+ || t_fail "codex explicit provider" "rc=0: $out"
7080
+ cp "$CXP/acct-02/config.toml" "$WORK/cx-acct-02.bak"
7081
+ printf '[mcp_servers.broken\ncommand = "x"\n' > "$CXP/acct-02/config.toml"
7082
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" CLAUDE_ACCOUNTS_ROOT="$CLM" codex-accounts mcp add partial -- echo p 2>&1)"
7083
+ rc=$?
7084
+ [ "$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"
7085
+ cx_has acct-01 partial && t_ok "codex: the healthy account was reconciled despite the corrupt one" || t_fail "codex partial healthy" "$out"
7086
+ 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" \
7087
+ && t_ok "codex: the partial add still reached the claude pool beside it" || t_fail "codex partial sibling" "$(cat "$CLM/acct-01/.claude.json")"
7088
+ cp "$WORK/cx-acct-02.bak" "$CXP/acct-02/config.toml"
7089
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts mcp apply 2>&1)"
7090
+ cx_has acct-02 partial && t_ok "codex: mcp apply repairs the account once its config is fixed" || t_fail "codex partial repair" "$out"
7091
+ out="$(CODEX_ACCOUNTS_ROOT="$CXP" codex-accounts import p3@cx --id acct-03 --no-sync 2>&1)"
7092
+ cx_has acct-03 reg1 && cx_has acct-03 shared && t_ok "codex: a newly seeded account gets every registered server" \
7093
+ || t_fail "codex seed applies registry" "$out $(cat "$CXP/acct-03/config.toml" 2>/dev/null)"
7094
+
6504
7095
  # ---- 20. panel pools: --json, credential transfer, instance roots, local sync -------
6505
7096
  # Everything here runs against pools under $WORK/panel, addressed with the NEW
6506
7097
  # CLAUDE_ACCOUNTS_ROOT/CODEX_ACCOUNTS_ROOT env — while CLAUDE_ACCOUNTS_DIR (the legacy
@@ -7303,6 +7894,16 @@ else
7303
7894
  fi
7304
7895
  # ...and its claude twin: the cedar_ember status rides on the usage response, the claim
7305
7896
  # is idempotent across the fleet, and a confirmed reset lifts even a weekly client park.
7897
+ # The claude half of the MCP registry must run on STOCK macOS python (3.9): the shim
7898
+ # reconciles with whatever `python3` is on PATH, and a runner Mac's is /usr/bin/python3.
7899
+ # Skipped silently where that interpreter is absent or already 3.11+ (CI is ubuntu).
7900
+ 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
7901
+ if /usr/bin/python3 "$REPO_DIR/tests/test_mcp_registry.py" >/dev/null 2>&1; then
7902
+ t_ok "MCP registry unit suite on stock python $(/usr/bin/python3 -c 'import sys;print("%d.%d"%sys.version_info[:2])')"
7903
+ else
7904
+ t_fail "MCP registry unit suite on stock python" "/usr/bin/python3 tests/test_mcp_registry.py failed"
7905
+ fi
7906
+ fi
7306
7907
  if CLAUDE_MULTIACC_AUTO_RESET=1 python3 -m unittest discover -s "$REPO_DIR/tests" -p 'test_claude_reset*.py'; then
7307
7908
  t_ok "claude: automatic limit-reset integration suite"
7308
7909
  else