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/README.md
CHANGED
|
@@ -395,6 +395,84 @@ an account, and if one slips through it returns to the pool by itself. Output is
|
|
|
395
395
|
never double-emits. Only engages when stdin is finite (tty / regular file / `/dev/null`)
|
|
396
396
|
and ≥2 accounts are eligible; service-spawned pipes take the plain exec path untouched.
|
|
397
397
|
|
|
398
|
+
## MCP servers for every account
|
|
399
|
+
|
|
400
|
+
Both clients keep MCP servers in the **config dir** — `<dir>/.claude.json` for Claude Code,
|
|
401
|
+
`<CODEX_HOME>/config.toml` for Codex — so under the pool there is one MCP configuration per
|
|
402
|
+
account, and a stock `claude mcp add` lands in **one random account**. That is how
|
|
403
|
+
`appinspire-mcp` was connected in some sessions and missing in others (2026-09-22). The pool
|
|
404
|
+
now owns a **registry**, `<pool>/mcp-servers.json`, that is reconciled into every account:
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
claude-accounts mcp add appinspire-mcp -- npx -y appinspire-mcp@latest serve # both pools
|
|
408
|
+
claude-accounts mcp add --provider claude local-dev -e KEY=v -- node /path/server.mjs serve
|
|
409
|
+
claude-accounts mcp add --scope project --project "$HOME/design-lab/run1" appinspire -e APPINSPIRE_LIBRARY_DIR="$HOME/.appinspire-mcp/library" -- node "$HOME/appinspire-mcp/bin/appinspire-mcp.mjs" serve
|
|
410
|
+
claude-accounts mcp remove adspower-local-api # retire EVERYWHERE: every account, every project entry
|
|
411
|
+
claude-accounts mcp remove foo --user-only # user scope only; project-local copies stay
|
|
412
|
+
claude-accounts mcp list [--json]
|
|
413
|
+
claude-accounts mcp apply # re-apply now (repair drift)
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
- **Default `--provider both`**: an MCP server is not provider-specific, so `claude-accounts
|
|
417
|
+
mcp add` also writes the codex pool's registry (and `codex-accounts mcp add` the claude
|
|
418
|
+
one); `--provider claude|codex` narrows it, and an explicit provider whose pool is missing
|
|
419
|
+
on this machine is an error (the default is silent about it). `-e KEY=VAL` takes one
|
|
420
|
+
variable per flag; write `$HOME/...`, never `~/...`, inside a value (zsh does not expand
|
|
421
|
+
`~` after `=` in a word).
|
|
422
|
+
- **Tombstones**: an explicit `mcp remove NAME` retires the server **everywhere** — user
|
|
423
|
+
scope and every project entry, in every account, on every Mac after sync — even a copy
|
|
424
|
+
that was only ever hand-added to a single account (registry key `retired`). `--user-only`
|
|
425
|
+
records a user-scope-only tombstone (`retiredUser`). A stock removal mirrored from the shim
|
|
426
|
+
is scope-limited: `claude mcp remove -s user X` retires X in user scope only, a `-s local`
|
|
427
|
+
removal retires it in that one project only. Re-adding a name lifts its tombstone.
|
|
428
|
+
- **Exit status 3** from `mcp add|add-json|remove` means the registry was saved but some
|
|
429
|
+
account could not be reconciled (a corrupt `.claude.json`, a `config.toml` layout the
|
|
430
|
+
editor refuses): the change still syncs and still reaches the sibling pool; fix the account
|
|
431
|
+
and run `mcp apply`. Exit 1 means nothing was saved.
|
|
432
|
+
- **Where it is applied**: at seed time (`add`/`import`/`sync`/`post-sync`, so a new account or
|
|
433
|
+
a fleet Mac gets it), and by the **shim right before exec** — the picked account is
|
|
434
|
+
reconciled when the registry, the machine-local overlay or the account's own config changed
|
|
435
|
+
since the last verified reconcile (stamp in `<acct>/.mcp-applied`); Claude Code rewrites
|
|
436
|
+
`.claude.json` during most sessions, so on the claude side that check usually costs one
|
|
437
|
+
short python start-up per launch. Fail-open: a missing python or lib never breaks a launch.
|
|
438
|
+
- **Stock commands are mirrored**: `claude mcp add|add-json|remove` and `codex mcp add|remove`
|
|
439
|
+
run under the shim execute against the picked account as usual, and the shim then copies the
|
|
440
|
+
outcome into the registry and every other account (`learn`). This includes a command run
|
|
441
|
+
from **inside** a pooled session (an agent's Bash tool, `npx appinspire-mcp install` run by
|
|
442
|
+
an agent): the session's config dir is a pool account, so the same mirror applies. A
|
|
443
|
+
config dir outside the pool is plain passthrough.
|
|
444
|
+
- **Replicas**: on a pool whose `sync-role` file says `replica`, `add`/`remove` and the
|
|
445
|
+
shim's mirror land in the machine-local overlay (owner `local`) instead of the synced
|
|
446
|
+
registry, because the source's next push would overwrite them. Make registry changes on
|
|
447
|
+
the source machine.
|
|
448
|
+
- **Project scope is claude-only** (`--scope project --project PATH`, or a stock `-s local`
|
|
449
|
+
add): Codex 0.156 reads no per-project MCP config, so a project entry is applied to every
|
|
450
|
+
claude account's `projects[PATH]` and skipped for codex.
|
|
451
|
+
- **Fleet**: the **claude** registry rides with the manifest — `sync` pushes it to the server
|
|
452
|
+
and every manifest peer (all runner Macs), and the claude limits pass fans it out with the
|
|
453
|
+
telemetry, so a mirrored add reaches every Mac within one cadence; each target's
|
|
454
|
+
`post-sync` re-seeds and applies it. The **codex** registry reaches only codex sync
|
|
455
|
+
targets (the server, gas-mini): runner Macs' codex pools are local-only and get their
|
|
456
|
+
servers from app-robot's runner overlay (`codex-accounts mcp import-local --owner
|
|
457
|
+
app-robot`, app-robot PR #1870) or from a direct `codex-accounts mcp add --provider codex …`
|
|
458
|
+
on that Mac. The **machine-local overlay** `mcp-servers.local.json` is applied on its Mac
|
|
459
|
+
and **never synced**; the synced registry wins on a name both hold.
|
|
460
|
+
- **Codex translation**: a Claude-style block becomes `[mcp_servers.<name>]`; unrelated
|
|
461
|
+
tables, their comments and blank lines are left alone. `KEY=${KEY}` env references become
|
|
462
|
+
`env_vars` (codex forwards a variable under its own name only); `KEY=${OTHER}` and
|
|
463
|
+
`${VAR:-default}` cannot be expressed, so that variable is left out with a warning. A stdio
|
|
464
|
+
server gets `startup_timeout_sec = 60` unless the block sets one (a cold `npx` download must
|
|
465
|
+
not trip Codex's 10 s default); SSE transports are skipped; codex-only keys (`oauth`,
|
|
466
|
+
`enabled_tools`, …) ride along in the registry block and never reach `.claude.json`.
|
|
467
|
+
- **Kill switches**: `CLAUDE_MULTIACC_MCP=0` / `CODEX_MULTIACC_MCP=0` stop the reconcile at
|
|
468
|
+
seed and launch and the shim's mirror; `claude-accounts mcp …`, sync and the fan-out still
|
|
469
|
+
work.
|
|
470
|
+
|
|
471
|
+
app-robot tasks are different: the panel launches Claude with `--strict-mcp-config` and a
|
|
472
|
+
merged MCP document (`agent_mcp.servers_for`) and Codex with the same document as `-c`
|
|
473
|
+
overrides, so a server every task must see is added there — never with `claude mcp add` on a
|
|
474
|
+
Mac.
|
|
475
|
+
|
|
398
476
|
## Codex support (OpenAI Codex CLI)
|
|
399
477
|
|
|
400
478
|
Both pools use the same selection policy. See [Codex account commands and behavior](docs/CODEX.md).
|
package/bin/claude
CHANGED
|
@@ -71,11 +71,117 @@ REAL="$(find_real)" || {
|
|
|
71
71
|
exit 127
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
if [ "$(uname -s)" = "Darwin" ]; then
|
|
75
|
+
file_mtime() { stat -f %m "$1" 2>/dev/null || echo 0; }
|
|
76
|
+
file_sig() { stat -L -f '%m:%z' "$1" 2>/dev/null || echo '?'; }
|
|
77
|
+
else
|
|
78
|
+
file_mtime() { stat -c %Y "$1" 2>/dev/null || echo 0; }
|
|
79
|
+
file_sig() { stat -L -c '%Y:%s' "$1" 2>/dev/null || echo '?'; }
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# ---- MCP servers for every account (lib/mcp_registry.py) -------------------------
|
|
83
|
+
# Claude Code keeps MCP servers in the CONFIG DIR (`<dir>/.claude.json`), so under the
|
|
84
|
+
# pool there is one MCP configuration per account — and a stock `claude mcp add` lands
|
|
85
|
+
# in ONE random one (2026-09-22: appinspire was connected in some sessions and not in
|
|
86
|
+
# others, depending on the account drawn). The pool's registry (mcp-servers.json, synced
|
|
87
|
+
# fleet-wide) plus a machine-local overlay (mcp-servers.local.json) are reconciled into
|
|
88
|
+
# the picked account right before exec, so a session gets the same servers whichever
|
|
89
|
+
# account it drew. Bash only on the hot path: python runs when the registry, the overlay
|
|
90
|
+
# or the account's own config changed since the last verified reconcile — the stamp in
|
|
91
|
+
# <dir>/.mcp-applied — and never otherwise. Fail-open throughout: a missing python, a
|
|
92
|
+
# missing lib or a broken registry costs nothing but the servers themselves.
|
|
93
|
+
MCP_PY="$SELF_DIR/../lib/mcp_registry.py"
|
|
94
|
+
MCP_PYBIN="${CLAUDE_MULTIACC_PYTHON:-python3}"
|
|
95
|
+
mcp_sig() { # $1 acct dir -> "m:z,m:z,m:z" (registry, overlay, account config; - = absent)
|
|
96
|
+
local f out="" s
|
|
97
|
+
for f in "$ACC_ROOT/mcp-servers.json" "$ACC_ROOT/mcp-servers.local.json" "$1/.claude.json"; do
|
|
98
|
+
if [ -f "$f" ]; then s="$(file_sig "$f")"; else s="-"; fi
|
|
99
|
+
out="$out${out:+,}$s"
|
|
100
|
+
done
|
|
101
|
+
printf '%s\n' "$out"
|
|
102
|
+
}
|
|
103
|
+
mcp_ensure() { # $1 acct dir — silent, never changes the exit status
|
|
104
|
+
[ "${CLAUDE_MULTIACC_MCP:-1}" = "0" ] && return 0
|
|
105
|
+
[ -f "$ACC_ROOT/mcp-servers.json" ] || [ -f "$ACC_ROOT/mcp-servers.local.json" ] || return 0
|
|
106
|
+
[ -f "$MCP_PY" ] || return 0
|
|
107
|
+
local stamp=""
|
|
108
|
+
[ -f "$1/.mcp-applied" ] && IFS= read -r stamp < "$1/.mcp-applied" 2>/dev/null
|
|
109
|
+
[ "${stamp:-}" = "$(mcp_sig "$1")" ] && return 0
|
|
110
|
+
"$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider claude apply --account-dir "$1" \
|
|
111
|
+
--fail-open --quiet >/dev/null 2>&1 </dev/null || true
|
|
112
|
+
return 0
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# `claude mcp add|add-json|remove` writes ONE account's .claude.json — the one it ran
|
|
116
|
+
# under — and nobody else's. So the shim runs it as a child instead of exec-ing it, then
|
|
117
|
+
# mirrors what it wrote into the registry and every other account (`learn` diffs the
|
|
118
|
+
# account against a snapshot taken just before). The real CLI's own parser and writer do
|
|
119
|
+
# the work; the shim only copies the outcome. Exits with the real binary's status; a
|
|
120
|
+
# mirror failure is one hint line. Returns 1 when this run is not such a command.
|
|
121
|
+
mcp_mirror_run() { # $1 acct dir, then the user's argv — exits when it handled the run
|
|
122
|
+
local d="$1" snap="" rc verb="${2:-}" sub="${3:-}"
|
|
123
|
+
shift
|
|
124
|
+
# The scoped-limit fallback may have prepended `--model X` (a global option the real
|
|
125
|
+
# CLI accepts in front of any subcommand): look past it.
|
|
126
|
+
if [ "$verb" = "--model" ]; then verb="${3:-}"; sub="${4:-}"; fi
|
|
127
|
+
[ "$verb" = "mcp" ] || return 1
|
|
128
|
+
case "$sub" in add|add-json|remove) ;; *) return 1 ;; esac
|
|
129
|
+
[ "${CLAUDE_MULTIACC_MCP:-1}" != "0" ] && [ -f "$MCP_PY" ] || return 1
|
|
130
|
+
mcp_ensure "$d"
|
|
131
|
+
snap="$(mktemp "${TMPDIR:-/tmp}/claude-mcp-snap.XXXXXX" 2>/dev/null)" || snap=""
|
|
132
|
+
if [ -n "$snap" ] && ! "$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider claude snapshot \
|
|
133
|
+
--account-dir "$d" > "$snap" 2>/dev/null </dev/null; then
|
|
134
|
+
rm -f "$snap" 2>/dev/null; snap=""
|
|
135
|
+
fi
|
|
136
|
+
"$REAL" "$@"
|
|
137
|
+
rc=$?
|
|
138
|
+
if [ "$rc" -eq 0 ] && [ -n "$snap" ]; then
|
|
139
|
+
"$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider claude learn --account-dir "$d" \
|
|
140
|
+
--before "$snap" --project "$(pwd -P 2>/dev/null || pwd)" </dev/null \
|
|
141
|
+
|| printf 'claude-multiacc: could not mirror the MCP change to the other accounts — run: claude-accounts mcp apply\n' >&2
|
|
142
|
+
elif [ "$rc" -eq 0 ]; then
|
|
143
|
+
printf 'claude-multiacc: could not mirror the MCP change to the other accounts — run: claude-accounts mcp apply\n' >&2
|
|
144
|
+
fi
|
|
145
|
+
[ -n "$snap" ] && rm -f "$snap" 2>/dev/null
|
|
146
|
+
exit "$rc"
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# A pooled SESSION exports CLAUDE_CONFIG_DIR=<acct> and CLAUDE_SHIM_ACTIVE=1, and every child
|
|
150
|
+
# process inherits both — so a `claude mcp add` run from INSIDE a session (an agent's
|
|
151
|
+
# Bash tool, `npx appinspire-mcp install` run by an agent) arrives at the fast passthrough
|
|
152
|
+
# below with the account already chosen, not at the selection path where the mirror
|
|
153
|
+
# lives. That is the very write the 2026-09-22 outage was made of, in the place MCP
|
|
154
|
+
# servers are most often installed. The account is known, so the same mirror applies:
|
|
155
|
+
# exits inside when it handled the run; anything else stays byte-identical passthrough.
|
|
156
|
+
mcp_nested_mirror() {
|
|
157
|
+
[ -f "$MANIFEST" ] || return 1
|
|
158
|
+
[ "${CLAUDE_MULTIACC_DISABLE:-0}" = "1" ] && return 1
|
|
159
|
+
[ "${CLAUDE_MULTIACC_MCP:-1}" = "0" ] && return 1
|
|
160
|
+
[ -n "${CLAUDE_CONFIG_DIR:-}" ] || return 1
|
|
161
|
+
local verb="${1:-}" cfg="${CLAUDE_CONFIG_DIR%/}" d=""
|
|
162
|
+
if [ "$verb" = "--model" ]; then verb="${3:-}"; fi
|
|
163
|
+
[ "$verb" = "mcp" ] || return 1
|
|
164
|
+
# A DIRECT child of the pool root, by its given spelling or its physical one (an
|
|
165
|
+
# adopted account is a symlink whose target is the operator's ~/.claude).
|
|
166
|
+
case "$cfg" in
|
|
167
|
+
"$ACC_ROOT"/acct-*) case "${cfg#"$ACC_ROOT"/}" in */*) ;; *) d="$cfg" ;; esac ;;
|
|
168
|
+
esac
|
|
169
|
+
if [ -z "$d" ]; then
|
|
170
|
+
local root_c; root_c="$(canon_path "$ACC_ROOT")"
|
|
171
|
+
case "$(canon_path "$cfg")" in
|
|
172
|
+
"$root_c"/acct-*) case "${cfg##*/}" in acct-*) d="$cfg" ;; esac ;;
|
|
173
|
+
esac
|
|
174
|
+
fi
|
|
175
|
+
[ -n "$d" ] && [ -d "$d" ] || return 1
|
|
176
|
+
mcp_mirror_run "$d" "$@"
|
|
177
|
+
}
|
|
178
|
+
|
|
74
179
|
# Fast passthrough: caller pinned a config dir or token, addon disabled, recursion
|
|
75
180
|
# guard, or no account data yet. Byte-identical behavior to stock claude.
|
|
76
181
|
if [ -n "${CLAUDE_CONFIG_DIR:-}" ] || [ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ] \
|
|
77
182
|
|| [ "${CLAUDE_MULTIACC_DISABLE:-0}" = "1" ] || [ -n "${CLAUDE_SHIM_ACTIVE:-}" ] \
|
|
78
183
|
|| [ ! -f "$MANIFEST" ]; then
|
|
184
|
+
mcp_nested_mirror "$@" || true
|
|
79
185
|
exec "$REAL" "$@"
|
|
80
186
|
fi
|
|
81
187
|
|
|
@@ -92,12 +198,6 @@ fi
|
|
|
92
198
|
# complaining to stderr.
|
|
93
199
|
case "$CLAUDE_MULTIACC_THRESHOLD" in ''|*[!0-9]*|??????*) CLAUDE_MULTIACC_THRESHOLD=90 ;; esac
|
|
94
200
|
|
|
95
|
-
if [ "$(uname -s)" = "Darwin" ]; then
|
|
96
|
-
file_mtime() { stat -f %m "$1" 2>/dev/null || echo 0; }
|
|
97
|
-
else
|
|
98
|
-
file_mtime() { stat -c %Y "$1" 2>/dev/null || echo 0; }
|
|
99
|
-
fi
|
|
100
|
-
|
|
101
201
|
# ---- macOS Keychain-held logins ----------------------------------------------------
|
|
102
202
|
# Claude Code on macOS moves a config dir's OAuth login into the login Keychain
|
|
103
203
|
# (service "Claude Code-credentials-<sha256(dir)[:8]>") the first time a GUI-session
|
|
@@ -1232,6 +1332,10 @@ if [ -n "${CLAUDE_ACCOUNT:-}" ]; then
|
|
|
1232
1332
|
fi
|
|
1233
1333
|
export CLAUDE_CODE_OAUTH_TOKEN="$tok"
|
|
1234
1334
|
fi
|
|
1335
|
+
# A pinned account carries every registered MCP server too, and a pinned
|
|
1336
|
+
# `claude mcp add|remove` is mirrored like any other (exits inside).
|
|
1337
|
+
mcp_ensure "$d"
|
|
1338
|
+
mcp_mirror_run "$d" "$@" || true
|
|
1235
1339
|
sel_capture_session "$d"
|
|
1236
1340
|
exec "$REAL" "$@"
|
|
1237
1341
|
fi
|
|
@@ -1572,6 +1676,8 @@ fi
|
|
|
1572
1676
|
# CLAUDE_ACCOUNT pin deliberately does not: a pin is a caller overriding selection,
|
|
1573
1677
|
# not a turn in the rotation.
|
|
1574
1678
|
remember_pick "$pick"
|
|
1679
|
+
# The picked account carries every registered MCP server before the client starts.
|
|
1680
|
+
mcp_ensure "$pick"
|
|
1575
1681
|
|
|
1576
1682
|
# Opportunistic limits refresh: non-blocking, throttled, backgrounded. The windows are
|
|
1577
1683
|
# deliberately wide (10m, matching the 5m scheduled pass): the usage endpoint rate-limits
|
|
@@ -1648,6 +1754,15 @@ fi
|
|
|
1648
1754
|
|
|
1649
1755
|
export CLAUDE_SHIM_ACTIVE=1
|
|
1650
1756
|
|
|
1757
|
+
# A `claude mcp add|add-json|remove` is run as a child under the picked account and
|
|
1758
|
+
# mirrored to every other one (exits inside); anything else falls through to exec.
|
|
1759
|
+
if [ "${1:-}" = "mcp" ] || { [ "${1:-}" = "--model" ] && [ "${3:-}" = "mcp" ]; }; then
|
|
1760
|
+
export CLAUDE_CONFIG_DIR="$pick"
|
|
1761
|
+
tok="$(acct_token "$pick")"
|
|
1762
|
+
[ -n "$tok" ] && export CLAUDE_CODE_OAUTH_TOKEN="$tok"
|
|
1763
|
+
mcp_mirror_run "$pick" "$@" || true
|
|
1764
|
+
fi
|
|
1765
|
+
|
|
1651
1766
|
# Auto-retry applies only to -p/--print runs with an alternative account available,
|
|
1652
1767
|
# and only when stdin is finite (tty, regular file, or char device like /dev/null).
|
|
1653
1768
|
# A service-spawned pipe that never EOFs must take the plain exec path, or the
|
|
@@ -1878,6 +1993,7 @@ while :; do
|
|
|
1878
1993
|
# The account that actually serves the work is the one the next run should rotate
|
|
1879
1994
|
# away from — not the one that bounced.
|
|
1880
1995
|
remember_pick "$cur"
|
|
1996
|
+
mcp_ensure "$cur"
|
|
1881
1997
|
rotated=1
|
|
1882
1998
|
attempt=2
|
|
1883
1999
|
continue
|
package/bin/claude-accounts
CHANGED
|
@@ -14,6 +14,12 @@ while [ -L "$_self" ]; do
|
|
|
14
14
|
done
|
|
15
15
|
BIN_DIR="$(cd "$(dirname "$_self")" && pwd -P)"
|
|
16
16
|
REPO_DIR="$(dirname "$BIN_DIR")"
|
|
17
|
+
# Pin the provider here, the way codex-accounts pins codex: lib/common.sh defaults to
|
|
18
|
+
# claude only when the variable is UNSET, and codex-accounts exports it — so a
|
|
19
|
+
# claude-accounts started by codex-accounts (the `mcp --provider both` hop) inherited
|
|
20
|
+
# `codex`, became a second codex CLI, hopped to "its sibling" claude-accounts again, and
|
|
21
|
+
# forked itself to death ("fork: Resource temporarily unavailable").
|
|
22
|
+
export MULTIACC_PROVIDER=claude
|
|
17
23
|
# shellcheck source=lib/common.sh
|
|
18
24
|
. "$REPO_DIR/lib/common.sh"
|
|
19
25
|
|
|
@@ -93,6 +99,30 @@ USAGE
|
|
|
93
99
|
claude-accounts health limits + full verify + shim-on-PATH probe; logs to health.log
|
|
94
100
|
claude-accounts self-update update this npm/git install; logs to update.log
|
|
95
101
|
claude-accounts post-sync (server side) seed dirs, fix perms, quick verify
|
|
102
|
+
claude-accounts mcp add <name> [--provider claude|codex|both] [--scope user|project]
|
|
103
|
+
[--project PATH] [-e KEY=VAL]... [-H 'Header: v']...
|
|
104
|
+
[--transport stdio|http] -- <command> [args...] (or a URL)
|
|
105
|
+
register an MCP server for EVERY account: writes the pool's mcp-servers.json
|
|
106
|
+
registry, reconciles every account dir, syncs the registry to the server and
|
|
107
|
+
peers (their post-sync applies it). Default --provider both: the same server is
|
|
108
|
+
registered in the codex pool too (MCP servers are not provider-specific).
|
|
109
|
+
A stock `claude mcp add|add-json|remove` run under the shim is mirrored into
|
|
110
|
+
the registry the same way — it no longer lands in one random account.
|
|
111
|
+
-e takes ONE variable per flag (KEY=VAL; write $HOME/..., not ~/..., in a value).
|
|
112
|
+
Exit 3 = registry saved, but an account could not be reconciled (it is named on
|
|
113
|
+
stderr): the change still syncs; fix the account and run `mcp apply`.
|
|
114
|
+
On a replica pool the change lands in the machine-local overlay instead of the
|
|
115
|
+
synced registry — make registry changes on the source machine.
|
|
116
|
+
claude-accounts mcp add-json <name> '<json>' [--provider ...] [--scope ...]
|
|
117
|
+
claude-accounts mcp remove <name> [--provider ...] [--user-only] [--scope project --project PATH]
|
|
118
|
+
unregister AND retire EVERYWHERE: every account (fleet-wide after sync) drops the
|
|
119
|
+
server from user scope and from every project entry, even one that was hand-added
|
|
120
|
+
to a single account. --user-only spares project-local copies.
|
|
121
|
+
claude-accounts mcp list [--json] effective registry (+ machine-local overlay)
|
|
122
|
+
claude-accounts mcp apply [--account-dir DIR]... [--all]
|
|
123
|
+
re-apply the registry now (repair drift)
|
|
124
|
+
claude-accounts mcp import-local --owner NAME (stdin JSON) machine-local overlay for
|
|
125
|
+
a runner daemon; applied here, never synced
|
|
96
126
|
|
|
97
127
|
ENV
|
|
98
128
|
CLAUDE_ACCOUNTS_ROOT pool root, overriding ~/.claude-accounts — one isolated pool
|
|
@@ -105,6 +135,7 @@ ENV
|
|
|
105
135
|
CLAUDE_SHIM_RETRY=0 disable -p auto-retry
|
|
106
136
|
CLAUDE_MULTIACC_DISABLE=1 bypass the shim entirely
|
|
107
137
|
CLAUDE_MULTIACC_AUTO_RESET=0 disable automatic limit-reset redemption
|
|
138
|
+
CLAUDE_MULTIACC_MCP=0 disable the MCP registry (no reconcile at seed/launch, no mirror)
|
|
108
139
|
CLAUDE_MULTIACC_RESET_MIN_HORIZON a reset is held while every park lifts on
|
|
109
140
|
its own within this many seconds (default 3600)
|
|
110
141
|
EOF
|
|
@@ -1541,6 +1572,10 @@ limits_distribute_now() {
|
|
|
1541
1572
|
# never grants any. Erring toward exclusion is the safe direction here.
|
|
1542
1573
|
[ -f "$d/.limited" ] && printf '%s/.limited\n' "$id" >> "$list"
|
|
1543
1574
|
done
|
|
1575
|
+
# The MCP registry rides along: a server the shim mirrored from a stock `claude mcp
|
|
1576
|
+
# add` reaches every peer within one limits cadence, with no explicit sync. The
|
|
1577
|
+
# machine-local overlay stays home (see sync_push_target).
|
|
1578
|
+
[ -f "$ACC_ROOT/mcp-servers.json" ] && printf 'mcp-servers.json\n' >> "$list"
|
|
1544
1579
|
if [ ! -s "$list" ]; then rm -f "$list"; return 0; fi
|
|
1545
1580
|
|
|
1546
1581
|
local pushed=0 failed=0
|
|
@@ -2893,6 +2928,14 @@ sync_push_target() {
|
|
|
2893
2928
|
|| fail "cannot reach $server"
|
|
2894
2929
|
rsync -az "$MANIFEST" "$server:$sroot/accounts.json" >>"$ACC_ROOT/sync.log" 2>&1 \
|
|
2895
2930
|
|| fail "manifest push to $server failed"
|
|
2931
|
+
# The pool's MCP registry rides with the manifest; the target's post-sync reconciles it
|
|
2932
|
+
# into every account dir there. A missing local registry leaves the target's alone,
|
|
2933
|
+
# and the machine-local overlay (mcp-servers.local.json, a runner daemon's own view
|
|
2934
|
+
# of THIS Mac) never travels.
|
|
2935
|
+
if [ -f "$ACC_ROOT/mcp-servers.json" ]; then
|
|
2936
|
+
rsync -az "$ACC_ROOT/mcp-servers.json" "$server:$sroot/mcp-servers.json" >>"$ACC_ROOT/sync.log" 2>&1 \
|
|
2937
|
+
|| fail "MCP registry push to $server failed"
|
|
2938
|
+
fi
|
|
2896
2939
|
|
|
2897
2940
|
local id d
|
|
2898
2941
|
for id in $(account_ids); do
|
|
@@ -3064,7 +3107,12 @@ EOF
|
|
|
3064
3107
|
if [ -n "$peers" ]; then
|
|
3065
3108
|
while IFS="$tab" read -r pt pr pp; do
|
|
3066
3109
|
[ -n "$pt" ] || continue
|
|
3067
|
-
|
|
3110
|
+
# </dev/null: the ssh/rsync calls inside read STDIN, and stdin here is the
|
|
3111
|
+
# peer list itself — the first peer's ssh swallowed every later line, so sync
|
|
3112
|
+
# reached gas-mini (first in the list) and never mini-3..mini-8 (peers since
|
|
3113
|
+
# 2026-09-04; the limits fan-out, which reads its list through process
|
|
3114
|
+
# substitution, was the only thing that ever reached them).
|
|
3115
|
+
sync_push_target "$pt" "$pr" "$pp" </dev/null
|
|
3068
3116
|
npeers=$((npeers + 1))
|
|
3069
3117
|
done <<EOF
|
|
3070
3118
|
$peers
|
|
@@ -3079,6 +3127,97 @@ EOF
|
|
|
3079
3127
|
fi
|
|
3080
3128
|
}
|
|
3081
3129
|
|
|
3130
|
+
# MCP servers for EVERY account (lib/mcp_registry.py). `add`/`add-json`/`remove` edit
|
|
3131
|
+
# this pool's registry under the mutate lock, reconcile every account, auto-sync, and
|
|
3132
|
+
# by default (--provider both) make the same change in the sibling provider's pool —
|
|
3133
|
+
# an MCP server is not provider-specific, and the operator wants it in both CLIs.
|
|
3134
|
+
# `list`/`apply`/`import-local` act on this pool alone; the overlay `import-local`
|
|
3135
|
+
# writes is machine-local by definition, so it is applied here and never synced.
|
|
3136
|
+
mcp_sibling_root() { # the other provider's pool root, by the same env precedence
|
|
3137
|
+
if [ "$MULTIACC_PROVIDER" = "codex" ]; then
|
|
3138
|
+
printf '%s\n' "${CLAUDE_ACCOUNTS_ROOT:-${CLAUDE_ACCOUNTS_DIR:-$HOME/.claude-accounts}}"
|
|
3139
|
+
else
|
|
3140
|
+
printf '%s\n' "${CODEX_ACCOUNTS_ROOT:-${CODEX_ACCOUNTS_DIR:-$HOME/.codex-accounts}}"
|
|
3141
|
+
fi
|
|
3142
|
+
}
|
|
3143
|
+
cmd_mcp() {
|
|
3144
|
+
require_manifest
|
|
3145
|
+
local sub="${1:-}" provider=both before_dd=1 a rc=0 sibling sibling_root
|
|
3146
|
+
local args=()
|
|
3147
|
+
case "$sub" in
|
|
3148
|
+
add|add-json|remove|list|apply|import-local) shift ;;
|
|
3149
|
+
"") usage; return 1 ;;
|
|
3150
|
+
*) die "unknown mcp subcommand: $sub (add, add-json, remove, list, apply, import-local)" ;;
|
|
3151
|
+
esac
|
|
3152
|
+
[ -f "$LIB_DIR/mcp_registry.py" ] || die "lib/mcp_registry.py is missing from this install"
|
|
3153
|
+
case "$sub" in
|
|
3154
|
+
list|apply|import-local)
|
|
3155
|
+
"$PYBIN" "$LIB_DIR/mcp_registry.py" --root "$ACC_ROOT" --provider "$MULTIACC_PROVIDER" "$sub" "$@"
|
|
3156
|
+
return $? ;;
|
|
3157
|
+
esac
|
|
3158
|
+
# --provider is OUR option: strip it, but only before a literal `--` — after it every
|
|
3159
|
+
# word belongs to the MCP server's own command line and is passed through untouched.
|
|
3160
|
+
while [ $# -gt 0 ]; do
|
|
3161
|
+
a="$1"
|
|
3162
|
+
if [ "$before_dd" = 1 ]; then
|
|
3163
|
+
case "$a" in
|
|
3164
|
+
--) before_dd=0 ;;
|
|
3165
|
+
--provider)
|
|
3166
|
+
[ $# -ge 2 ] || die "--provider needs a value: claude, codex or both"
|
|
3167
|
+
provider="$2"; shift 2; continue ;;
|
|
3168
|
+
--provider=*) provider="${a#--provider=}"; shift; continue ;;
|
|
3169
|
+
esac
|
|
3170
|
+
fi
|
|
3171
|
+
args+=("$a"); shift
|
|
3172
|
+
done
|
|
3173
|
+
case "$provider" in
|
|
3174
|
+
claude|codex|both) ;;
|
|
3175
|
+
*) die "--provider must be claude, codex or both (got '$provider')" ;;
|
|
3176
|
+
esac
|
|
3177
|
+
if [ "$provider" = "$MULTIACC_PROVIDER" ] || [ "$provider" = both ]; then
|
|
3178
|
+
mutate_lock || die "could not acquire the account lock — try again"
|
|
3179
|
+
trap mutate_unlock EXIT
|
|
3180
|
+
"$PYBIN" "$LIB_DIR/mcp_registry.py" --root "$ACC_ROOT" --provider "$MULTIACC_PROVIDER" "$sub" \
|
|
3181
|
+
${args[@]+"${args[@]}"}
|
|
3182
|
+
rc=$?
|
|
3183
|
+
mutate_unlock
|
|
3184
|
+
trap - EXIT
|
|
3185
|
+
# 3 = the registry WAS saved and some account could not be reconciled (a corrupt
|
|
3186
|
+
# config, a layout the TOML editor refuses): the change still has to reach the
|
|
3187
|
+
# server, the peers and the sibling pool, and `mcp apply` repairs the account once
|
|
3188
|
+
# it is fixed. 1/2 = nothing was saved: stop here.
|
|
3189
|
+
case "$rc" in
|
|
3190
|
+
0) ;;
|
|
3191
|
+
3) printf '%s: registry saved; some accounts were not reconciled — run: %s mcp apply\n' \
|
|
3192
|
+
"$PROVIDER_CLI" "$PROVIDER_CLI" >&2 ;;
|
|
3193
|
+
*) return "$rc" ;;
|
|
3194
|
+
esac
|
|
3195
|
+
auto_sync
|
|
3196
|
+
fi
|
|
3197
|
+
if [ "$provider" != "$MULTIACC_PROVIDER" ]; then
|
|
3198
|
+
if [ "$MULTIACC_PROVIDER" = "codex" ]; then sibling=claude; else sibling=codex; fi
|
|
3199
|
+
sibling_root="$(mcp_sibling_root)"
|
|
3200
|
+
# With the DEFAULT (both), a machine without the sibling pool or CLI has nothing to
|
|
3201
|
+
# mirror, and silently so — a codex-only Mac must not warn on every claude
|
|
3202
|
+
# change. An EXPLICIT --provider codex asked for that pool by name: refuse loudly.
|
|
3203
|
+
if [ ! -f "$sibling_root/accounts.json" ]; then
|
|
3204
|
+
[ "$provider" = both ] && return "$rc"
|
|
3205
|
+
die "no $sibling pool on this machine ($sibling_root/accounts.json is missing)"
|
|
3206
|
+
fi
|
|
3207
|
+
if [ ! -x "$BIN_DIR/$sibling-accounts" ]; then
|
|
3208
|
+
[ "$provider" = both ] && return "$rc"
|
|
3209
|
+
die "$sibling-accounts is not installed beside $PROVIDER_CLI"
|
|
3210
|
+
fi
|
|
3211
|
+
# The caller's no-sync request travels with the hop, in the sibling's spelling.
|
|
3212
|
+
if [ "${CLAUDE_MULTIACC_NO_SYNC:-0}" = "1" ]; then export CODEX_MULTIACC_NO_SYNC=1; fi
|
|
3213
|
+
local sib_rc=0
|
|
3214
|
+
MULTIACC_PROVIDER="$sibling" "$BIN_DIR/$sibling-accounts" mcp "$sub" --provider "$sibling" \
|
|
3215
|
+
${args[@]+"${args[@]}"} || sib_rc=$?
|
|
3216
|
+
[ "$rc" -eq 0 ] && rc="$sib_rc"
|
|
3217
|
+
fi
|
|
3218
|
+
return "$rc"
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3082
3221
|
cmd_post_sync() {
|
|
3083
3222
|
require_manifest
|
|
3084
3223
|
local_pool_fixup
|
|
@@ -3194,11 +3333,13 @@ $shim_out"
|
|
|
3194
3333
|
# distribution for as long as the arg loops rejected the flag. An UNKNOWN verb must still
|
|
3195
3334
|
# FAIL, or the probe stops meaning what it says — so this list must hold exactly the verbs
|
|
3196
3335
|
# the dispatcher below implements, and a test pins that both ways.
|
|
3197
|
-
_KNOWN_VERBS="list status add import export-credential export-cred import-credential import-cred adopt dedupe remove mint login expired relogin re-login sync verify limits post-sync health self-update"
|
|
3336
|
+
_KNOWN_VERBS="list status add import export-credential export-cred import-credential import-cred adopt dedupe remove mint login expired relogin re-login sync verify limits post-sync health self-update mcp"
|
|
3198
3337
|
case " $_KNOWN_VERBS " in
|
|
3199
3338
|
*" ${1:-help} "*)
|
|
3339
|
+
# ...but never past a literal `--`: from there on the words belong to an MCP
|
|
3340
|
+
# server's own command line (`mcp add … -- node s.js -h 127.0.0.1`).
|
|
3200
3341
|
for _arg in "$@"; do
|
|
3201
|
-
case "$_arg" in --help|-h) usage; exit 0 ;; esac
|
|
3342
|
+
case "$_arg" in --) break ;; --help|-h) usage; exit 0 ;; esac
|
|
3202
3343
|
done ;;
|
|
3203
3344
|
esac
|
|
3204
3345
|
|
|
@@ -3224,6 +3365,7 @@ case "${1:-help}" in
|
|
|
3224
3365
|
post-sync) shift; cmd_post_sync "$@" ;;
|
|
3225
3366
|
health) shift; cmd_health "$@" ;;
|
|
3226
3367
|
self-update) shift; cmd_self_update "$@" ;;
|
|
3368
|
+
mcp) shift; cmd_mcp "$@" ;;
|
|
3227
3369
|
help|--help|-h) usage ;;
|
|
3228
3370
|
*) usage; exit 1 ;;
|
|
3229
3371
|
esac
|
package/bin/codex
CHANGED
|
@@ -73,11 +73,112 @@ REAL="$(find_real)" || {
|
|
|
73
73
|
exit 127
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
if [ "$(uname -s)" = "Darwin" ]; then
|
|
77
|
+
file_mtime() { stat -f %m "$1" 2>/dev/null || echo 0; }
|
|
78
|
+
file_sig() { stat -L -f '%m:%z' "$1" 2>/dev/null || echo '?'; }
|
|
79
|
+
else
|
|
80
|
+
file_mtime() { stat -c %Y "$1" 2>/dev/null || echo 0; }
|
|
81
|
+
file_sig() { stat -L -c '%Y:%s' "$1" 2>/dev/null || echo '?'; }
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# ---- MCP servers for every account (lib/mcp_registry.py) -------------------------
|
|
85
|
+
# Codex keeps MCP servers in CODEX_HOME (`<dir>/config.toml`), so under the pool there
|
|
86
|
+
# is one MCP configuration per account — and a stock `codex mcp add` lands in ONE random
|
|
87
|
+
# one. The pool's registry (mcp-servers.json, synced) plus a machine-local overlay
|
|
88
|
+
# (mcp-servers.local.json) are reconciled into the picked account right before exec, so
|
|
89
|
+
# a session gets the same servers whichever account it drew. Bash only on the hot path:
|
|
90
|
+
# python runs when the registry, the overlay or the account's own config changed since
|
|
91
|
+
# the last verified reconcile — the stamp in <dir>/.mcp-applied — and never otherwise.
|
|
92
|
+
# Fail-open throughout: a missing python, lib or a broken registry costs nothing but
|
|
93
|
+
# the servers themselves. Same contract as bin/claude's mcp_ensure.
|
|
94
|
+
MCP_PY="$SELF_DIR/../lib/mcp_registry.py"
|
|
95
|
+
MCP_PYBIN="${CLAUDE_MULTIACC_PYTHON:-python3}"
|
|
96
|
+
mcp_sig() { # $1 acct dir -> "m:z,m:z,m:z" (registry, overlay, account config; - = absent)
|
|
97
|
+
local f out="" s
|
|
98
|
+
for f in "$ACC_ROOT/mcp-servers.json" "$ACC_ROOT/mcp-servers.local.json" "$1/config.toml"; do
|
|
99
|
+
if [ -f "$f" ]; then s="$(file_sig "$f")"; else s="-"; fi
|
|
100
|
+
out="$out${out:+,}$s"
|
|
101
|
+
done
|
|
102
|
+
printf '%s\n' "$out"
|
|
103
|
+
}
|
|
104
|
+
mcp_ensure() { # $1 acct dir — silent, never changes the exit status
|
|
105
|
+
[ "${CODEX_MULTIACC_MCP:-1}" = "0" ] && return 0
|
|
106
|
+
[ -f "$ACC_ROOT/mcp-servers.json" ] || [ -f "$ACC_ROOT/mcp-servers.local.json" ] || return 0
|
|
107
|
+
[ -f "$MCP_PY" ] || return 0
|
|
108
|
+
local stamp=""
|
|
109
|
+
[ -f "$1/.mcp-applied" ] && IFS= read -r stamp < "$1/.mcp-applied" 2>/dev/null
|
|
110
|
+
[ "${stamp:-}" = "$(mcp_sig "$1")" ] && return 0
|
|
111
|
+
"$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider codex apply --account-dir "$1" \
|
|
112
|
+
--fail-open --quiet >/dev/null 2>&1 </dev/null || true
|
|
113
|
+
return 0
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# `codex mcp add|remove` writes ONE account's config.toml — the one it ran under — and
|
|
117
|
+
# nobody else's. So the shim runs it as a child instead of exec-ing it, then mirrors what
|
|
118
|
+
# it wrote into the registry and every other account (`learn` diffs the account against a
|
|
119
|
+
# snapshot taken just before). The real CLI's own parser and writer do the work; the shim
|
|
120
|
+
# only copies the outcome. Exits with the real binary's status; a mirror failure is one
|
|
121
|
+
# hint line. Returns 1 when this run is not such a command.
|
|
122
|
+
mcp_mirror_run() { # $1 acct dir, then the user's argv — exits when it handled the run
|
|
123
|
+
local d="$1" snap="" rc
|
|
124
|
+
shift
|
|
125
|
+
[ "${1:-}" = "mcp" ] || return 1
|
|
126
|
+
case "${2:-}" in add|remove) ;; *) return 1 ;; esac
|
|
127
|
+
[ "${CODEX_MULTIACC_MCP:-1}" != "0" ] && [ -f "$MCP_PY" ] || return 1
|
|
128
|
+
mcp_ensure "$d"
|
|
129
|
+
snap="$(mktemp "${TMPDIR:-/tmp}/codex-mcp-snap.XXXXXX" 2>/dev/null)" || snap=""
|
|
130
|
+
if [ -n "$snap" ] && ! "$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider codex snapshot \
|
|
131
|
+
--account-dir "$d" > "$snap" 2>/dev/null </dev/null; then
|
|
132
|
+
rm -f "$snap" 2>/dev/null; snap=""
|
|
133
|
+
fi
|
|
134
|
+
"$REAL" "$@"
|
|
135
|
+
rc=$?
|
|
136
|
+
if [ "$rc" -eq 0 ] && [ -n "$snap" ]; then
|
|
137
|
+
"$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider codex learn --account-dir "$d" \
|
|
138
|
+
--before "$snap" </dev/null \
|
|
139
|
+
|| printf 'codex-multiacc: could not mirror the MCP change to the other accounts — run: codex-accounts mcp apply\n' >&2
|
|
140
|
+
elif [ "$rc" -eq 0 ]; then
|
|
141
|
+
printf 'codex-multiacc: could not mirror the MCP change to the other accounts — run: codex-accounts mcp apply\n' >&2
|
|
142
|
+
fi
|
|
143
|
+
[ -n "$snap" ] && rm -f "$snap" 2>/dev/null
|
|
144
|
+
exit "$rc"
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# A pooled SESSION exports CODEX_HOME=<acct> and CODEX_SHIM_ACTIVE=1, and every child
|
|
148
|
+
# process inherits both — so a `codex mcp add` run from INSIDE a session (an agent's
|
|
149
|
+
# Bash tool, `npx appinspire-mcp install` run by an agent) arrives at the fast passthrough
|
|
150
|
+
# below with the account already chosen, not at the selection path where the mirror
|
|
151
|
+
# lives. That is the very write the 2026-09-22 outage was made of, in the place MCP
|
|
152
|
+
# servers are most often installed. The account is known, so the same mirror applies:
|
|
153
|
+
# exits inside when it handled the run; anything else stays byte-identical passthrough.
|
|
154
|
+
mcp_nested_mirror() {
|
|
155
|
+
[ -f "$MANIFEST" ] || return 1
|
|
156
|
+
[ "${CODEX_MULTIACC_DISABLE:-0}" = "1" ] && return 1
|
|
157
|
+
[ "${CODEX_MULTIACC_MCP:-1}" = "0" ] && return 1
|
|
158
|
+
[ -n "${CODEX_HOME:-}" ] || return 1
|
|
159
|
+
local verb="${1:-}" cfg="${CODEX_HOME%/}" d=""
|
|
160
|
+
[ "$verb" = "mcp" ] || return 1
|
|
161
|
+
# A DIRECT child of the pool root, by its given spelling or its physical one (an
|
|
162
|
+
# adopted account is a symlink whose target is the operator's ~/.claude).
|
|
163
|
+
case "$cfg" in
|
|
164
|
+
"$ACC_ROOT"/acct-*) case "${cfg#"$ACC_ROOT"/}" in */*) ;; *) d="$cfg" ;; esac ;;
|
|
165
|
+
esac
|
|
166
|
+
if [ -z "$d" ]; then
|
|
167
|
+
local root_c; root_c="$(canon_path "$ACC_ROOT")"
|
|
168
|
+
case "$(canon_path "$cfg")" in
|
|
169
|
+
"$root_c"/acct-*) case "${cfg##*/}" in acct-*) d="$cfg" ;; esac ;;
|
|
170
|
+
esac
|
|
171
|
+
fi
|
|
172
|
+
[ -n "$d" ] && [ -d "$d" ] || return 1
|
|
173
|
+
mcp_mirror_run "$d" "$@"
|
|
174
|
+
}
|
|
175
|
+
|
|
76
176
|
# Fast passthrough: caller pinned a config dir, addon disabled, recursion guard, or
|
|
77
177
|
# no account data yet. Byte-identical behavior to stock codex.
|
|
78
178
|
if [ -n "${CODEX_HOME:-}" ] \
|
|
79
179
|
|| [ "${CODEX_MULTIACC_DISABLE:-0}" = "1" ] || [ -n "${CODEX_SHIM_ACTIVE:-}" ] \
|
|
80
180
|
|| [ ! -f "$MANIFEST" ]; then
|
|
181
|
+
mcp_nested_mirror "$@" || true
|
|
81
182
|
exec "$REAL" "$@"
|
|
82
183
|
fi
|
|
83
184
|
|
|
@@ -94,13 +195,6 @@ fi
|
|
|
94
195
|
# complaining to stderr.
|
|
95
196
|
case "$CODEX_MULTIACC_THRESHOLD" in ''|*[!0-9]*|??????*) CODEX_MULTIACC_THRESHOLD=90 ;; esac
|
|
96
197
|
|
|
97
|
-
if [ "$(uname -s)" = "Darwin" ]; then
|
|
98
|
-
file_mtime() { stat -f %m "$1" 2>/dev/null || echo 0; }
|
|
99
|
-
else
|
|
100
|
-
file_mtime() { stat -c %Y "$1" 2>/dev/null || echo 0; }
|
|
101
|
-
fi
|
|
102
|
-
|
|
103
|
-
|
|
104
198
|
# A number this shim will do ARITHMETIC on: digits only, and short enough that bash
|
|
105
199
|
# cannot go out of range. An over-range value makes `[ x -lt y ]` print
|
|
106
200
|
# "integer expression expected" on stderr — which a service-spawned run must never see —
|
|
@@ -645,6 +739,10 @@ if [ -n "${CODEX_ACCOUNT:-}" ]; then
|
|
|
645
739
|
share_state_index "$d"
|
|
646
740
|
export CODEX_HOME="$d"
|
|
647
741
|
export CODEX_SHIM_ACTIVE=1
|
|
742
|
+
# A pinned account carries every registered MCP server too, and a pinned
|
|
743
|
+
# `codex mcp add|remove` is mirrored like any other (exits inside).
|
|
744
|
+
mcp_ensure "$d"
|
|
745
|
+
mcp_mirror_run "$d" "$@" || true
|
|
648
746
|
exec "$REAL" "$@"
|
|
649
747
|
fi
|
|
650
748
|
sel_log "pin-invalid account=$CODEX_ACCOUNT (no such dir; random fallback)"
|
|
@@ -874,6 +972,8 @@ pick="$PICK_DIR"
|
|
|
874
972
|
# CODEX_ACCOUNT pin deliberately does not: a pin is a caller overriding selection,
|
|
875
973
|
# not a turn in the rotation.
|
|
876
974
|
remember_pick "$pick"
|
|
975
|
+
# The picked account carries every registered MCP server before the client starts.
|
|
976
|
+
mcp_ensure "$pick"
|
|
877
977
|
|
|
878
978
|
# Opportunistic limits refresh: non-blocking, throttled, backgrounded. The windows are
|
|
879
979
|
# deliberately wide (10m, matching the 5m scheduled pass): the usage endpoint rate-limits
|
|
@@ -902,6 +1002,14 @@ sel_log "$acct weekly=$(fresh_field "$pick" weekly_percent || echo '?')%" \
|
|
|
902
1002
|
|
|
903
1003
|
export CODEX_SHIM_ACTIVE=1
|
|
904
1004
|
|
|
1005
|
+
# A `codex mcp add|remove` is run as a child under the picked account and mirrored to
|
|
1006
|
+
# every other one (exits inside); anything else falls through to exec.
|
|
1007
|
+
if [ "${1:-}" = "mcp" ]; then
|
|
1008
|
+
share_state_index "$pick"
|
|
1009
|
+
export CODEX_HOME="$pick"
|
|
1010
|
+
mcp_mirror_run "$pick" "$@" || true
|
|
1011
|
+
fi
|
|
1012
|
+
|
|
905
1013
|
# Auto-retry applies only to `codex exec` runs with an alternative account available,
|
|
906
1014
|
# and only when stdin is finite (tty, regular file, or char device like /dev/null).
|
|
907
1015
|
# A service-spawned pipe that never EOFs must take the plain exec path, or the
|
|
@@ -1033,6 +1141,7 @@ while :; do
|
|
|
1033
1141
|
# The account that actually serves the work is the one the next run should rotate
|
|
1034
1142
|
# away from — not the one that bounced.
|
|
1035
1143
|
remember_pick "$cur"
|
|
1144
|
+
mcp_ensure "$cur"
|
|
1036
1145
|
attempt=2
|
|
1037
1146
|
continue
|
|
1038
1147
|
fi
|