claude-multiacc 2.0.58 → 2.0.60
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 +125 -4
- package/bin/claude +389 -94
- package/bin/claude-accounts +276 -42
- package/bin/codex +69 -5
- package/bin/codex-accounts +33 -4
- package/docs/ACCOUNT_OPERATIONS.md +62 -2
- package/docs/AUTORESUME-HANDOFF.md +28 -3
- package/docs/AUTORESUME.md +275 -18
- package/docs/CODEX.md +1 -0
- package/docs/INSTALLATION.md +7 -1
- package/docs/VERIFICATION.md +4 -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_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/audit.py +88 -22
- package/lib/autoresume.py +1197 -82
- package/lib/autoresume_cross.py +3 -1
- package/lib/autoresume_handoff.py +81 -9
- package/lib/autoresume_probe.py +2 -2
- package/lib/claude_client_error.py +9 -4
- package/lib/codex_trust.py +263 -0
- package/lib/common.sh +146 -3
- package/lib/credential.py +5 -2
- package/lib/install_actions.sh +19 -0
- package/lib/keychain.py +180 -36
- package/lib/mcp_oauth.py +1748 -0
- package/lib/mcp_registry.py +230 -8
- package/lib/model_catalog.json +404 -0
- package/lib/model_catalog.py +201 -0
- package/lib/model_match.py +104 -0
- package/lib/report.py +8 -5
- package/lib/session_settings.py +123 -0
- 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_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/fake_codex_tui.py +25 -0
- package/tests/fake_security.sh +24 -2
- package/tests/fixtures/cross_provider_cli.py +109 -0
- package/tests/run-tests.sh +1171 -23
- package/tests/test_autoresume.py +215 -8
- package/tests/test_autoresume_catalog.py +226 -0
- package/tests/test_autoresume_catalog_cli.py +185 -0
- package/tests/test_autoresume_codex_records.py +16 -0
- package/tests/test_autoresume_codex_trust.py +247 -0
- package/tests/test_autoresume_cross_guards.py +28 -0
- package/tests/test_autoresume_cross_tmux.py +27 -2
- package/tests/test_autoresume_cross_tokens.py +32 -2
- package/tests/test_autoresume_handoff_argv.py +120 -16
- package/tests/test_autoresume_handoff_origins.py +14 -0
- package/tests/test_autoresume_memory.py +1141 -0
- package/tests/test_autoresume_memory_tmux.py +94 -0
- package/tests/test_autoresume_probe.py +2 -1
- package/tests/test_autoresume_relaunch_settings.py +317 -0
- package/tests/test_autoresume_settings.py +135 -0
- package/tests/test_autoresume_sidechain.py +1518 -0
- package/tests/test_autoresume_sidechain_tmux.py +79 -0
- package/tests/test_autoresume_trust_tmux.py +63 -0
- package/tests/test_claude_client_error.py +10 -7
- package/tests/test_claude_org_block.py +1 -0
- package/tests/test_claude_pool_prepare.py +4 -1
- package/tests/test_claude_reset.py +3 -0
- package/tests/test_keychain_nologin.py +340 -0
- package/tests/test_mcp_oauth.py +1414 -0
- package/tests/test_mcp_registry.py +234 -3
- package/tests/test_packaged_commands.py +41 -0
package/bin/claude
CHANGED
|
@@ -123,20 +123,115 @@ mcp_ensure() { # $1 acct dir — silent, never changes the exit status
|
|
|
123
123
|
return 0
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
# ---- MCP OAuth logins for every account (lib/mcp_oauth.py) ------------------------
|
|
127
|
+
# The client keeps an MCP server's OAuth login in the config dir's credential store
|
|
128
|
+
# (`mcpOAuth` in .credentials.json, or in the account's Keychain item) — so a server
|
|
129
|
+
# authenticated under one pooled account said "needs authentication" under every other
|
|
130
|
+
# (2026-10-19: five separate higgsfield grants in five accounts, stubs everywhere else).
|
|
131
|
+
# The pool keeps the newest login per server in <pool>/mcp-oauth.json (machine-local,
|
|
132
|
+
# never synced) and merges it into the picked account's stores right before exec — a
|
|
133
|
+
# setup-token account with no .credentials.json gets one holding MCP state alone, which
|
|
134
|
+
# file_login reads as no login, so its token is still exported — and only behind a
|
|
135
|
+
# recent refreshing limits pass: one shared login, one refresher. No network here:
|
|
136
|
+
# refreshing is that pass's job. Bash only on the hot path —
|
|
137
|
+
# python runs when an account's file changed since the last harvest (the mtime of
|
|
138
|
+
# <pool>/.mcp-oauth-seen) and either a store exists or that file carries MCP OAuth state,
|
|
139
|
+
# or when the store changed since this account last got it (its <dir>/.mcp-oauth-applied
|
|
140
|
+
# stamp).
|
|
141
|
+
# Fail-open: python bounds its own run and never changes the exit status.
|
|
142
|
+
MCP_OAUTH_PY="$SELF_DIR/../lib/mcp_oauth.py"
|
|
143
|
+
mcp_oauth_ensure() { # $1 acct dir — silent, never changes the exit status
|
|
144
|
+
[ "${CLAUDE_MULTIACC_MCP_OAUTH:-1}" = "0" ] && return 0
|
|
145
|
+
[ -f "$MCP_OAUTH_PY" ] || return 0
|
|
146
|
+
local store="$ACC_ROOT/mcp-oauth.json" f changed=0 run=0
|
|
147
|
+
local -a creds
|
|
148
|
+
creds=("$ACC_ROOT"/acct-*/.credentials.json)
|
|
149
|
+
# -nt is also true when the right-hand file is missing: a first look always runs.
|
|
150
|
+
for f in "${creds[@]}"; do
|
|
151
|
+
if [ "$f" -nt "$ACC_ROOT/.mcp-oauth-seen" ]; then changed=1; break; fi
|
|
152
|
+
done
|
|
153
|
+
if [ -f "$store" ]; then
|
|
154
|
+
if [ "$changed" = 1 ] || [ "$store" -nt "$1/.mcp-oauth-applied" ]; then run=1; fi
|
|
155
|
+
elif [ "$changed" = 1 ] && LC_ALL=C grep -qs '"mcpOAuth"' "${creds[@]}" 2>/dev/null; then
|
|
156
|
+
run=1 # no store yet: a grep, and only after a change
|
|
157
|
+
fi
|
|
158
|
+
[ "$run" = 1 ] || return 0
|
|
159
|
+
"$MCP_PYBIN" "$MCP_OAUTH_PY" ensure --root "$ACC_ROOT" --account-dir "$1" --quiet \
|
|
160
|
+
>/dev/null 2>&1 </dev/null || true
|
|
161
|
+
return 0
|
|
162
|
+
}
|
|
163
|
+
|
|
126
164
|
# `claude mcp add|add-json|remove` writes ONE account's .claude.json — the one it ran
|
|
127
165
|
# under — and nobody else's. So the shim runs it as a child instead of exec-ing it, then
|
|
128
166
|
# mirrors what it wrote into the registry and every other account (`learn` diffs the
|
|
129
167
|
# account against a snapshot taken just before). The real CLI's own parser and writer do
|
|
130
168
|
# the work; the shim only copies the outcome. Exits with the real binary's status; a
|
|
131
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) return 0 ;; esac
|
|
228
|
+
return 1
|
|
229
|
+
}
|
|
230
|
+
|
|
132
231
|
mcp_mirror_run() { # $1 acct dir, then the user's argv — exits when it handled the run
|
|
133
|
-
local d="$1" snap="" rc
|
|
232
|
+
local d="$1" snap="" rc
|
|
134
233
|
shift
|
|
135
|
-
|
|
136
|
-
# CLI accepts in front of any subcommand): look past it.
|
|
137
|
-
if [ "$verb" = "--model" ]; then verb="${3:-}"; sub="${4:-}"; fi
|
|
138
|
-
[ "$verb" = "mcp" ] || return 1
|
|
139
|
-
case "$sub" in add|add-json|remove) ;; *) return 1 ;; esac
|
|
234
|
+
mcp_mutation_argv "$@" || return 1
|
|
140
235
|
[ "${CLAUDE_MULTIACC_MCP:-1}" != "0" ] && [ -f "$MCP_PY" ] || return 1
|
|
141
236
|
mcp_ensure "$d"
|
|
142
237
|
snap="$(mktemp "${TMPDIR:-/tmp}/claude-mcp-snap.XXXXXX" 2>/dev/null)" || snap=""
|
|
@@ -169,9 +264,8 @@ mcp_nested_mirror() {
|
|
|
169
264
|
[ "${CLAUDE_MULTIACC_DISABLE:-0}" = "1" ] && return 1
|
|
170
265
|
[ "${CLAUDE_MULTIACC_MCP:-1}" = "0" ] && return 1
|
|
171
266
|
[ -n "${CLAUDE_CONFIG_DIR:-}" ] || return 1
|
|
172
|
-
local
|
|
173
|
-
|
|
174
|
-
[ "$verb" = "mcp" ] || return 1
|
|
267
|
+
local cfg="${CLAUDE_CONFIG_DIR%/}" d=""
|
|
268
|
+
mcp_mutation_argv "$@" || return 1
|
|
175
269
|
# A DIRECT child of the pool root, by its given spelling or its physical one (an
|
|
176
270
|
# adopted account is a symlink whose target is the operator's ~/.claude).
|
|
177
271
|
case "$cfg" in
|
|
@@ -243,18 +337,38 @@ kc_service() { # $1 = acct dir -> the keychain service name the client uses for
|
|
|
243
337
|
elif command -v sha256sum >/dev/null 2>&1; then h="$(printf '%s' "$1" | sha256sum 2>/dev/null)"; fi
|
|
244
338
|
printf 'Claude Code-credentials-%s' "$(printf '%s' "$h" | cut -c1-8)"
|
|
245
339
|
}
|
|
340
|
+
# A credentials document — the Keychain item or <dir>/.credentials.json — is a Claude
|
|
341
|
+
# LOGIN only when it carries a `claudeAiOauth` object. The client keeps its MCP servers'
|
|
342
|
+
# OAuth state in the same document (`mcpOAuth`, every entry with an expiresAt and a
|
|
343
|
+
# refreshToken of its own), and from a session that cannot open the Keychain (ssh/tmux)
|
|
344
|
+
# Claude Code 2.1.303 writes a .credentials.json holding ONLY that as soon as an OAuth MCP
|
|
345
|
+
# server is discovered. Counted as the login, such a file got its account picked with no
|
|
346
|
+
# token exported — "Not logged in · Run /login" on every run — while the greedy expiry
|
|
347
|
+
# scrape read the MCP entry's future expiresAt as the login's (2026-10-19). So every check
|
|
348
|
+
# reads the login object alone. It is flat: this isolates it up to its closing brace, or to
|
|
349
|
+
# the end of a truncated document (as lenient as the scrape always was). The C locale
|
|
350
|
+
# keeps one invalid byte elsewhere in the document from hiding it; no process is started.
|
|
351
|
+
CRED_LOGIN_RE='"claudeAiOauth"[[:space:]]*:[[:space:]]*[{][^}]*'
|
|
352
|
+
CRED_LOGIN=""
|
|
353
|
+
cred_login_of() { # $1 = document text; sets CRED_LOGIN to its login object, rc 1 when none
|
|
354
|
+
local LC_ALL=C
|
|
355
|
+
CRED_LOGIN=""
|
|
356
|
+
[[ $1 =~ $CRED_LOGIN_RE ]] || return 1
|
|
357
|
+
CRED_LOGIN="${BASH_REMATCH[0]}"
|
|
358
|
+
}
|
|
246
359
|
# One lookup per dir per run: has_auth, auth_dead and acct_token all ask about the
|
|
247
360
|
# same dir in a row, and every `security` call is a process. Memoised in variables
|
|
248
361
|
# (not a $(...) result) so the memo survives — callers read KC_JSON after kc_lookup.
|
|
249
362
|
KC_MEMO_DIR=""; KC_MEMO_RC=1; KC_JSON=""
|
|
250
|
-
kc_lookup() { # $1 = acct dir; sets KC_JSON; rc 0 readable, 1 none
|
|
363
|
+
kc_lookup() { # $1 = acct dir; sets KC_JSON; rc 0 readable login, 1 none (an item holding
|
|
364
|
+
# MCP state alone included), 2 exists but locked here
|
|
251
365
|
[ "$KC_ON" = "1" ] || { KC_JSON=""; return 1; }
|
|
252
366
|
if [ "$KC_MEMO_DIR" = "$1" ]; then return "$KC_MEMO_RC"; fi
|
|
253
367
|
local out rc
|
|
254
368
|
out="$(security find-generic-password -a "$KC_ACCOUNT" -s "$(kc_service "$1")" -w 2>/dev/null)"; rc=$?
|
|
255
369
|
KC_MEMO_DIR="$1"; KC_JSON=""; KC_MEMO_RC=1
|
|
256
370
|
case "$rc" in
|
|
257
|
-
0)
|
|
371
|
+
0) if cred_login_of "$out"; then KC_JSON="$out"; KC_MEMO_RC=0; fi ;;
|
|
258
372
|
36) KC_MEMO_RC=2 ;;
|
|
259
373
|
esac
|
|
260
374
|
return "$KC_MEMO_RC"
|
|
@@ -288,7 +402,8 @@ sel_log() {
|
|
|
288
402
|
|
|
289
403
|
# ---- auto-resume ----------------------------------------------------------------
|
|
290
404
|
# An interactive session that stops on a usage limit, a failed login, an API error or a
|
|
291
|
-
# crash is resumed — same session id, no
|
|
405
|
+
# crash, or whose client shows its critical-memory banner, is resumed — same session id, no
|
|
406
|
+
# keystroke — on another pooled account (crash/memory: any one, the same included). The TUI
|
|
292
407
|
# at a limit does not exit; it idles on "continuing automatically at <reset>", which can
|
|
293
408
|
# be days away. The shim's share of the job is small on purpose and never blocks:
|
|
294
409
|
# - the plain interactive exec stays byte-identical; just before it, a detached watcher
|
|
@@ -321,6 +436,8 @@ AR_LOADED=0
|
|
|
321
436
|
AR_ARGV=()
|
|
322
437
|
AR_ORIG_ARGV=()
|
|
323
438
|
AR_R_CLASS="" AR_R_ACCT="" AR_R_RESET="" AR_R_RTYPE="" AR_R_MARKED="" AR_R_SID=""
|
|
439
|
+
AR_R_MODEL="" # the model the relaunched session last ran on (its --resume restores it)
|
|
440
|
+
AR_R_VIA="" # token|login: the credential the left run carried (its watcher's state)
|
|
324
441
|
AR_TRUST=0 # this run continues a relaunch: its cwd is trusted in the picked account
|
|
325
442
|
AR_PY=""
|
|
326
443
|
|
|
@@ -343,7 +460,7 @@ ar_field_ok() { # $1 value
|
|
|
343
460
|
# later, by ar_relaunch_apply, still ahead of any selection.
|
|
344
461
|
ar_relaunch_load() { # $1 token -> 0 with AR_ARGV and the AR_R_*/chain fields set
|
|
345
462
|
local tok="$1" rf af line k v n=0 a="" ok=0 ver="" prov="" sid="" cwd=""
|
|
346
|
-
local cls="" acct="" reset="" rtype="" marked="" depth=0 avoid="" hist="" chain="" cross_bad=0
|
|
463
|
+
local cls="" acct="" reset="" rtype="" marked="" depth=0 avoid="" hist="" chain="" cross_bad=0 model="" via=""
|
|
347
464
|
# Alphanumerics only: the token becomes part of a path, so no '/' and no '..'.
|
|
348
465
|
case "$tok" in *[!A-Za-z0-9]*) return 1 ;; esac
|
|
349
466
|
[ "${#tok}" -ge 8 ] && [ "${#tok}" -le 64 ] || return 1
|
|
@@ -379,6 +496,8 @@ ar_relaunch_load() { # $1 token -> 0 with AR_ARGV and the AR_R_*/chain fields se
|
|
|
379
496
|
if [ "$k" = cross ]; then AR_CROSS="$v"; else AR_HANDOFF_TOKEN="$v"; fi ;;
|
|
380
497
|
hist) ar_field_ok "$v" && hist="$v" ;;
|
|
381
498
|
chain) ar_field_ok "$v" && [ "${#v}" -le 64 ] && chain="$v" ;;
|
|
499
|
+
model) case "$v" in ''|[!A-Za-z0-9]*|*[!]A-Za-z0-9._:/@+[-]*) ;; *) [ "${#v}" -le 200 ] && model="$v" ;; esac ;;
|
|
500
|
+
via) case "$v" in token|login) via="$v" ;; esac ;;
|
|
382
501
|
esac
|
|
383
502
|
done < "$rf"
|
|
384
503
|
if [ "$ver" = 1 ] && [ "$prov" = claude ] && [ -f "$af" ] \
|
|
@@ -404,7 +523,7 @@ ar_relaunch_load() { # $1 token -> 0 with AR_ARGV and the AR_R_*/chain fields se
|
|
|
404
523
|
fi
|
|
405
524
|
AR_LOADED=1
|
|
406
525
|
AR_R_CLASS="$cls"; AR_R_ACCT="$acct"; AR_R_RESET="$reset"; AR_R_RTYPE="$rtype"
|
|
407
|
-
AR_R_MARKED="$marked"
|
|
526
|
+
AR_R_MARKED="$marked"; AR_R_MODEL="$model"; AR_R_SID="$sid"; AR_R_VIA="$via"
|
|
408
527
|
AR_DEPTH="$depth"; AR_HIST="$hist"; AR_CHAIN="$chain"; AR_AVOID="$avoid"
|
|
409
528
|
# The pane's shell sits wherever the session was launched, but the session's own cwd is
|
|
410
529
|
# what --resume looks the transcript up by. Everything already resolved against the old
|
|
@@ -439,18 +558,30 @@ ar_token_dead() { # $1 session id (the token's, else the file's) or empty
|
|
|
439
558
|
# The verdict the watcher saw, written through the SAME writers the transcript scans use
|
|
440
559
|
# (never shortening a longer park), so the old account is out of the pool before this
|
|
441
560
|
# very selection runs — and for every other launch too. Other classes (model-scoped,
|
|
442
|
-
# transient, billing/hold
|
|
561
|
+
# transient, billing/hold) leave no marker: the watcher's AVOID list covers them. crash and
|
|
562
|
+
# memory (the client at critical memory) are about the PROCESS: nothing is marked, the chain
|
|
563
|
+
# avoids nothing more, and selection is the ordinary one, the account just left included.
|
|
443
564
|
ar_relaunch_apply() {
|
|
444
565
|
local d=""
|
|
445
566
|
[ "$AR_LOADED" = 1 ] || return 0
|
|
446
567
|
[ -n "$AR_R_ACCT" ] && [ -d "$ACC_ROOT/$AR_R_ACCT" ] && d="$ACC_ROOT/$AR_R_ACCT"
|
|
447
568
|
if [ -n "$d" ]; then
|
|
448
569
|
case "$AR_R_CLASS" in
|
|
570
|
+
crash|memory) ;;
|
|
449
571
|
quota)
|
|
450
572
|
if num_ok "$AR_R_RESET" && [ "$AR_R_RESET" -gt "$now" ] && [ -n "$AR_R_RTYPE" ]; then
|
|
451
573
|
mark_client_limit "$d" "$AR_R_RESET" "$AR_R_RTYPE" "$AR_R_MARKED"
|
|
452
574
|
fi ;;
|
|
453
|
-
auth)
|
|
575
|
+
auth)
|
|
576
|
+
# The watcher knew what its run carried (the state the launching shim wrote); only
|
|
577
|
+
# a relaunch file without that (an older watcher) falls back to the sid's index line,
|
|
578
|
+
# which names the session the run was launched on, not one it switched to since.
|
|
579
|
+
if [ -n "$AR_R_VIA" ]; then
|
|
580
|
+
SESS_VIA=""; [ "$AR_R_VIA" = token ] && SESS_VIA=token
|
|
581
|
+
else
|
|
582
|
+
sess_via "$d" "$AR_R_SID"
|
|
583
|
+
fi
|
|
584
|
+
mark_client_auth_dead "$d" "$SESS_VIA" ;;
|
|
454
585
|
blocked)
|
|
455
586
|
if [ "$AR_R_RTYPE" = "oauth_org_not_allowed" ]; then
|
|
456
587
|
mark_proven_auth_dead "$d" "organization disabled Claude Code subscription access" "org-blocked"
|
|
@@ -563,8 +694,10 @@ ar_python() {
|
|
|
563
694
|
AR_PY="$py"
|
|
564
695
|
}
|
|
565
696
|
|
|
566
|
-
ar_start_watcher() { # $1 provider, $2 picked acct dir
|
|
567
|
-
|
|
697
|
+
ar_start_watcher() { # $1 provider, $2 picked acct dir, $3 token when the run carries the
|
|
698
|
+
# portable token (the relaunch names the credential it left: via=)
|
|
699
|
+
local prov="$1" dir="$2" lib st tmp launched root nl=$'\n' via=login
|
|
700
|
+
[ "${3:-}" = token ] && via=token
|
|
568
701
|
# Kill switches: the env for one shell, the file for the whole pool (the watcher
|
|
569
702
|
# re-reads the file every tick, so it also stops sessions already running).
|
|
570
703
|
case "${CLAUDE_MULTIACC_AUTORESUME:-1}" in 0|false|no|off) return 1 ;; esac
|
|
@@ -606,7 +739,7 @@ ar_start_watcher() { # $1 provider, $2 picked acct dir
|
|
|
606
739
|
"self=$SELF" "acc_root=$root" "depth=$AR_DEPTH" "avoid=$AR_AVOID" \
|
|
607
740
|
"hist=$AR_HIST" "chain=$AR_CHAIN" "avoid_claude=$AR_AVOID_CLAUDE" \
|
|
608
741
|
"avoid_codex=$AR_AVOID_CODEX" "pool_claude=$AR_POOL_CLAUDE" "pool_codex=$AR_POOL_CODEX" \
|
|
609
|
-
"handoff_token=$AR_HANDOFF_TOKEN" 2>/dev/null > "$tmp" \
|
|
742
|
+
"handoff_token=$AR_HANDOFF_TOKEN" "via=$via" 2>/dev/null > "$tmp" \
|
|
610
743
|
&& mv -f "$tmp" "$st" 2>/dev/null \
|
|
611
744
|
|| { rm -f "$tmp" 2>/dev/null; return 1; }
|
|
612
745
|
if [ -n "$AR_HANDOFF_TOKEN" ]; then
|
|
@@ -637,7 +770,7 @@ ar_trust() { # $1 picked acct dir
|
|
|
637
770
|
|
|
638
771
|
# Called right before the plain interactive exec. Whatever the gate decided, the TUI
|
|
639
772
|
# never sees an auto-resume variable (a nested `claude` must not inherit a probe flag).
|
|
640
|
-
ar_spawn_watcher() { # $1 provider, $2 picked acct dir
|
|
773
|
+
ar_spawn_watcher() { # $1 provider, $2 picked acct dir, $3 token|"" (ar_start_watcher)
|
|
641
774
|
local rc=0 v
|
|
642
775
|
ar_start_watcher "$@" || rc=$?
|
|
643
776
|
for v in ${!CLAUDE_MULTIACC_AR_*}; do unset "$v"; done
|
|
@@ -1271,20 +1404,44 @@ iso_of_epoch() { # $1 seconds -> UTC ISO8601 ('' when neither date(1) dialect wo
|
|
|
1271
1404
|
# Locale-proof (plain integers), and short enough that num_ok always passes.
|
|
1272
1405
|
iso_key() { local t="${1%%.*}"; t="$(printf '%s' "$t" | LC_ALL=C tr -cd '0-9')"; printf '%s\n' "${t}"; }
|
|
1273
1406
|
|
|
1274
|
-
# Index line: "<session id> <ISO claim>". The claim is the session's OWN start
|
|
1275
|
-
# an id belongs to exactly ONE account.
|
|
1407
|
+
# Index line: "<session id> <ISO claim>[ token]". The claim is the session's OWN start
|
|
1408
|
+
# time, and an id belongs to exactly ONE account.
|
|
1276
1409
|
# Why both: `claude --continue` resumes the SAME session id under whichever account the
|
|
1277
1410
|
# pool hands out next (the client only mints a new id with --fork-session), and the
|
|
1278
1411
|
# transcript is shared — so without a single owner one rejection would mark every account
|
|
1279
1412
|
# that ever touched that session, and without the claim time the new owner would inherit
|
|
1280
1413
|
# a rejection the PREVIOUS owner earned. Each rule only ever removes attribution: the
|
|
1281
1414
|
# failure mode is a missed limit, never an invented one.
|
|
1282
|
-
|
|
1283
|
-
|
|
1415
|
+
# `token` says the shim launched the run on the account's portable token itself (its
|
|
1416
|
+
# capture, $4): only such a run's authentication failure is evidence against that token.
|
|
1417
|
+
# A session merely sighted carries none, nor one launched on the login; the launching run
|
|
1418
|
+
# sets it every time, since a session resumed in the same account may change credential.
|
|
1419
|
+
sess_index_add() { # $1 acct dir, $2 session id, $3 start epoch (optional), $4 token|login:
|
|
1420
|
+
# the credential THIS shim launched the run on (sel_capture_session)
|
|
1421
|
+
local idx="$1/.sessions-index" tmp oidx claim="" via="" ln out="" hit=0
|
|
1284
1422
|
sess_id_ok "$2" || return 0
|
|
1423
|
+
[ "${4:-}" = token ] && via=" token"
|
|
1285
1424
|
# `( |$)` also matches a claim-less line from an older build, so such an entry is still
|
|
1286
1425
|
# deduped and can still be released when another account takes the session over.
|
|
1287
|
-
[ -f "$idx" ] && LC_ALL=C grep -qE -- "^$2( |$)" "$idx" 2>/dev/null
|
|
1426
|
+
if [ -f "$idx" ] && LC_ALL=C grep -qE -- "^$2( |$)" "$idx" 2>/dev/null; then
|
|
1427
|
+
[ -n "${4:-}" ] || return 0
|
|
1428
|
+
# The launching run's credential, onto the line the session already has (claim kept;
|
|
1429
|
+
# a claim-less line cannot carry it). Builtins only, and no write when it is right.
|
|
1430
|
+
while IFS= read -r ln || [ -n "$ln" ]; do
|
|
1431
|
+
case "$ln" in
|
|
1432
|
+
"$2 "*)
|
|
1433
|
+
claim="${ln#* }"; claim="${claim%% *}"
|
|
1434
|
+
[ "$ln" = "$2 $claim$via" ] || { ln="$2 $claim$via"; hit=1; } ;;
|
|
1435
|
+
esac
|
|
1436
|
+
out="$out$ln
|
|
1437
|
+
"
|
|
1438
|
+
ln=""
|
|
1439
|
+
done < "$idx"
|
|
1440
|
+
[ "$hit" = 1 ] || return 0
|
|
1441
|
+
printf '%s' "$out" 2>/dev/null > "$idx.$$" && mv -f "$idx.$$" "$idx" 2>/dev/null \
|
|
1442
|
+
|| rm -f "$idx.$$" 2>/dev/null
|
|
1443
|
+
return 0
|
|
1444
|
+
fi
|
|
1288
1445
|
num_ok "${3:-}" && claim="$(iso_of_epoch "$3")"
|
|
1289
1446
|
[ -n "$claim" ] || claim="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
1290
1447
|
for oidx in "$ACC_ROOT"/acct-*/.sessions-index; do
|
|
@@ -1300,35 +1457,90 @@ sess_index_add() { # $1 acct dir, $2 session id, $3 start epoch (optional)
|
|
|
1300
1457
|
fi
|
|
1301
1458
|
done
|
|
1302
1459
|
tmp="$idx.$$"
|
|
1303
|
-
{ [ -f "$idx" ] && cat "$idx" 2>/dev/null; printf '%s %s\n' "$2" "$claim"; } \
|
|
1460
|
+
{ [ -f "$idx" ] && cat "$idx" 2>/dev/null; printf '%s %s%s\n' "$2" "$claim" "$via"; } \
|
|
1304
1461
|
| tail -n "$SESS_INDEX_MAX" 2>/dev/null > "$tmp" \
|
|
1305
1462
|
&& mv -f "$tmp" "$idx" 2>/dev/null || rm -f "$tmp" 2>/dev/null
|
|
1306
1463
|
return 0
|
|
1307
1464
|
}
|
|
1308
1465
|
|
|
1309
|
-
|
|
1310
|
-
|
|
1466
|
+
# The credential the shim launched <session> on, from <dir>'s index: SESS_VIA=token, or
|
|
1467
|
+
# empty (the login, or not launched by the shim at all). Builtins only.
|
|
1468
|
+
SESS_VIA=""
|
|
1469
|
+
sess_via() { # $1 acct dir, $2 session id; sets SESS_VIA
|
|
1470
|
+
local ln
|
|
1471
|
+
SESS_VIA=""
|
|
1472
|
+
sess_id_ok "${2:-}" || return 0
|
|
1473
|
+
[ -f "$1/.sessions-index" ] || return 0
|
|
1474
|
+
while IFS= read -r ln; do
|
|
1475
|
+
case "$ln" in "$2 "*" token") SESS_VIA=token ;; "$2"|"$2 "*) SESS_VIA="" ;; esac
|
|
1476
|
+
done < "$1/.sessions-index"
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
sess_index_refresh() { # $1 acct dir — record every run currently live in this account;
|
|
1480
|
+
# $2 the pid this shim execs into and $3 the credential that run
|
|
1481
|
+
# carries (token|login): sel_capture_session only
|
|
1482
|
+
local f id started known="" tagged=" " ln via pid rst rvia
|
|
1311
1483
|
sessions_owned "$1" || return 0
|
|
1312
1484
|
# Read the index ONCE with the builtin, so the steady state (every live session already
|
|
1313
1485
|
# claimed) costs one sed per session and not a grep and a second sed on top.
|
|
1314
1486
|
if [ -f "$1/.sessions-index" ]; then
|
|
1315
|
-
while IFS= read -r ln; do
|
|
1487
|
+
while IFS= read -r ln; do
|
|
1488
|
+
known="$known ${ln%% *}"
|
|
1489
|
+
case "$ln" in *" token") tagged="$tagged${ln%% *} " ;; esac
|
|
1490
|
+
done < "$1/.sessions-index"
|
|
1316
1491
|
fi
|
|
1317
1492
|
for f in "$1"/sessions/*.json; do
|
|
1318
1493
|
[ -f "$f" ] || continue
|
|
1319
1494
|
# A symlinked registry entry would let another pool's session id in under this
|
|
1320
1495
|
# account's name (second-pass codex-review finding, nested-symlink variant).
|
|
1321
1496
|
[ -L "$f" ] && continue
|
|
1497
|
+
pid="${f##*/}"; pid="${pid%.json}"
|
|
1498
|
+
# The run this shim execs into is the one session whose credential it knows: its line
|
|
1499
|
+
# gets it even when another run sighted the session first. Any other live run the shim
|
|
1500
|
+
# launched has its credential on record by PROCESS (sess_via_record), for whichever
|
|
1501
|
+
# session the client has switched it to since (/clear, /resume inside the TUI).
|
|
1502
|
+
via=""; rvia=""
|
|
1503
|
+
if [ -n "${3:-}" ] && [ "$pid" = "${2:-}" ]; then
|
|
1504
|
+
via="$3"
|
|
1505
|
+
else
|
|
1506
|
+
case "$pid" in
|
|
1507
|
+
''|*[!0-9]*) ;;
|
|
1508
|
+
*) if [ -f "$1/.sessions-via/$pid" ] && [ ! -L "$1/.sessions-via/$pid" ]; then
|
|
1509
|
+
rst=""
|
|
1510
|
+
{ IFS=' ' read -r rst rvia < "$1/.sessions-via/$pid"; } 2>/dev/null
|
|
1511
|
+
case "$rvia" in token|login) ;; *) rvia="" ;; esac
|
|
1512
|
+
fi ;;
|
|
1513
|
+
esac
|
|
1514
|
+
fi
|
|
1322
1515
|
id="$(LC_ALL=C sed -n 's/.*"sessionId"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$f" 2>/dev/null | head -1)"
|
|
1323
1516
|
sess_id_ok "$id" || continue
|
|
1324
|
-
case "$known" in
|
|
1517
|
+
case "$known" in
|
|
1518
|
+
*" $id "*|*" $id")
|
|
1519
|
+
if [ -z "$via" ]; then
|
|
1520
|
+
# Builtins only while the line already says what the record says.
|
|
1521
|
+
[ -n "$rvia" ] || continue
|
|
1522
|
+
case "$tagged" in
|
|
1523
|
+
*" $id "*) [ "$rvia" = token ] && continue ;;
|
|
1524
|
+
*) [ "$rvia" = login ] && continue ;;
|
|
1525
|
+
esac
|
|
1526
|
+
fi ;;
|
|
1527
|
+
esac
|
|
1325
1528
|
# startedAt is epoch MILLIseconds; using the session's real start (not "now") is what
|
|
1326
1529
|
# lets a session that has been running since before this shim was installed still be
|
|
1327
|
-
# attributed correctly.
|
|
1530
|
+
# attributed correctly. It also names the process: the client rewrites only the
|
|
1531
|
+
# sessionId when it switches sessions, so a record whose startedAt differs was written
|
|
1532
|
+
# for an earlier process under a reused pid, and says nothing about this one.
|
|
1328
1533
|
started="$(LC_ALL=C sed -n 's/.*"startedAt"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$f" 2>/dev/null | head -1)"
|
|
1534
|
+
if [ -n "$rvia" ] && num_ok "$started" && [ "$started" = "$rst" ]; then
|
|
1535
|
+
via="$rvia"
|
|
1536
|
+
fi
|
|
1537
|
+
if [ -z "$via" ]; then
|
|
1538
|
+
case "$known" in *" $id "*|*" $id") continue ;; esac
|
|
1539
|
+
fi
|
|
1329
1540
|
num_ok "$started" && started=$((started / 1000)) || started=""
|
|
1330
|
-
sess_index_add "$1" "$id" "$started"
|
|
1541
|
+
sess_index_add "$1" "$id" "$started" "$via"
|
|
1331
1542
|
known="$known $id"
|
|
1543
|
+
case "$via" in token) tagged="$tagged$id " ;; login) tagged="${tagged/ $id / }" ;; esac
|
|
1332
1544
|
done
|
|
1333
1545
|
return 0
|
|
1334
1546
|
}
|
|
@@ -1350,7 +1562,10 @@ claim_conflicted() { # $1 acct dir, $2 session id, $3 our ISO claim
|
|
|
1350
1562
|
# older claim followed by a newer one), and it is the newest rival that decides.
|
|
1351
1563
|
while IFS= read -r other; do
|
|
1352
1564
|
[ -n "$other" ] || continue
|
|
1353
|
-
case "$other" in
|
|
1565
|
+
case "$other" in
|
|
1566
|
+
*' '*) other="${other#* }"; ok="$(iso_key "${other%% *}")" ;;
|
|
1567
|
+
*) return 0 ;;
|
|
1568
|
+
esac
|
|
1354
1569
|
num_ok "$ok" || return 0
|
|
1355
1570
|
[ "$ok" -ge "$mk" ] && return 0
|
|
1356
1571
|
done <<EOF
|
|
@@ -1401,7 +1616,7 @@ client_limit_scan() { # $1 acct dir
|
|
|
1401
1616
|
while IFS= read -r ln; do
|
|
1402
1617
|
id="${ln%% *}"
|
|
1403
1618
|
claim=""
|
|
1404
|
-
case "$ln" in *' '*) claim="${ln#* }" ;; esac
|
|
1619
|
+
case "$ln" in *' '*) claim="${ln#* }"; claim="${claim%% *}" ;; esac
|
|
1405
1620
|
sess_id_ok "$id" && { ids+=("$id"); claims+=("$claim"); }
|
|
1406
1621
|
done < "$idx"
|
|
1407
1622
|
# The budget bounds ENTRIES EXAMINED, not just transcripts read: an entry that fails the
|
|
@@ -1475,11 +1690,14 @@ client_limit_scan() { # $1 acct dir
|
|
|
1475
1690
|
# A TUI rejection cannot use the -p retry path because the client owns the terminal.
|
|
1476
1691
|
# Read only structured API errors belonging to this account's session claim. The JSON
|
|
1477
1692
|
# helper runs only when the bounded grep finds an auth or organization error code.
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1693
|
+
# A `claude-accounts verify` that reached the account (.client-org-cleared) supersedes
|
|
1694
|
+
# every error recorded before it, an authentication failure as much as an org refusal.
|
|
1695
|
+
CLIENT_ERROR_REASON=""; CLIENT_ERROR_VIA=""
|
|
1696
|
+
client_error_scan() { # $1 acct dir; sets CLIENT_ERROR_REASON, and CLIENT_ERROR_VIA=token
|
|
1697
|
+
# when an auth failure came from a run the shim launched on the token
|
|
1698
|
+
local idx="$1/.sessions-index" ln id claim via p line read_n=0 i cleared=0 parser auth=""
|
|
1699
|
+
local ids=() claims=() vias=()
|
|
1700
|
+
CLIENT_ERROR_REASON=""; CLIENT_ERROR_VIA=""
|
|
1483
1701
|
{ [ -L "$1" ] || [ -L "$1/sessions" ]; } && return 1
|
|
1484
1702
|
parser="$SELF_DIR/../lib/claude_client_error.py"
|
|
1485
1703
|
[ -f "$parser" ] || return 1
|
|
@@ -1488,13 +1706,15 @@ client_error_scan() { # $1 acct dir; sets CLIENT_ERROR_REASON
|
|
|
1488
1706
|
[ -f "$1/.client-org-cleared" ] \
|
|
1489
1707
|
&& { IFS= read -r cleared < "$1/.client-org-cleared" 2>/dev/null || cleared=0; }
|
|
1490
1708
|
while IFS= read -r ln; do
|
|
1491
|
-
id="${ln%% *}"; claim=""
|
|
1709
|
+
id="${ln%% *}"; claim=""; via=""
|
|
1492
1710
|
case "$ln" in *' '*) claim="${ln#* }" ;; esac
|
|
1493
|
-
|
|
1711
|
+
case "$claim" in *' '*) via="${claim#* }"; claim="${claim%% *}" ;; esac
|
|
1712
|
+
sess_id_ok "$id" && { ids+=("$id"); claims+=("$claim"); vias+=("$via"); }
|
|
1494
1713
|
done < "$idx"
|
|
1495
1714
|
i=$(( ${#ids[@]} - 1 ))
|
|
1496
1715
|
while [ "$i" -ge 0 ] && [ "$read_n" -lt "$QUOTA_SCAN_MAX_FILES" ]; do
|
|
1497
|
-
id="${ids[$i]}"; claim="${claims[$i]}"; i
|
|
1716
|
+
id="${ids[$i]}"; claim="${claims[$i]}"; via="${vias[$i]}"
|
|
1717
|
+
i=$((i - 1)); read_n=$((read_n + 1))
|
|
1498
1718
|
sess_transcript "$1" "$id" || continue
|
|
1499
1719
|
p="$SESS_TRANSCRIPT"
|
|
1500
1720
|
[ $((now - $(file_mtime "$p"))) -le "$QUOTA_SCAN_MAX_AGE" ] || continue
|
|
@@ -1507,7 +1727,7 @@ client_error_scan() { # $1 acct dir; sets CLIENT_ERROR_REASON
|
|
|
1507
1727
|
--claim "$claim" --org-cleared "$cleared" 2>/dev/null)" || continue
|
|
1508
1728
|
case "$line" in
|
|
1509
1729
|
org-blocked\ *) CLIENT_ERROR_REASON="org-blocked"; return 0 ;;
|
|
1510
|
-
auth\ *) auth=1 ;;
|
|
1730
|
+
auth\ *) auth=1; [ "$via" = token ] && CLIENT_ERROR_VIA=token ;;
|
|
1511
1731
|
esac
|
|
1512
1732
|
done
|
|
1513
1733
|
[ -n "$auth" ] && { CLIENT_ERROR_REASON="auth"; return 0; }
|
|
@@ -1527,10 +1747,14 @@ mark_proven_auth_dead() { # $1 acct dir, $2 fixed safe detail, $3 optional reaso
|
|
|
1527
1747
|
|| rm -f "$1/.expired.$$" 2>/dev/null || true
|
|
1528
1748
|
}
|
|
1529
1749
|
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1750
|
+
# The park names the credential the FAILED run carried, never the one this run would pick
|
|
1751
|
+
# (they differ whenever the account's credentials changed in between): a run the shim
|
|
1752
|
+
# launched on the portable token (its index line, sess_index_add) retires that token, and
|
|
1753
|
+
# anything else is the login's. Asking acct_token here charged the token for runs that
|
|
1754
|
+
# never carried one — the "Not logged in" runs of 2026-10-19, once an MCP-only file stopped
|
|
1755
|
+
# counting as their login — and parked a working token until a new one was minted.
|
|
1756
|
+
mark_client_auth_dead() { # $1 acct dir, $2 token when the failed run carried the token
|
|
1757
|
+
if [ "${2:-}" = token ]; then
|
|
1534
1758
|
rm -f "$1/.server-token-verified" 2>/dev/null || true
|
|
1535
1759
|
mark_proven_auth_dead "$1" "client rejected the portable OAuth token" \
|
|
1536
1760
|
"setup-token-invalid"
|
|
@@ -1569,47 +1793,97 @@ mark_client_limit() { # $1 acct dir, $2 reset epoch, $3 rate limit type, $4 reje
|
|
|
1569
1793
|
# it (and every other live run of the account) in the index, because by the time the
|
|
1570
1794
|
# user quits a limit-hit session and starts a new one, the client has already deleted
|
|
1571
1795
|
# its registry file and nothing else can name the transcript that holds the evidence.
|
|
1572
|
-
|
|
1573
|
-
|
|
1796
|
+
# It also records what the run carries — the portable token, or the login — which only
|
|
1797
|
+
# this shim knows (mark_client_auth_dead).
|
|
1798
|
+
sel_capture_session() { # $1 acct dir, $2 token when the run carries the portable token
|
|
1799
|
+
local d="$1" pid=$$ via=login
|
|
1800
|
+
[ "${2:-}" = token ] && via=token
|
|
1574
1801
|
[ "${CLAUDE_MULTIACC_CLIENT_LIMITS:-1}" = "0" ] && return 0
|
|
1575
1802
|
(
|
|
1576
1803
|
# Ctrl-C / a closed terminal must not kill the capture: it is bounded (60s) and
|
|
1577
1804
|
# stops the moment the run it is watching is gone.
|
|
1578
1805
|
trap '' INT HUP TERM QUIT
|
|
1579
|
-
i=0
|
|
1806
|
+
i=0 tries=0
|
|
1580
1807
|
while [ "$i" -lt 600 ]; do
|
|
1581
1808
|
kill -0 "$pid" 2>/dev/null || break
|
|
1582
|
-
sess_index_refresh "$d"
|
|
1583
|
-
[ -f "$d/sessions/$pid.json" ]
|
|
1809
|
+
sess_index_refresh "$d" "$pid" "$via"
|
|
1810
|
+
if [ -f "$d/sessions/$pid.json" ]; then
|
|
1811
|
+
# Registered: the run's credential goes on record by process. A registry file caught
|
|
1812
|
+
# mid-write (no startedAt yet) gets a few more looks.
|
|
1813
|
+
sess_via_record "$d" "$pid" "$via" && break
|
|
1814
|
+
tries=$((tries + 1)); [ "$tries" -lt 10 ] || break
|
|
1815
|
+
fi
|
|
1584
1816
|
sleep 0.1
|
|
1585
1817
|
i=$((i + 1))
|
|
1586
1818
|
done
|
|
1587
|
-
sess_index_refresh "$d"
|
|
1819
|
+
sess_index_refresh "$d" "$pid" "$via"
|
|
1588
1820
|
) >/dev/null 2>&1 </dev/null &
|
|
1589
1821
|
return 0
|
|
1590
1822
|
}
|
|
1591
1823
|
|
|
1824
|
+
# The credential the shim launched a PROCESS on, for that process's whole life:
|
|
1825
|
+
# <acct>/.sessions-via/<pid> holds "<its registry startedAt, ms> token|login". The client
|
|
1826
|
+
# keeps one registry file per process and, on /clear or a /resume inside the TUI, rewrites
|
|
1827
|
+
# only its sessionId (2.1.304's session-switch listener merges {sessionId, updatedAt} into
|
|
1828
|
+
# it), so the capture tags only the run's FIRST session; sess_index_refresh tags every later
|
|
1829
|
+
# one from this record. startedAt survives those rewrites and names the process, so a reused
|
|
1830
|
+
# pid inherits nothing. Written once the registry file is there (the capture, in the
|
|
1831
|
+
# background); a record whose registry file is gone is dropped then too.
|
|
1832
|
+
sess_via_record() { # $1 acct dir, $2 pid, $3 token|login; rc 1: the registry file is there
|
|
1833
|
+
# but names no startedAt (yet)
|
|
1834
|
+
local dir="$1/.sessions-via" f st
|
|
1835
|
+
sessions_owned "$1" || return 0
|
|
1836
|
+
[ -f "$1/sessions/$2.json" ] && [ ! -L "$1/sessions/$2.json" ] || return 0
|
|
1837
|
+
st="$(LC_ALL=C sed -n 's/.*"startedAt"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' \
|
|
1838
|
+
"$1/sessions/$2.json" 2>/dev/null | head -1)"
|
|
1839
|
+
num_ok "$st" || return 1
|
|
1840
|
+
[ -L "$dir" ] && return 0
|
|
1841
|
+
[ -d "$dir" ] || mkdir "$dir" 2>/dev/null || [ -d "$dir" ] || return 0
|
|
1842
|
+
for f in "$dir"/*; do
|
|
1843
|
+
[ -f "$f" ] || continue
|
|
1844
|
+
[ -e "$1/sessions/${f##*/}.json" ] || rm -f "$f" 2>/dev/null
|
|
1845
|
+
done
|
|
1846
|
+
printf '%s %s\n' "$st" "$3" 2>/dev/null > "$dir/.$2.$$" \
|
|
1847
|
+
&& mv -f "$dir/.$2.$$" "$dir/$2" 2>/dev/null || rm -f "$dir/.$2.$$" 2>/dev/null
|
|
1848
|
+
return 0
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
# The login a dir's .credentials.json carries (cred_login_of) — a file holding only MCP
|
|
1852
|
+
# state, or an empty one, carries none. Memoised for the dir like kc_lookup: has_auth,
|
|
1853
|
+
# auth_dead and acct_token all read it in a row, and the read is a fork.
|
|
1854
|
+
FILE_MEMO_DIR=""; FILE_MEMO_RC=1; FILE_LOGIN=""
|
|
1855
|
+
file_login() { # $1 = acct dir; sets CRED_LOGIN; rc 0 when the file holds a login
|
|
1856
|
+
if [ "$FILE_MEMO_DIR" != "$1" ]; then
|
|
1857
|
+
local doc=""
|
|
1858
|
+
FILE_MEMO_DIR="$1"; FILE_MEMO_RC=1; FILE_LOGIN=""
|
|
1859
|
+
if [ -s "$1/.credentials.json" ]; then
|
|
1860
|
+
{ doc="$(< "$1/.credentials.json")"; } 2>/dev/null
|
|
1861
|
+
cred_login_of "$doc" && { FILE_LOGIN="$CRED_LOGIN"; FILE_MEMO_RC=0; }
|
|
1862
|
+
fi
|
|
1863
|
+
fi
|
|
1864
|
+
CRED_LOGIN="$FILE_LOGIN"
|
|
1865
|
+
return "$FILE_MEMO_RC"
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1592
1868
|
# An empty credentials file is NOT auth (an interrupted write must not make a
|
|
1593
|
-
# dead account selectable and turn a working stock run into an auth failure)
|
|
1869
|
+
# dead account selectable and turn a working stock run into an auth failure), and
|
|
1870
|
+
# neither is one holding only MCP state: it leaves the decision to the token/Keychain.
|
|
1594
1871
|
# A Keychain-held login counts only when THIS session can read it (kc_lookup rc 0).
|
|
1595
|
-
has_auth() {
|
|
1872
|
+
has_auth() { file_login "$1" || [ -s "$1/server.token" ] || kc_lookup "$1"; }
|
|
1596
1873
|
# The OAuth login itself, wherever the client keeps it (file or readable Keychain item).
|
|
1597
|
-
has_oauth() {
|
|
1874
|
+
has_oauth() { file_login "$1" || kc_lookup "$1"; }
|
|
1598
1875
|
|
|
1599
1876
|
cred_num() { # cred_num <json text> <json key> -> integer (epoch ms) or empty
|
|
1600
1877
|
# The leading quote makes "expiresAt" unambiguous: "refreshTokenExpiresAt" cannot match it.
|
|
1601
1878
|
printf '%s' "$1" | LC_ALL=C sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p" 2>/dev/null | head -1
|
|
1602
1879
|
}
|
|
1603
1880
|
|
|
1604
|
-
# The
|
|
1605
|
-
#
|
|
1606
|
-
# this session can read.
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
elif kc_lookup "$1"; then
|
|
1611
|
-
printf '%s' "$KC_JSON"
|
|
1612
|
-
fi
|
|
1881
|
+
# The login object for <dir>: from .credentials.json when that file carries one (even
|
|
1882
|
+
# truncated — the scrape below is deliberately lenient), else from the Keychain item
|
|
1883
|
+
# this session can read. rc 1, CRED_LOGIN empty, when neither holds a login.
|
|
1884
|
+
cred_login() { # $1 = acct dir; sets CRED_LOGIN
|
|
1885
|
+
file_login "$1" && return 0
|
|
1886
|
+
kc_lookup "$1" && cred_login_of "$KC_JSON"
|
|
1613
1887
|
}
|
|
1614
1888
|
|
|
1615
1889
|
# TRUE when the dir's OAuth login exists but can no longer authenticate: the access
|
|
@@ -1617,17 +1891,17 @@ cred_text() { # $1 = acct dir
|
|
|
1617
1891
|
# itself expired). Such an account fails EVERY run with "OAuth session expired and
|
|
1618
1892
|
# could not be refreshed", so selecting it is strictly worse than not having it.
|
|
1619
1893
|
# Anything unparseable fails OPEN (treated as alive) — telemetry-free guesswork must
|
|
1620
|
-
# never take a working account out of the pool.
|
|
1894
|
+
# never take a working account out of the pool. Read from the login object only: the
|
|
1895
|
+
# MCP entries beside it carry an expiresAt and a refreshToken that say nothing about it.
|
|
1621
1896
|
creds_dead() { # $1 = acct dir
|
|
1622
|
-
local
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
exp="$(cred_num "$src" expiresAt)"
|
|
1897
|
+
local exp rexp
|
|
1898
|
+
cred_login "$1" || return 1
|
|
1899
|
+
exp="$(cred_num "$CRED_LOGIN" expiresAt)"
|
|
1626
1900
|
case "$exp" in ''|*[!0-9]*) exp=0 ;; esac
|
|
1627
1901
|
[ $((exp / 1000)) -gt "$now" ] && return 1 # access token still valid
|
|
1628
1902
|
# Access token expired: only a live refresh token can save it.
|
|
1629
|
-
printf '%s' "$
|
|
1630
|
-
rexp="$(cred_num "$
|
|
1903
|
+
printf '%s' "$CRED_LOGIN" | grep -q '"refreshToken"[[:space:]]*:[[:space:]]*"[^"]' 2>/dev/null || return 0
|
|
1904
|
+
rexp="$(cred_num "$CRED_LOGIN" refreshTokenExpiresAt)"
|
|
1631
1905
|
case "$rexp" in ''|*[!0-9]*) return 1 ;; esac # unknown expiry => fail open
|
|
1632
1906
|
[ $((rexp / 1000)) -le "$now" ]
|
|
1633
1907
|
}
|
|
@@ -1635,8 +1909,9 @@ creds_dead() { # $1 = acct dir
|
|
|
1635
1909
|
# `.expired` is the persistent "this account needs a re-login" marker: written by
|
|
1636
1910
|
# claude-accounts (refresh grant expired/revoked) and by the retry path below when a
|
|
1637
1911
|
# real call fails with an auth error. It SELF-HEALS: any credential written after the
|
|
1638
|
-
# marker (successful re-login, or a refresh by another process
|
|
1639
|
-
# successful `claude-accounts limits` fetch removes it
|
|
1912
|
+
# marker (successful re-login, or a refresh by another process — a login or a token, never
|
|
1913
|
+
# MCP state alone) clears it, and a successful `claude-accounts limits` fetch removes it
|
|
1914
|
+
# outright.
|
|
1640
1915
|
# A marker written by the SHIM (a guess from one failed run) also carries
|
|
1641
1916
|
# `soft_until=<epoch>`: once that passes the account returns to the pool by itself, so
|
|
1642
1917
|
# a misread never costs an account permanently. Markers written by claude-accounts —
|
|
@@ -1645,7 +1920,7 @@ creds_dead() { # $1 = acct dir
|
|
|
1645
1920
|
# An org block stays until verify proves access, including a usage-limiter refusal.
|
|
1646
1921
|
# Time and credential refreshes cannot prove the organization's policy changed.
|
|
1647
1922
|
expired_marked() { # $1 = acct dir
|
|
1648
|
-
local m="$1/.expired" mt
|
|
1923
|
+
local m="$1/.expired" mt soft reason kc
|
|
1649
1924
|
[ -f "$m" ] || return 1
|
|
1650
1925
|
reason="$(LC_ALL=C sed -n 's/.*reason=\([A-Za-z0-9._-][A-Za-z0-9._-]*\).*/\1/p' "$m" 2>/dev/null | head -1)"
|
|
1651
1926
|
[ "$reason" = "org-blocked" ] && return 0
|
|
@@ -1681,17 +1956,27 @@ expired_marked() { # $1 = acct dir
|
|
|
1681
1956
|
# "Your organization has disabled Claude subscription access".
|
|
1682
1957
|
if [ "$reason" != "org-blocked" ]; then
|
|
1683
1958
|
mt="$(file_mtime "$m")"
|
|
1684
|
-
|
|
1685
|
-
|
|
1959
|
+
if [ -f "$1/server.token" ] && [ "$(file_mtime "$1/server.token")" -gt "$mt" ]; then
|
|
1960
|
+
rm -f "$m" 2>/dev/null
|
|
1961
|
+
return 1
|
|
1962
|
+
fi
|
|
1963
|
+
# A newer LOGIN is the evidence. The client rewrites .credentials.json with MCP state
|
|
1964
|
+
# alone on every MCP token refresh, which says nothing about the login, so only a
|
|
1965
|
+
# file that carries one can heal the park; any other leaves it to the Keychain.
|
|
1966
|
+
if file_login "$1"; then
|
|
1967
|
+
if [ "$(file_mtime "$1/.credentials.json")" -gt "$mt" ]; then
|
|
1686
1968
|
rm -f "$m" 2>/dev/null
|
|
1687
1969
|
return 1
|
|
1688
1970
|
fi
|
|
1689
|
-
done
|
|
1690
1971
|
# A login the client keeps in the Keychain is "written after the marker" in the
|
|
1691
|
-
# same sense; its stamp is readable even from a session that cannot open it.
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1972
|
+
# same sense; its stamp is readable even from a session that cannot open it. An item
|
|
1973
|
+
# this session CAN read is judged by what it holds: MCP state alone heals nothing.
|
|
1974
|
+
elif [ "$(kc_mtime "$1")" -gt "$mt" ]; then
|
|
1975
|
+
kc=0; kc_lookup "$1" || kc=$?
|
|
1976
|
+
if [ "$kc" -ne 1 ]; then
|
|
1977
|
+
rm -f "$m" 2>/dev/null
|
|
1978
|
+
return 1
|
|
1979
|
+
fi
|
|
1695
1980
|
fi
|
|
1696
1981
|
fi
|
|
1697
1982
|
soft="$(LC_ALL=C sed -n 's/.*soft_until=\([0-9][0-9]*\).*/\1/p' "$m" 2>/dev/null | head -1)"
|
|
@@ -1824,11 +2109,13 @@ if [ -n "${CLAUDE_ACCOUNT:-}" ]; then
|
|
|
1824
2109
|
fi
|
|
1825
2110
|
export CLAUDE_CODE_OAUTH_TOKEN="$tok"
|
|
1826
2111
|
fi
|
|
1827
|
-
# A pinned account carries every registered MCP server
|
|
1828
|
-
# `claude mcp add|remove` is mirrored like any other
|
|
2112
|
+
# A pinned account carries every registered MCP server (and the pool's MCP OAuth
|
|
2113
|
+
# logins) too, and a pinned `claude mcp add|remove` is mirrored like any other
|
|
2114
|
+
# (exits inside).
|
|
1829
2115
|
mcp_ensure "$d"
|
|
2116
|
+
mcp_oauth_ensure "$d"
|
|
1830
2117
|
mcp_mirror_run "$d" "$@" || true
|
|
1831
|
-
sel_capture_session "$d"
|
|
2118
|
+
sel_capture_session "$d" "${tok:+token}"
|
|
1832
2119
|
exec "$REAL" "$@"
|
|
1833
2120
|
fi
|
|
1834
2121
|
sel_log "pin-invalid account=$CLAUDE_ACCOUNT (no such dir; random fallback)"
|
|
@@ -1854,7 +2141,7 @@ for d in "$ACC_ROOT"/acct-*; do
|
|
|
1854
2141
|
mark_proven_auth_dead "$d" "organization disabled Claude Code subscription access" "org-blocked"
|
|
1855
2142
|
sel_log "${d##*/} parked (org-blocked) — client-reported"
|
|
1856
2143
|
else
|
|
1857
|
-
mark_client_auth_dead "$d"
|
|
2144
|
+
mark_client_auth_dead "$d" "$CLIENT_ERROR_VIA"
|
|
1858
2145
|
fi
|
|
1859
2146
|
valid=("${valid[@]:0:${#valid[@]}-1}")
|
|
1860
2147
|
expired+=("$d")
|
|
@@ -2192,20 +2479,27 @@ PICK_TOKEN="$tok"
|
|
|
2192
2479
|
# session on "You've reached your Fable 5 limit" — the exact failure this selection
|
|
2193
2480
|
# path exists to avoid (operator, tmux 136) — so the fallback model is pinned once,
|
|
2194
2481
|
# before exec. An explicit --model is never touched: such a run does not get here.
|
|
2482
|
+
# A relaunch names the model its session last ran on, which --resume restores: only a
|
|
2483
|
+
# session on the limited model itself is pinned off it; any other keeps its own model.
|
|
2195
2484
|
if [ -z "$RUN_MODEL" ]; then
|
|
2196
2485
|
sel_tok="$(scoped_tok_of_marker "$pick")"
|
|
2197
2486
|
[ -n "$sel_tok" ] || sel_tok="$(scoped_over_tok "$pick")"
|
|
2198
2487
|
if [ -n "$sel_tok" ] && ! scoped_blocks_run "$sel_tok"; then
|
|
2199
|
-
|
|
2200
|
-
|
|
2488
|
+
case "$(sel_lc "${AR_R_MODEL:-$sel_tok}")" in
|
|
2489
|
+
*"$(sel_lc "$sel_tok")"*)
|
|
2490
|
+
set -- --model "$FALLBACK_MODEL" "$@"
|
|
2491
|
+
sel_log "$(basename "$pick") -> --model $FALLBACK_MODEL (its $sel_tok bucket is full)" ;;
|
|
2492
|
+
esac
|
|
2201
2493
|
fi
|
|
2202
2494
|
fi
|
|
2203
2495
|
# Remember the pick so the NEXT run does not hand back the same account. An explicit
|
|
2204
2496
|
# CLAUDE_ACCOUNT pin deliberately does not: a pin is a caller overriding selection,
|
|
2205
2497
|
# not a turn in the rotation.
|
|
2206
2498
|
remember_pick "$pick"
|
|
2207
|
-
# The picked account carries every registered MCP server
|
|
2499
|
+
# The picked account carries every registered MCP server, and the MCP OAuth logins any
|
|
2500
|
+
# account on this machine holds, before the client starts.
|
|
2208
2501
|
mcp_ensure "$pick"
|
|
2502
|
+
mcp_oauth_ensure "$pick"
|
|
2209
2503
|
|
|
2210
2504
|
# Opportunistic limits refresh: non-blocking, throttled, backgrounded. The windows are
|
|
2211
2505
|
# deliberately wide (10m, matching the 5m scheduled pass): the usage endpoint rate-limits
|
|
@@ -2284,7 +2578,7 @@ export CLAUDE_SHIM_ACTIVE=1
|
|
|
2284
2578
|
|
|
2285
2579
|
# A `claude mcp add|add-json|remove` is run as a child under the picked account and
|
|
2286
2580
|
# mirrored to every other one (exits inside); anything else falls through to exec.
|
|
2287
|
-
if
|
|
2581
|
+
if mcp_mutation_argv "$@"; then
|
|
2288
2582
|
export CLAUDE_CONFIG_DIR="$pick"
|
|
2289
2583
|
tok="$PICK_TOKEN"
|
|
2290
2584
|
[ -n "$tok" ] && export CLAUDE_CODE_OAUTH_TOKEN="$tok"
|
|
@@ -2320,9 +2614,9 @@ if [ "$wants_retry" = "0" ]; then
|
|
|
2320
2614
|
export CLAUDE_CONFIG_DIR="$pick"
|
|
2321
2615
|
tok="$PICK_TOKEN"
|
|
2322
2616
|
[ -n "$tok" ] && export CLAUDE_CODE_OAUTH_TOKEN="$tok"
|
|
2323
|
-
sel_capture_session "$pick"
|
|
2617
|
+
sel_capture_session "$pick" "${tok:+token}"
|
|
2324
2618
|
ar_trust "$pick"
|
|
2325
|
-
if ! ar_spawn_watcher claude "$pick" && [ -n "$AR_HANDOFF_TOKEN" ]; then
|
|
2619
|
+
if ! ar_spawn_watcher claude "$pick" "${tok:+token}" && [ -n "$AR_HANDOFF_TOKEN" ]; then
|
|
2326
2620
|
printf 'claude-multiacc: auto-resume handoff could not start its watcher.\n' >&2
|
|
2327
2621
|
exit 2
|
|
2328
2622
|
fi
|
|
@@ -2335,7 +2629,7 @@ tmpd="$(mktemp -d "$ACC_ROOT/tmp/shim.XXXXXX" 2>/dev/null)" || {
|
|
|
2335
2629
|
export CLAUDE_CONFIG_DIR="$pick"
|
|
2336
2630
|
tok="$PICK_TOKEN"
|
|
2337
2631
|
[ -n "$tok" ] && export CLAUDE_CODE_OAUTH_TOKEN="$tok"
|
|
2338
|
-
sel_capture_session "$pick"
|
|
2632
|
+
sel_capture_session "$pick" "${tok:+token}"
|
|
2339
2633
|
exec "$REAL" "$@"
|
|
2340
2634
|
}
|
|
2341
2635
|
trap 'rm -rf "$tmpd"' EXIT
|
|
@@ -2347,7 +2641,7 @@ if ! : > "$tmpd/out" 2>/dev/null || ! : > "$tmpd/err" 2>/dev/null; then
|
|
|
2347
2641
|
export CLAUDE_CONFIG_DIR="$pick"
|
|
2348
2642
|
tok="$PICK_TOKEN"
|
|
2349
2643
|
[ -n "$tok" ] && export CLAUDE_CODE_OAUTH_TOKEN="$tok"
|
|
2350
|
-
sel_capture_session "$pick"
|
|
2644
|
+
sel_capture_session "$pick" "${tok:+token}"
|
|
2351
2645
|
exec "$REAL" "$@"
|
|
2352
2646
|
fi
|
|
2353
2647
|
|
|
@@ -2530,6 +2824,7 @@ while :; do
|
|
|
2530
2824
|
# away from — not the one that bounced.
|
|
2531
2825
|
remember_pick "$cur"
|
|
2532
2826
|
mcp_ensure "$cur"
|
|
2827
|
+
mcp_oauth_ensure "$cur"
|
|
2533
2828
|
rotated=1
|
|
2534
2829
|
attempt=2
|
|
2535
2830
|
continue
|