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.
- package/README.md +78 -0
- package/bin/claude +122 -6
- package/bin/claude-accounts +145 -3
- package/bin/codex +116 -7
- package/bin/codex-accounts +137 -3
- package/docs/ACCOUNT_OPERATIONS.md +34 -0
- package/docs/CODEX.md +26 -0
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
- package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
- package/lib/common.sh +19 -0
- package/lib/mcp_registry.py +1372 -0
- package/package.json +2 -2
- package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
- package/tests/run-tests.sh +605 -4
- package/tests/test_account_seeding.py +40 -0
- package/tests/test_mcp_registry.py +597 -0
package/tests/run-tests.sh
CHANGED
|
@@ -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
|
|
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" = "
|
|
3932
|
-
|| t_fail "per-target removal" "expected
|
|
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
|