claude-multiacc 2.0.59 → 2.0.61
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 +19 -1
- package/bin/claude +78 -11
- package/bin/claude-accounts +3 -0
- package/bin/codex +65 -7
- package/bin/codex-accounts +2 -0
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_dispatch.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_receipt.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_handoff.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_receipt.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_autoresume_mode.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_trust.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_mirror.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_oauth.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
- package/lib/__pycache__/model_catalog.cpython-312.pyc +0 -0
- package/lib/__pycache__/model_match.cpython-312.pyc +0 -0
- package/lib/__pycache__/report.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_funding.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__/session_settings.cpython-312.pyc +0 -0
- package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
- package/lib/common.sh +15 -0
- package/lib/mcp_mirror.py +49 -0
- package/lib/mcp_mirror.sh +47 -0
- package/lib/mcp_registry.py +237 -10
- package/package.json +2 -2
- package/tests/__pycache__/autoresume_cross_receipt_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_unit_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_tmux_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog_cli.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_records.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_trust.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_guards.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_holders.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_origin.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_policy.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_receipt.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tokens.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cursor.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_argv.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_history.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_origins.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_mode.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_receipt.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_settings.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_trust_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_fetch.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/__pycache__/test_mcp_mirror.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_mcp_mirror_args.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_mcp_oauth.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_mcp_registry.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
- package/tests/run-tests.sh +146 -1
- package/tests/test_mcp_mirror.py +148 -0
- package/tests/test_mcp_mirror_args.py +129 -0
- package/tests/test_mcp_registry.py +234 -3
package/README.md
CHANGED
|
@@ -568,7 +568,25 @@ claude-accounts mcp apply # re-apply now (repair drift)
|
|
|
568
568
|
outcome into the registry and every other account (`learn`). This includes a command run
|
|
569
569
|
from **inside** a pooled session (an agent's Bash tool, `npx appinspire-mcp install` run by
|
|
570
570
|
an agent): the session's config dir is a pool account, so the same mirror applies. A
|
|
571
|
-
config dir outside the pool is plain passthrough.
|
|
571
|
+
config dir outside the pool is plain passthrough. Global options in front of `mcp` are
|
|
572
|
+
looked past the way the real parser does (`alias claude='claude
|
|
573
|
+
--dangerously-skip-permissions'` puts one before every typed `claude mcp add`; until
|
|
574
|
+
2026-10-22 that kept motionsites in 2 of 11 accounts). Mirroring is limited to the server
|
|
575
|
+
named by the command, including its project approvals: a concurrent runner update to
|
|
576
|
+
another server cannot become a synced override or tombstone. An unrecognized command
|
|
577
|
+
option leaves the native command intact and warns that its change was not mirrored.
|
|
578
|
+
Claude's desktop bulk import mirrors changes except names and retirements belonging to
|
|
579
|
+
another machine-local overlay owner; an explicit named command can still override one.
|
|
580
|
+
- **Adoption**: whatever reached an account by another route — the real binary run
|
|
581
|
+
directly, a hand edit, another tool's installer — is adopted by the next reconcile: a
|
|
582
|
+
server (user scope or a project entry) or a project's `.mcp.json` approval that no section
|
|
583
|
+
names, live or retired, joins the registry (a replica: its overlay) and so every account.
|
|
584
|
+
Several definitions of one name: the one most accounts hold wins, then the newest config
|
|
585
|
+
file. Every limits pass reconciles all accounts this way, so an idle account catches up too.
|
|
586
|
+
Never adopted: anything the pool itself once wrote into that account (`<acct>/.mcp-managed.json`
|
|
587
|
+
— so a stray synced entry dropped without a tombstone, or a vanished overlay owner's
|
|
588
|
+
server, stays out of the synced registry), tombstoned names, and nothing at all with
|
|
589
|
+
`CLAUDE_MULTIACC_MCP_ADOPT=0` / `CODEX_MULTIACC_MCP_ADOPT=0`.
|
|
572
590
|
- **Runner-managed servers**: on app-robot fleet Macs the runner publishes `appinspire-mcp` into
|
|
573
591
|
each Mac's machine-local overlay with that Mac's own paths. Never `mcp add` it to the synced
|
|
574
592
|
registry (directly, through a mirrored `claude|codex mcp add`, or via `appinspire-mcp install`):
|
package/bin/claude
CHANGED
|
@@ -167,15 +167,83 @@ mcp_oauth_ensure() { # $1 acct dir — silent, never changes the exit status
|
|
|
167
167
|
# account against a snapshot taken just before). The real CLI's own parser and writer do
|
|
168
168
|
# the work; the shim only copies the outcome. Exits with the real binary's status; a
|
|
169
169
|
# mirror failure is one hint line. Returns 1 when this run is not such a command.
|
|
170
|
+
# The index of the first positional argument as the real CLI's parser (commander) finds
|
|
171
|
+
# it: global options may come first, in any number. The operator's `alias claude='claude
|
|
172
|
+
# --dangerously-skip-permissions'` puts one in front of every `claude mcp add` typed at a
|
|
173
|
+
# prompt, and the scoped-limit fallback prepends `--model X`; the mirror once looked at $1
|
|
174
|
+
# alone, so on 2026-10-22 motionsites (and playwright, inspo) sat in the one account a
|
|
175
|
+
# typed add ran under. A value option skips its value; an optional-value one the next
|
|
176
|
+
# word unless it is an option; a variadic one every word up to the next option. `--`, or
|
|
177
|
+
# a positional after `--` counts (commander's own rule). No positional: status 1. Options
|
|
178
|
+
# of claude 2.1.308 (`claude --help`; hidden ones left out on purpose): an unknown one
|
|
179
|
+
# counts as a flag — at worst its value is taken for the positional, which is then no
|
|
180
|
+
# `mcp`, the run is not mirrored, and the registry's adoption picks the server up instead.
|
|
181
|
+
CLI_VALUE_OPTS=" --agent --agents --append-system-prompt --append-system-prompt-file \
|
|
182
|
+
--autocompact --debug-file --effort --environment --fallback-model --input-format \
|
|
183
|
+
--json-schema --max-budget-usd --model -n --name --output-format --permission-mode \
|
|
184
|
+
--permission-prompts --permission-prompt-tool --plugin-dir --plugin-url \
|
|
185
|
+
--remote-control-session-name-prefix --session-id --setting-sources --settings \
|
|
186
|
+
--system-prompt --system-prompt-file --system-prompt-snapshot "
|
|
187
|
+
CLI_OPTIONAL_OPTS=" -d --debug --cloud --from-pr --prompt-suggestions --remote-control --rc \
|
|
188
|
+
--remote -r --resume --teleport -w --worktree "
|
|
189
|
+
CLI_VARIADIC_OPTS=" --add-dir --allowedTools --allowed-tools --betas --disallowedTools \
|
|
190
|
+
--disallowed-tools --file --mcp-config --tools "
|
|
191
|
+
cli_positional_at() { # argv -> prints the index of its first positional argument
|
|
192
|
+
local i=1 a nx
|
|
193
|
+
while [ "$i" -le "$#" ]; do
|
|
194
|
+
a="${!i}"
|
|
195
|
+
case "$a" in
|
|
196
|
+
--) i=$((i + 1)); [ "$i" -le "$#" ] || return 1; printf '%s\n' "$i"; return 0 ;;
|
|
197
|
+
--*=*) i=$((i + 1)); continue ;;
|
|
198
|
+
-?*) ;;
|
|
199
|
+
*) printf '%s\n' "$i"; return 0 ;;
|
|
200
|
+
esac
|
|
201
|
+
i=$((i + 1))
|
|
202
|
+
case "$CLI_VALUE_OPTS" in *" $a "*) i=$((i + 1)); continue ;; esac
|
|
203
|
+
case "$CLI_OPTIONAL_OPTS" in
|
|
204
|
+
*" $a "*) nx="${!i:-}"; case "$nx" in ''|-*) ;; *) i=$((i + 1)) ;; esac; continue ;;
|
|
205
|
+
esac
|
|
206
|
+
case "$CLI_VARIADIC_OPTS" in
|
|
207
|
+
*" $a "*)
|
|
208
|
+
while [ "$i" -le "$#" ]; do
|
|
209
|
+
nx="${!i}"; case "$nx" in -*) break ;; esac; i=$((i + 1))
|
|
210
|
+
done ;;
|
|
211
|
+
esac
|
|
212
|
+
done
|
|
213
|
+
return 1
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# Whether argv is `claude [global options] mcp add|add-json|add-from-claude-desktop|remove …`.
|
|
217
|
+
# The exact word -p/--print (or --bg/--background) anywhere before a `--` puts the client in
|
|
218
|
+
# print/background mode, which never runs the subcommand.
|
|
219
|
+
mcp_mutation_argv() {
|
|
220
|
+
local i j a
|
|
221
|
+
for a in "$@"; do
|
|
222
|
+
case "$a" in --) break ;; -p|--print|--bg|--background) return 1 ;; esac
|
|
223
|
+
done
|
|
224
|
+
i="$(cli_positional_at "$@")" || return 1
|
|
225
|
+
[ "${!i}" = "mcp" ] || return 1
|
|
226
|
+
j=$((i + 1))
|
|
227
|
+
case "${!j:-}" in add|add-json|add-from-claude-desktop|remove) MCP_MUTATION_AT="$j"; return 0 ;; esac
|
|
228
|
+
return 1
|
|
229
|
+
}
|
|
230
|
+
|
|
170
231
|
mcp_mirror_run() { # $1 acct dir, then the user's argv — exits when it handled the run
|
|
171
|
-
local d="$1" snap="" rc
|
|
232
|
+
local d="$1" snap="" rc parse_rc name="" mirror_args=()
|
|
172
233
|
shift
|
|
173
|
-
|
|
174
|
-
# CLI accepts in front of any subcommand): look past it.
|
|
175
|
-
if [ "$verb" = "--model" ]; then verb="${3:-}"; sub="${4:-}"; fi
|
|
176
|
-
[ "$verb" = "mcp" ] || return 1
|
|
177
|
-
case "$sub" in add|add-json|remove) ;; *) return 1 ;; esac
|
|
234
|
+
mcp_mutation_argv "$@" || return 1
|
|
178
235
|
[ "${CLAUDE_MULTIACC_MCP:-1}" != "0" ] && [ -f "$MCP_PY" ] || return 1
|
|
236
|
+
if [ "${!MCP_MUTATION_AT}" != add-from-claude-desktop ]; then
|
|
237
|
+
if name="$(. "$SELF_DIR/../lib/mcp_mirror.sh" && mcp_mutation_name claude "${@:$MCP_MUTATION_AT}")"; then
|
|
238
|
+
mirror_args=("--name=$name")
|
|
239
|
+
else
|
|
240
|
+
parse_rc=$?
|
|
241
|
+
"$REAL" "$@"; rc=$?
|
|
242
|
+
[ "$rc" -ne 0 ] || [ "$parse_rc" = 2 ] \
|
|
243
|
+
|| printf 'claude-multiacc: could not identify the MCP target; change not mirrored\n' >&2
|
|
244
|
+
exit "$rc"
|
|
245
|
+
fi
|
|
246
|
+
fi
|
|
179
247
|
mcp_ensure "$d"
|
|
180
248
|
snap="$(mktemp "${TMPDIR:-/tmp}/claude-mcp-snap.XXXXXX" 2>/dev/null)" || snap=""
|
|
181
249
|
if [ -n "$snap" ] && ! "$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider claude snapshot \
|
|
@@ -186,7 +254,7 @@ mcp_mirror_run() { # $1 acct dir, then the user's argv — exits when it handled
|
|
|
186
254
|
rc=$?
|
|
187
255
|
if [ "$rc" -eq 0 ] && [ -n "$snap" ]; then
|
|
188
256
|
"$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider claude learn --account-dir "$d" \
|
|
189
|
-
--before "$snap" --project "$(pwd -P 2>/dev/null || pwd)" </dev/null \
|
|
257
|
+
--before "$snap" --project "$(pwd -P 2>/dev/null || pwd)" ${mirror_args[@]+"${mirror_args[@]}"} </dev/null \
|
|
190
258
|
|| printf 'claude-multiacc: could not mirror the MCP change to the other accounts — run: claude-accounts mcp apply\n' >&2
|
|
191
259
|
elif [ "$rc" -eq 0 ]; then
|
|
192
260
|
printf 'claude-multiacc: could not mirror the MCP change to the other accounts — run: claude-accounts mcp apply\n' >&2
|
|
@@ -207,9 +275,8 @@ mcp_nested_mirror() {
|
|
|
207
275
|
[ "${CLAUDE_MULTIACC_DISABLE:-0}" = "1" ] && return 1
|
|
208
276
|
[ "${CLAUDE_MULTIACC_MCP:-1}" = "0" ] && return 1
|
|
209
277
|
[ -n "${CLAUDE_CONFIG_DIR:-}" ] || return 1
|
|
210
|
-
local
|
|
211
|
-
|
|
212
|
-
[ "$verb" = "mcp" ] || return 1
|
|
278
|
+
local cfg="${CLAUDE_CONFIG_DIR%/}" d=""
|
|
279
|
+
mcp_mutation_argv "$@" || return 1
|
|
213
280
|
# A DIRECT child of the pool root, by its given spelling or its physical one (an
|
|
214
281
|
# adopted account is a symlink whose target is the operator's ~/.claude).
|
|
215
282
|
case "$cfg" in
|
|
@@ -2522,7 +2589,7 @@ export CLAUDE_SHIM_ACTIVE=1
|
|
|
2522
2589
|
|
|
2523
2590
|
# A `claude mcp add|add-json|remove` is run as a child under the picked account and
|
|
2524
2591
|
# mirrored to every other one (exits inside); anything else falls through to exec.
|
|
2525
|
-
if
|
|
2592
|
+
if mcp_mutation_argv "$@"; then
|
|
2526
2593
|
export CLAUDE_CONFIG_DIR="$pick"
|
|
2527
2594
|
tok="$PICK_TOKEN"
|
|
2528
2595
|
[ -n "$tok" ] && export CLAUDE_CODE_OAUTH_TOKEN="$tok"
|
package/bin/claude-accounts
CHANGED
|
@@ -2963,6 +2963,9 @@ PYEOF
|
|
|
2963
2963
|
# lock while a consumer reads its output.
|
|
2964
2964
|
limits_lock_release
|
|
2965
2965
|
trap - EXIT
|
|
2966
|
+
# Every account's MCP servers, adopting what one holds that the registry does not know
|
|
2967
|
+
# (lib/common.sh) — before the push below, which carries the registry to the peers.
|
|
2968
|
+
mcp_registry_pass
|
|
2966
2969
|
# Whoever just refreshed is the only machine that CAN — pass it on before the
|
|
2967
2970
|
# report, so a blind peer stops picking drained accounts within one cycle.
|
|
2968
2971
|
limits_distribute
|
package/bin/codex
CHANGED
|
@@ -119,12 +119,70 @@ mcp_ensure() { # $1 acct dir — silent, never changes the exit status
|
|
|
119
119
|
# snapshot taken just before). The real CLI's own parser and writer do the work; the shim
|
|
120
120
|
# only copies the outcome. Exits with the real binary's status; a mirror failure is one
|
|
121
121
|
# hint line. Returns 1 when this run is not such a command.
|
|
122
|
+
# The index of the first positional argument as the real CLI's parser (clap) finds it:
|
|
123
|
+
# global options may come first, in any number. The operator's `alias codex='codex
|
|
124
|
+
# --dangerously-bypass-approvals-and-sandbox'` puts one in front of every `codex mcp add`
|
|
125
|
+
# typed at a prompt, and the mirror once looked at $1 alone (bin/claude met the same on
|
|
126
|
+
# 2026-10-22). A value option skips its value (attached `-mX`/`--model=X` is one word); a
|
|
127
|
+
# variadic one every word up to the next option. `--`, or no positional: status 1.
|
|
128
|
+
# Options of codex-cli 0.163 (`codex --help`); an unknown one counts as a flag.
|
|
129
|
+
CLI_VALUE_OPTS=" -c --config --enable --disable --remote --remote-auth-token-env -m --model \
|
|
130
|
+
--local-provider -p --profile -s --sandbox -C --cd --add-dir -a --ask-for-approval "
|
|
131
|
+
CLI_VARIADIC_OPTS=" -i --image "
|
|
132
|
+
cli_positional_at() { # argv -> prints the index of its first positional argument
|
|
133
|
+
local i=1 a nx
|
|
134
|
+
while [ "$i" -le "$#" ]; do
|
|
135
|
+
a="${!i}"
|
|
136
|
+
case "$a" in
|
|
137
|
+
--) return 1 ;;
|
|
138
|
+
--*=*) i=$((i + 1)); continue ;;
|
|
139
|
+
-?*) ;;
|
|
140
|
+
*) printf '%s\n' "$i"; return 0 ;;
|
|
141
|
+
esac
|
|
142
|
+
i=$((i + 1))
|
|
143
|
+
case "$CLI_VALUE_OPTS" in *" $a "*) i=$((i + 1)); continue ;; esac
|
|
144
|
+
case "$CLI_VARIADIC_OPTS" in
|
|
145
|
+
*" $a "*)
|
|
146
|
+
while [ "$i" -le "$#" ]; do
|
|
147
|
+
nx="${!i}"; case "$nx" in -*) break ;; esac; i=$((i + 1))
|
|
148
|
+
done ;;
|
|
149
|
+
esac
|
|
150
|
+
done
|
|
151
|
+
return 1
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Whether argv is `codex [global options] mcp add|remove …`.
|
|
155
|
+
mcp_mutation_argv() {
|
|
156
|
+
local i j
|
|
157
|
+
i="$(cli_positional_at "$@")" || return 1
|
|
158
|
+
[ "${!i}" = "mcp" ] || return 1
|
|
159
|
+
j=$((i + 1))
|
|
160
|
+
# `mcp` takes options of its own before its subcommand: -c/--config, --enable, --disable
|
|
161
|
+
while :; do
|
|
162
|
+
case "${!j:-}" in
|
|
163
|
+
-c|--config|--enable|--disable) j=$((j + 2)) ;;
|
|
164
|
+
--*=*|-c?*) j=$((j + 1)) ;;
|
|
165
|
+
*) break ;;
|
|
166
|
+
esac
|
|
167
|
+
done
|
|
168
|
+
case "${!j:-}" in add|remove) MCP_MUTATION_AT="$j"; return 0 ;; esac
|
|
169
|
+
return 1
|
|
170
|
+
}
|
|
171
|
+
|
|
122
172
|
mcp_mirror_run() { # $1 acct dir, then the user's argv — exits when it handled the run
|
|
123
|
-
local d="$1" snap="" rc
|
|
173
|
+
local d="$1" snap="" rc parse_rc name=""
|
|
124
174
|
shift
|
|
125
|
-
|
|
126
|
-
case "${2:-}" in add|remove) ;; *) return 1 ;; esac
|
|
175
|
+
mcp_mutation_argv "$@" || return 1
|
|
127
176
|
[ "${CODEX_MULTIACC_MCP:-1}" != "0" ] && [ -f "$MCP_PY" ] || return 1
|
|
177
|
+
if name="$(. "$SELF_DIR/../lib/mcp_mirror.sh" && mcp_mutation_name codex "${@:$MCP_MUTATION_AT}")"; then
|
|
178
|
+
:
|
|
179
|
+
else
|
|
180
|
+
parse_rc=$?
|
|
181
|
+
"$REAL" "$@"; rc=$?
|
|
182
|
+
[ "$rc" -ne 0 ] || [ "$parse_rc" = 2 ] \
|
|
183
|
+
|| printf 'codex-multiacc: could not identify the MCP target; change not mirrored\n' >&2
|
|
184
|
+
exit "$rc"
|
|
185
|
+
fi
|
|
128
186
|
mcp_ensure "$d"
|
|
129
187
|
snap="$(mktemp "${TMPDIR:-/tmp}/codex-mcp-snap.XXXXXX" 2>/dev/null)" || snap=""
|
|
130
188
|
if [ -n "$snap" ] && ! "$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider codex snapshot \
|
|
@@ -135,7 +193,7 @@ mcp_mirror_run() { # $1 acct dir, then the user's argv — exits when it handled
|
|
|
135
193
|
rc=$?
|
|
136
194
|
if [ "$rc" -eq 0 ] && [ -n "$snap" ]; then
|
|
137
195
|
"$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider codex learn --account-dir "$d" \
|
|
138
|
-
--before "$snap" </dev/null \
|
|
196
|
+
--before "$snap" "--name=$name" </dev/null \
|
|
139
197
|
|| printf 'codex-multiacc: could not mirror the MCP change to the other accounts — run: codex-accounts mcp apply\n' >&2
|
|
140
198
|
elif [ "$rc" -eq 0 ]; then
|
|
141
199
|
printf 'codex-multiacc: could not mirror the MCP change to the other accounts — run: codex-accounts mcp apply\n' >&2
|
|
@@ -156,8 +214,8 @@ mcp_nested_mirror() {
|
|
|
156
214
|
[ "${CODEX_MULTIACC_DISABLE:-0}" = "1" ] && return 1
|
|
157
215
|
[ "${CODEX_MULTIACC_MCP:-1}" = "0" ] && return 1
|
|
158
216
|
[ -n "${CODEX_HOME:-}" ] || return 1
|
|
159
|
-
local
|
|
160
|
-
|
|
217
|
+
local cfg="${CODEX_HOME%/}" d=""
|
|
218
|
+
mcp_mutation_argv "$@" || return 1
|
|
161
219
|
# A DIRECT child of the pool root, by its given spelling or its physical one (an
|
|
162
220
|
# adopted account is a symlink whose target is the operator's ~/.claude).
|
|
163
221
|
case "$cfg" in
|
|
@@ -1526,7 +1584,7 @@ export CODEX_SHIM_ACTIVE=1
|
|
|
1526
1584
|
|
|
1527
1585
|
# A `codex mcp add|remove` is run as a child under the picked account and mirrored to
|
|
1528
1586
|
# every other one (exits inside); anything else falls through to exec.
|
|
1529
|
-
if
|
|
1587
|
+
if mcp_mutation_argv "$@"; then
|
|
1530
1588
|
share_state_index "$pick"
|
|
1531
1589
|
export CODEX_HOME="$pick"
|
|
1532
1590
|
mcp_mirror_run "$pick" "$@" || true
|
package/bin/codex-accounts
CHANGED
|
@@ -1846,6 +1846,8 @@ PYEOF
|
|
|
1846
1846
|
# lock while a consumer reads its output.
|
|
1847
1847
|
codex_limits_lock_release
|
|
1848
1848
|
trap - EXIT
|
|
1849
|
+
# Every account's MCP servers, adopting what one holds that the registry does not know.
|
|
1850
|
+
mcp_registry_pass
|
|
1849
1851
|
# This pool's model catalog, once its copy is an hour old (lib/common.sh).
|
|
1850
1852
|
model_catalog_backstop
|
|
1851
1853
|
# The refresher fails open per account; a NON-zero status means the pass itself
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/common.sh
CHANGED
|
@@ -771,6 +771,21 @@ seed_mcp_registry() { # $1 = acct dir
|
|
|
771
771
|
return 0
|
|
772
772
|
}
|
|
773
773
|
|
|
774
|
+
# Once per scheduled limits pass: reconcile EVERY account, which first adopts what any
|
|
775
|
+
# one of them holds that the registry does not know (lib/mcp_registry.py adopt) — the
|
|
776
|
+
# backstop for a server added where the shims' mirror never saw it, reaching accounts no
|
|
777
|
+
# session launched on since. Unlike the shims' pre-exec apply it also runs before this
|
|
778
|
+
# pool has a registry at all. Inline, quiet, fail-open: a pass never dies on it.
|
|
779
|
+
mcp_registry_pass() {
|
|
780
|
+
local off
|
|
781
|
+
if [ "$MULTIACC_PROVIDER" = "codex" ]; then off="${CODEX_MULTIACC_MCP:-1}"; else off="${CLAUDE_MULTIACC_MCP:-1}"; fi
|
|
782
|
+
[ "$off" = "0" ] && return 0
|
|
783
|
+
[ -f "$LIB_DIR/mcp_registry.py" ] || return 0
|
|
784
|
+
"$PYBIN" "$LIB_DIR/mcp_registry.py" --root "$ACC_ROOT" --provider "$MULTIACC_PROVIDER" apply \
|
|
785
|
+
--all --fail-open --quiet >/dev/null 2>&1 </dev/null || true
|
|
786
|
+
return 0
|
|
787
|
+
}
|
|
788
|
+
|
|
774
789
|
seed_account_dir() { # $1 = acct dir
|
|
775
790
|
local d="$1"
|
|
776
791
|
mkdir -p "$d"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Keep stock MCP mutations scoped to the server the command actually named."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _external_names(overlay, target_owner):
|
|
7
|
+
"""Bulk imports cannot promote another local owner's machine-specific entries."""
|
|
8
|
+
user, everywhere, projects = set(), set(), {}
|
|
9
|
+
for owner, section in overlay.get("owners", {}).items():
|
|
10
|
+
if owner == target_owner:
|
|
11
|
+
continue
|
|
12
|
+
everywhere.update(section.get("retired") or [])
|
|
13
|
+
user.update(section.get("mcpServers") or {})
|
|
14
|
+
user.update(section.get("retiredUser") or [])
|
|
15
|
+
for path, entry in (section.get("projects") or {}).items():
|
|
16
|
+
names = projects.setdefault(path, set())
|
|
17
|
+
names.update(entry.get("mcpServers") or {})
|
|
18
|
+
names.update(entry.get("retired") or [])
|
|
19
|
+
names.update(entry.get("enabledMcpjsonServers") or [])
|
|
20
|
+
return user | everywhere, everywhere, projects
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _filtered_snapshot(snapshot, keep_user, keep_project):
|
|
24
|
+
result = copy.deepcopy(snapshot)
|
|
25
|
+
result["user"] = {name: block for name, block in (result.get("user") or {}).items()
|
|
26
|
+
if keep_user(name)}
|
|
27
|
+
for path, entry in (result.get("projects") or {}).items():
|
|
28
|
+
entry["mcpServers"] = {name: block for name, block in (entry.get("mcpServers") or {}).items()
|
|
29
|
+
if keep_project(path, name)}
|
|
30
|
+
for key in ("enabledMcpjsonServers", "disabledMcpjsonServers"):
|
|
31
|
+
entry[key] = [name for name in (entry.get(key) or []) if keep_project(path, name)]
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def scoped_snapshots(before, after, name, overlay, target_owner=None):
|
|
36
|
+
"""Named commands mirror only their target, including project approvals.
|
|
37
|
+
|
|
38
|
+
Without a name (desktop bulk import), externally owned names are excluded even
|
|
39
|
+
if a delayed reconcile writes an older generation than the current overlay.
|
|
40
|
+
"""
|
|
41
|
+
if name is not None:
|
|
42
|
+
keep_user = lambda candidate: candidate == name
|
|
43
|
+
keep_project = lambda _path, candidate: candidate == name
|
|
44
|
+
else:
|
|
45
|
+
user, everywhere, projects = _external_names(overlay, target_owner)
|
|
46
|
+
keep_user = lambda candidate: candidate not in user
|
|
47
|
+
keep_project = lambda path, candidate: candidate not in everywhere | projects.get(path, set())
|
|
48
|
+
return (_filtered_snapshot(before, keep_user, keep_project),
|
|
49
|
+
_filtered_snapshot(after, keep_user, keep_project))
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Target of a stock MCP mutation. Global CLI options are already skipped by the shim.
|
|
2
|
+
# Unknown options fail closed: the native command still runs, but is not mirrored.
|
|
3
|
+
# Status 2 is a help request before the target; pass it through without a warning.
|
|
4
|
+
mcp_mutation_name() { # provider, verb, then the verb's argv -> server name
|
|
5
|
+
local provider="$1" verb="$2" values="" variadic="" flags=" " a opt attached
|
|
6
|
+
shift 2
|
|
7
|
+
case "$provider:$verb" in
|
|
8
|
+
claude:add)
|
|
9
|
+
values=" -s --scope -t --transport --callback-port --client-id "
|
|
10
|
+
variadic=" -e --env -H --header "; flags="$flags--client-secret " ;;
|
|
11
|
+
claude:add-json) values=" -s --scope "; flags="$flags--client-secret " ;;
|
|
12
|
+
claude:remove) values=" -s --scope " ;;
|
|
13
|
+
codex:add)
|
|
14
|
+
values=" -c --config --env --enable --disable --url --bearer-token-env-var \
|
|
15
|
+
--oauth-client-id --oauth-client-secret --oauth-client-registration --oauth-resource " ;;
|
|
16
|
+
codex:remove) values=" -c --config --enable --disable " ;;
|
|
17
|
+
*) return 1 ;;
|
|
18
|
+
esac
|
|
19
|
+
while [ "$#" -gt 0 ]; do
|
|
20
|
+
a="$1"; shift
|
|
21
|
+
case "$a" in
|
|
22
|
+
--) [ "$#" -gt 0 ] || return 1; printf '%s\n' "$1"; return 0 ;;
|
|
23
|
+
-h|--help) return 2 ;;
|
|
24
|
+
-?*) ;;
|
|
25
|
+
*) printf '%s\n' "$a"; return 0 ;;
|
|
26
|
+
esac
|
|
27
|
+
opt="$a"; attached=0
|
|
28
|
+
case "$a" in
|
|
29
|
+
--*=*) opt="${a%%=*}"; attached=1 ;;
|
|
30
|
+
--*) ;;
|
|
31
|
+
-?*) opt="${a:0:2}"; [ "${#a}" -le 2 ] || attached=1 ;;
|
|
32
|
+
esac
|
|
33
|
+
case "$values" in
|
|
34
|
+
*" $opt "*)
|
|
35
|
+
if [ "$attached" = 0 ]; then [ "$#" -gt 0 ] || return 1; shift; fi
|
|
36
|
+
continue ;;
|
|
37
|
+
esac
|
|
38
|
+
case "$variadic" in
|
|
39
|
+
*" $opt "*)
|
|
40
|
+
while [ "$#" -gt 0 ]; do case "$1" in -*) break ;; esac; shift; done
|
|
41
|
+
continue ;;
|
|
42
|
+
esac
|
|
43
|
+
case "$flags" in *" $opt "*) [ "$attached" = 0 ] || return 1; continue ;; esac
|
|
44
|
+
return 1
|
|
45
|
+
done
|
|
46
|
+
return 1
|
|
47
|
+
}
|