claude-multiacc 2.0.61 → 2.0.64
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 +6 -2
- package/bin/codex +30 -7
- package/bin/codex-accounts +12 -4
- package/docs/ACCOUNT_OPERATIONS.md +2 -2
- package/docs/AUTORESUME.md +6 -1
- package/docs/CODEX.md +19 -2
- package/docs/CODEX_MCP_AUTH.md +58 -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_mcp_oauth.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_resume_cwd.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/autoresume.py +6 -5
- package/lib/autoresume_cross.py +2 -1
- package/lib/autoresume_handoff.py +13 -5
- package/lib/codex_exec_stdin.sh +45 -0
- package/lib/codex_mcp_oauth.py +204 -0
- package/lib/codex_mcp_oauth.sh +29 -0
- package/lib/codex_mcp_oauth_locks.py +158 -0
- package/lib/codex_mcp_oauth_native.py +102 -0
- package/lib/codex_mcp_oauth_store.py +149 -0
- package/lib/codex_resume_cwd.py +57 -0
- package/lib/codex_retry_process.sh +37 -0
- package/lib/common.sh +3 -0
- package/package.json +9 -4
- 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_cwd.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_handoff.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_reader.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_tmux.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_exec_stdin.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_mcp_oauth.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/fake_codex_tui.py +23 -1
- package/tests/test_autoresume_cwd.py +71 -0
- package/tests/test_autoresume_cwd_handoff.py +92 -0
- package/tests/test_autoresume_cwd_reader.py +105 -0
- package/tests/test_autoresume_cwd_tmux.py +54 -0
- package/tests/test_codex_exec_stdin.py +178 -0
- package/tests/test_codex_mcp_oauth.py +146 -0
- package/tests/test_codex_mcp_oauth_adopt.py +87 -0
- package/tests/test_codex_mcp_oauth_migration.py +141 -0
- package/tests/test_codex_mcp_oauth_native.py +109 -0
- package/tests/test_codex_mcp_oauth_safety.py +130 -0
- package/tests/test_codex_mcp_oauth_shim.py +188 -0
- package/tests/test_codex_retry_process.py +143 -0
package/README.md
CHANGED
|
@@ -685,8 +685,12 @@ claude-accounts mcp auth-forget higgsfield # log it out of EVERY account on th
|
|
|
685
685
|
cannot open) stays ignored and is cleared by a later pass.
|
|
686
686
|
- **Kill switches**: `CLAUDE_MULTIACC_MCP_OAUTH=0` stops sharing (the shim's merge and the
|
|
687
687
|
limits pass's sync); `CLAUDE_MULTIACC_MCP_OAUTH_REFRESH=0` never refreshes — and so shares
|
|
688
|
-
nothing. `auth-status` and `auth-forget` always run.
|
|
689
|
-
|
|
688
|
+
nothing. `auth-status` and `auth-forget` always run. These commands describe the Claude pool.
|
|
689
|
+
|
|
690
|
+
Codex shares its native MCP credential file and refresh locks locally, so a login in
|
|
691
|
+
one pooled account works in the others. Run `codex-accounts mcp auth-sync` to repair
|
|
692
|
+
sharing now; use native `codex mcp login NAME`, `list`, and `logout NAME` for authentication.
|
|
693
|
+
See [Codex MCP authentication](docs/CODEX_MCP_AUTH.md) for storage constraints and recovery.
|
|
690
694
|
|
|
691
695
|
app-robot tasks are different: the panel launches Claude with `--strict-mcp-config` and a
|
|
692
696
|
merged MCP document (`agent_mcp.servers_for`) and Codex with the same document as `-c`
|
package/bin/codex
CHANGED
|
@@ -93,6 +93,11 @@ fi
|
|
|
93
93
|
# the servers themselves. Same contract as bin/claude's mcp_ensure.
|
|
94
94
|
MCP_PY="$SELF_DIR/../lib/mcp_registry.py"
|
|
95
95
|
MCP_PYBIN="${CLAUDE_MULTIACC_PYTHON:-python3}"
|
|
96
|
+
mcp_oauth_ensure() {
|
|
97
|
+
[ -f "$SELF_DIR/../lib/codex_mcp_oauth.sh" ] || return 0
|
|
98
|
+
bash "$SELF_DIR/../lib/codex_mcp_oauth.sh" "$ACC_ROOT" "$MCP_PYBIN" \
|
|
99
|
+
"$SELF_DIR/../lib/codex_mcp_oauth.py" "$1" "$REAL" >/dev/null 2>&1 </dev/null || true
|
|
100
|
+
}
|
|
96
101
|
mcp_sig() { # $1 acct dir -> "m:z,m:z,m:z" (registry, overlay, account config; - = absent)
|
|
97
102
|
local f out="" s
|
|
98
103
|
for f in "$ACC_ROOT/mcp-servers.json" "$ACC_ROOT/mcp-servers.local.json" "$1/config.toml"; do
|
|
@@ -243,6 +248,9 @@ if [ -n "${CODEX_HOME:-}" ] \
|
|
|
243
248
|
printf 'codex-multiacc: auto-resume cannot enter account selection.\n' >&2
|
|
244
249
|
exit 2
|
|
245
250
|
fi
|
|
251
|
+
if [ "${CODEX_MULTIACC_DISABLE:-0}" != "1" ] && [ -n "${CODEX_HOME:-}" ]; then
|
|
252
|
+
mcp_oauth_ensure "$CODEX_HOME"
|
|
253
|
+
fi
|
|
246
254
|
mcp_nested_mirror "$@" || true
|
|
247
255
|
exec "$REAL" "$@"
|
|
248
256
|
fi
|
|
@@ -1258,9 +1266,11 @@ if [ -n "${CODEX_ACCOUNT:-}" ] && [ "$AR_PROBE" != "1" ]; then
|
|
|
1258
1266
|
share_state_index "$d"
|
|
1259
1267
|
export CODEX_HOME="$d"
|
|
1260
1268
|
export CODEX_SHIM_ACTIVE=1
|
|
1269
|
+
unset CODEX_MULTIACC_SELECTED_HOME
|
|
1261
1270
|
# A pinned account carries every registered MCP server too, and a pinned
|
|
1262
1271
|
# `codex mcp add|remove` is mirrored like any other (exits inside).
|
|
1263
1272
|
mcp_ensure "$d"
|
|
1273
|
+
mcp_oauth_ensure "$d"
|
|
1264
1274
|
mcp_mirror_run "$d" "$@" || true
|
|
1265
1275
|
exec "$REAL" "$@"
|
|
1266
1276
|
fi
|
|
@@ -1551,6 +1561,7 @@ if [ "$AR_TIER" = "credits" ]; then
|
|
|
1551
1561
|
fi
|
|
1552
1562
|
# The picked account carries every registered MCP server before the client starts.
|
|
1553
1563
|
mcp_ensure "$pick"
|
|
1564
|
+
mcp_oauth_ensure "$pick"
|
|
1554
1565
|
|
|
1555
1566
|
# Opportunistic limits refresh: non-blocking, throttled, backgrounded. The windows are
|
|
1556
1567
|
# deliberately wide (10m, matching the 5m scheduled pass): the usage endpoint rate-limits
|
|
@@ -1581,6 +1592,9 @@ sel_log "$acct weekly=$(fresh_field "$pick" weekly_percent || echo '?')%" \
|
|
|
1581
1592
|
"session-ok=${PICK_GATE_COUNT} pwd=$PWD${credit_funding_log}"
|
|
1582
1593
|
|
|
1583
1594
|
export CODEX_SHIM_ACTIVE=1
|
|
1595
|
+
# An isolated child job can distinguish this automatic selection from a caller pin.
|
|
1596
|
+
# Consumers must also require CODEX_HOME to match and respect explicit pins/disable.
|
|
1597
|
+
export CODEX_MULTIACC_SELECTED_HOME="$pick"
|
|
1584
1598
|
|
|
1585
1599
|
# A `codex mcp add|remove` is run as a child under the picked account and mirrored to
|
|
1586
1600
|
# every other one (exits inside); anything else falls through to exec.
|
|
@@ -1591,9 +1605,9 @@ if mcp_mutation_argv "$@"; then
|
|
|
1591
1605
|
fi
|
|
1592
1606
|
|
|
1593
1607
|
# Auto-retry applies only to `codex exec` runs with an alternative account available,
|
|
1594
|
-
# and
|
|
1595
|
-
#
|
|
1596
|
-
#
|
|
1608
|
+
# and stdin is a file/device or an explicit `exec ... -` prompt. That prompt needs
|
|
1609
|
+
# EOF in the native CLI too. Other pipes stay unbuffered so a non-consuming command
|
|
1610
|
+
# can finish even when its service leaves stdin open.
|
|
1597
1611
|
wants_retry=0
|
|
1598
1612
|
if [ "${CODEX_SHIM_RETRY:-1}" != "0" ] \
|
|
1599
1613
|
&& { [ "${#eligible[@]}" -ge 2 ] || [ "$AR_TIER" = "credits" ]; }; then
|
|
@@ -1603,19 +1617,26 @@ if [ "${CODEX_SHIM_RETRY:-1}" != "0" ] \
|
|
|
1603
1617
|
if [ "$wants_retry" = "1" ]; then
|
|
1604
1618
|
# A TTY cannot be buffered or replayed: `codex exec` with no prompt argument reads
|
|
1605
1619
|
# stdin, and the retry path would hand it /dev/null. Plain exec instead —
|
|
1606
|
-
# stdin is inherited untouched.
|
|
1607
|
-
#
|
|
1608
|
-
#
|
|
1620
|
+
# stdin is inherited untouched. Explicit stdin prompts can replay a pipe: an
|
|
1621
|
+
# AppInspire/Node child receives a complete prompt through a socket, not a file.
|
|
1622
|
+
# Keep unknown commands and pipes the CLI need not consume on direct exec.
|
|
1609
1623
|
if [ -t 0 ]; then
|
|
1610
1624
|
wants_retry=0
|
|
1611
1625
|
elif [ -f /dev/fd/0 ] || [ -c /dev/fd/0 ]; then
|
|
1612
1626
|
:
|
|
1627
|
+
elif bash "$SELF_DIR/../lib/codex_exec_stdin.sh" "$@" 2>/dev/null; then
|
|
1628
|
+
:
|
|
1613
1629
|
else
|
|
1614
1630
|
wants_retry=0
|
|
1615
1631
|
fi
|
|
1616
1632
|
fi
|
|
1617
1633
|
fi
|
|
1618
1634
|
|
|
1635
|
+
# Keep a damaged installation fail-open rather than wrapping an unowned child.
|
|
1636
|
+
if [ "$wants_retry" = 1 ] && ! . "$SELF_DIR/../lib/codex_retry_process.sh" 2>/dev/null; then
|
|
1637
|
+
wants_retry=0
|
|
1638
|
+
fi
|
|
1639
|
+
|
|
1619
1640
|
share_state_index "$pick"
|
|
1620
1641
|
if [ "$wants_retry" = "0" ]; then
|
|
1621
1642
|
export CODEX_HOME="$pick"
|
|
@@ -1687,7 +1708,8 @@ cur="$pick"
|
|
|
1687
1708
|
rc=0
|
|
1688
1709
|
while :; do
|
|
1689
1710
|
if [ -n "$stdin_file" ]; then exec 3< "$stdin_file"; else exec 3< /dev/null; fi
|
|
1690
|
-
CODEX_HOME="$cur" "$
|
|
1711
|
+
CODEX_HOME="$cur" CODEX_MULTIACC_SELECTED_HOME="$cur" \
|
|
1712
|
+
codex_retry_run "$REAL" "$@" <&3 > "$tmpd/out" 2> "$tmpd/err"
|
|
1691
1713
|
rc=$?
|
|
1692
1714
|
exec 3<&-
|
|
1693
1715
|
if [ "$rc" -ne 0 ] && [ "$attempt" -eq 1 ] \
|
|
@@ -1749,6 +1771,7 @@ while :; do
|
|
|
1749
1771
|
# away from — not the one that bounced.
|
|
1750
1772
|
remember_pick "$cur"
|
|
1751
1773
|
mcp_ensure "$cur"
|
|
1774
|
+
mcp_oauth_ensure "$cur"
|
|
1752
1775
|
attempt=2
|
|
1753
1776
|
continue
|
|
1754
1777
|
fi
|
package/bin/codex-accounts
CHANGED
|
@@ -133,6 +133,8 @@ USAGE
|
|
|
133
133
|
re-apply the registry now (repair drift)
|
|
134
134
|
codex-accounts mcp import-local --owner NAME (stdin JSON) machine-local overlay for
|
|
135
135
|
a runner daemon; applied here, never synced
|
|
136
|
+
codex-accounts mcp auth-sync share native MCP files and refresh locks locally
|
|
137
|
+
Sign in with `codex mcp login NAME`; sign out with `codex mcp logout NAME`.
|
|
136
138
|
|
|
137
139
|
ENV
|
|
138
140
|
CODEX_ACCOUNTS_ROOT pool root, overriding ~/.codex-accounts — one isolated pool
|
|
@@ -146,6 +148,7 @@ ENV
|
|
|
146
148
|
CODEX_MULTIACC_DISABLE=1 bypass the shim entirely
|
|
147
149
|
CODEX_MULTIACC_AUTO_RESET=0 disable automatic usage-reset redemption
|
|
148
150
|
CODEX_MULTIACC_MCP=0 disable the MCP registry (no reconcile at seed/launch, no mirror)
|
|
151
|
+
CODEX_MULTIACC_MCP_OAUTH=0 stop preparing shared native MCP storage and locks
|
|
149
152
|
CODEX_MULTIACC_MODEL_CATALOG=0 never refresh the codex pool's model-catalog.json
|
|
150
153
|
(MULTIACC_MODEL_CATALOG=0: neither pool's)
|
|
151
154
|
EOF
|
|
@@ -1848,6 +1851,7 @@ PYEOF
|
|
|
1848
1851
|
trap - EXIT
|
|
1849
1852
|
# Every account's MCP servers, adopting what one holds that the registry does not know.
|
|
1850
1853
|
mcp_registry_pass
|
|
1854
|
+
codex_mcp_oauth_ensure "$ACC_ROOT" "$PYBIN" "$LIB_DIR/codex_mcp_oauth.py"
|
|
1851
1855
|
# This pool's model catalog, once its copy is an hour old (lib/common.sh).
|
|
1852
1856
|
model_catalog_backstop
|
|
1853
1857
|
# The refresher fails open per account; a NON-zero status means the pass itself
|
|
@@ -2264,10 +2268,14 @@ cmd_mcp() {
|
|
|
2264
2268
|
local args=()
|
|
2265
2269
|
case "$sub" in
|
|
2266
2270
|
add|add-json|remove|list|apply|import-local) shift ;;
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
+
auth-sync)
|
|
2272
|
+
shift
|
|
2273
|
+
local real
|
|
2274
|
+
real="$(find_real_codex "$_self")" || die "real codex binary not found"
|
|
2275
|
+
"$PYBIN" "$LIB_DIR/codex_mcp_oauth.py" ensure --root "$ACC_ROOT" --codex-bin "$real" "$@"
|
|
2276
|
+
return $? ;;
|
|
2277
|
+
auth-status|auth-forget)
|
|
2278
|
+
die "use codex mcp list for native auth status, or codex mcp logout NAME to clear shared MCP authentication" ;;
|
|
2271
2279
|
"") usage; return 1 ;;
|
|
2272
2280
|
*) die "unknown mcp subcommand: $sub (add, add-json, remove, list, apply, import-local)" ;;
|
|
2273
2281
|
esac
|
|
@@ -260,8 +260,8 @@ What to expect, and how to read `auth-status`:
|
|
|
260
260
|
re-authentication in ANY one account revokes the shared login at the server, so the pool
|
|
261
261
|
retires it in every account (`login: none — retired … (cleared in acct-NN file)`); one
|
|
262
262
|
sign-in in any account shares the new one. `mcp auth-forget NAME` is the same logout from
|
|
263
|
-
the operator's side.
|
|
264
|
-
|
|
263
|
+
the operator's side. Codex uses native login/logout over shared local storage;
|
|
264
|
+
`codex-accounts mcp auth-sync` repairs it. See [Codex MCP authentication](CODEX_MCP_AUTH.md).
|
|
265
265
|
- **Rolling it out**: the first share runs at install time (the limits agent starts on
|
|
266
266
|
install), and a build older than this one reads a shared MCP-only file as a live login:
|
|
267
267
|
every setup-token account would then say "Not logged in". Install it only where no older
|
package/docs/AUTORESUME.md
CHANGED
|
@@ -164,7 +164,12 @@ shell: runs the shim again → token → marker on the old account → normal
|
|
|
164
164
|
1. It consumes the token. A token is single-use and valid for 10 minutes.
|
|
165
165
|
2. It writes the old account's marker with the pool's existing writers (see the tables
|
|
166
166
|
below).
|
|
167
|
-
3. It `cd`s to the session's directory.
|
|
167
|
+
3. It `cd`s to the session's directory. For Codex, this is the latest directory saved
|
|
168
|
+
by that session, including a change since its first turn. Starting or resuming it
|
|
169
|
+
from another shell directory therefore does not stop the automatic restart at
|
|
170
|
+
**Working directory · resume**. The selected account trusts this same directory
|
|
171
|
+
before startup. If the saved directory is unavailable, an existing initial session
|
|
172
|
+
directory or launch directory is used as a fallback.
|
|
168
173
|
4. It runs **normal selection**, with one addition: the accounts this chain of
|
|
169
174
|
relaunches left are skipped until their own reset. The selection policy is
|
|
170
175
|
otherwise untouched.
|
package/docs/CODEX.md
CHANGED
|
@@ -18,8 +18,21 @@ then the 30-point weekly-headroom band with random spread, ≥90% any-bucket
|
|
|
18
18
|
exclusion, peers rotate), same marker semantics (`.limited` cooldowns, `.expired` parks with
|
|
19
19
|
credential/policy scoping and soft expiry), same fail-open guarantees, same sync
|
|
20
20
|
safety guards. The `codex` shim engages the buffered auto-retry only for
|
|
21
|
-
`codex exec` runs with
|
|
22
|
-
|
|
21
|
+
`codex exec` runs with file/device stdin and explicit `codex exec ... -` prompts from pipes.
|
|
22
|
+
The latter require EOF in the native CLI too; the shim saves and replays the exact prompt
|
|
23
|
+
on its one account retry. Unknown commands, interactive stdin and non-consuming live pipes
|
|
24
|
+
remain unbuffered. Explicit `CODEX_HOME` and `CODEX_ACCOUNT` pins still bypass rotation.
|
|
25
|
+
Cancelling a buffered run forwards TERM/INT/HUP to its active native process and reaps it
|
|
26
|
+
before removing the temporary prompt/output files.
|
|
27
|
+
Interactive `codex` in tmux gets [auto-resume](AUTORESUME.md) instead, like `claude`.
|
|
28
|
+
|
|
29
|
+
Automatic selection exports `CODEX_MULTIACC_SELECTED_HOME` equal to the serving
|
|
30
|
+
`CODEX_HOME`, including after a retry. Selecting an explicit `CODEX_ACCOUNT` clears the marker;
|
|
31
|
+
a home passthrough never creates it. Nested invocations still pass through.
|
|
32
|
+
An isolated job launcher may request fresh pool selection only when `CODEX_SHIM_ACTIVE=1`,
|
|
33
|
+
the marker exactly matches `CODEX_HOME`, no `CODEX_ACCOUNT` is set and
|
|
34
|
+
`CODEX_MULTIACC_DISABLE` is not `1`. Omit the inherited home/guard/marker only for that
|
|
35
|
+
child. Unmarked older sessions and overridden homes retain their existing pin behavior.
|
|
23
36
|
|
|
24
37
|
Codex-specific notes:
|
|
25
38
|
|
|
@@ -215,6 +228,10 @@ claude-multiacc codex status # same status command via the npm wrapper
|
|
|
215
228
|
|
|
216
229
|
## MCP servers for every account
|
|
217
230
|
|
|
231
|
+
OAuth logins also need shared storage; see
|
|
232
|
+
[Codex MCP authentication](CODEX_MCP_AUTH.md). `codex-accounts mcp auth-sync` repairs
|
|
233
|
+
local credential and refresh-lock sharing without touching account `auth.json` files.
|
|
234
|
+
|
|
218
235
|
`codex-accounts mcp add|add-json|remove|list|apply|import-local` manages the pool's MCP
|
|
219
236
|
registry (`~/.codex-accounts/mcp-servers.json`); `add`/`remove` default to `--provider both`,
|
|
220
237
|
so the claude pool beside it gets the same change, and `mcp remove` retires the server in
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Codex MCP authentication across pooled accounts
|
|
2
|
+
|
|
3
|
+
The MCP registry shares server configuration. Codex stores OAuth logins separately:
|
|
4
|
+
the direct OS keyring is shared, but its fallback `.credentials.json` belongs to
|
|
5
|
+
`CODEX_HOME`. On 2026-10-23 MotionSites was configured in all 18 accounts, while its
|
|
6
|
+
login existed only in `acct-06`. The other 17 reported `not_logged_in`.
|
|
7
|
+
|
|
8
|
+
The addon prepares one machine-local native MCP credential file and one directory
|
|
9
|
+
of native refresh/store locks under `<pool>/.codex-mcp-oauth/`. Accounts hardlink
|
|
10
|
+
`.credentials.json` to that file and link `mcp-oauth-locks` to the shared locks.
|
|
11
|
+
Codex reads, refreshes, saves and logs out using its own implementation. The addon
|
|
12
|
+
never refreshes a token or reads the OS keyring. Account `auth.json` files remain
|
|
13
|
+
independent, and none of this MCP state is included in fleet sync.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
codex-accounts mcp auth-sync # repair storage sharing now
|
|
17
|
+
codex mcp login motionsites # authorize once on this machine
|
|
18
|
+
codex mcp list # native authentication status
|
|
19
|
+
codex mcp logout motionsites # clear the shared login
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Preparation runs before pooled launches, during seeding, and after limits passes.
|
|
23
|
+
It requires native Codex 0.163.0 or newer, whose storage and lock contract is checked
|
|
24
|
+
before preparation; older clients keep their existing stores.
|
|
25
|
+
A first login saved after preparation is picked up at the next launch or explicit
|
|
26
|
+
`auth-sync`. Restart an already running session to reload its MCP connections.
|
|
27
|
+
`CODEX_MULTIACC_MCP_OAUTH=0` stops preparation; it does not undo existing shared
|
|
28
|
+
storage. Each machine still needs its own authorization.
|
|
29
|
+
|
|
30
|
+
## Native storage constraints
|
|
31
|
+
|
|
32
|
+
Codex 0.163.0 refuses final-component credential symlinks with `O_NOFOLLOW`, so a
|
|
33
|
+
regular hardlinked file is necessary. Its file writer updates that inode in place
|
|
34
|
+
under `mcp-oauth-locks/file-store.lock`. A tokenless reserved entry keeps the native
|
|
35
|
+
document nonempty after its last real login is removed: otherwise Codex unlinks
|
|
36
|
+
only the account's filename, leaving the other hardlinks with the old login.
|
|
37
|
+
The reserved entry is not a configured server and cannot authenticate a request.
|
|
38
|
+
|
|
39
|
+
The refresh lock must be shared too: native locks normally live under each
|
|
40
|
+
`CODEX_HOME`, even when the grant is in the globally shared direct OS keyring.
|
|
41
|
+
Sharing a rotating grant without its lock lets simultaneous sessions reuse an
|
|
42
|
+
obsolete refresh token. Preparation preserves existing lock inodes, defers busy
|
|
43
|
+
stores and refuses independent credential documents rather than choosing a grant.
|
|
44
|
+
Initial migration atomically exchanges native lock directories with their shared
|
|
45
|
+
links. It retains discarded lock files until preexisting client handles close;
|
|
46
|
+
an operation racing that first migration can make preparation wait for its native
|
|
47
|
+
lock timeout. Later launches use the existing shared paths without that migration.
|
|
48
|
+
Linux excludes an inaccessible process only after proving that it has exited with
|
|
49
|
+
no living threads; inaccessible live or uncertain handle state still defers migration.
|
|
50
|
+
Only manifest accounts are eligible, including the supported `acct-NN → ~/.codex`
|
|
51
|
+
adoption. Foreign directory symlinks, duplicate homes and unsupported scoped
|
|
52
|
+
credential entries are refused. Encrypted secrets storage is not merged.
|
|
53
|
+
|
|
54
|
+
Implementation: `lib/codex_mcp_oauth.py` and its supporting modules. Native contract:
|
|
55
|
+
[OAuth storage](https://github.com/openai/codex/blob/rust-v0.163.0/codex-rs/rmcp-client/src/oauth.rs)
|
|
56
|
+
and [refresh locks](https://github.com/openai/codex/blob/rust-v0.163.0/codex-rs/rmcp-client/src/oauth/refresh_lock.rs).
|
|
57
|
+
The supported store settings are documented in the
|
|
58
|
+
[OpenAI configuration reference](https://learn.chatgpt.com/docs/config-file/config-reference).
|
|
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
|
|
Binary file
|
|
Binary file
|
package/lib/autoresume.py
CHANGED
|
@@ -69,6 +69,7 @@ from autoresume_cross_receipt import confirm_cross
|
|
|
69
69
|
from model_catalog import load as load_catalog, parse_id
|
|
70
70
|
from model_match import resume_settings, settings_flags
|
|
71
71
|
from session_settings import session_settings
|
|
72
|
+
from codex_resume_cwd import codex_resume_cwd
|
|
72
73
|
|
|
73
74
|
# ---- vocabulary ---------------------------------------------------------------------
|
|
74
75
|
|
|
@@ -2698,11 +2699,11 @@ class Watcher:
|
|
|
2698
2699
|
return self.busy_memo[1]
|
|
2699
2700
|
|
|
2700
2701
|
def session_cwd(self):
|
|
2701
|
-
"""
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2702
|
+
"""Restore the session's directory before the picked account trusts and resumes it.
|
|
2703
|
+
Codex's saved cwd can differ from both the launching shell and initial metadata."""
|
|
2704
|
+
order = (self.cwd, self.st["cwd"])
|
|
2705
|
+
if self.provider == "codex":
|
|
2706
|
+
order = (codex_resume_cwd(self.transcript, self.sid),) + order
|
|
2706
2707
|
for cand in order:
|
|
2707
2708
|
if cand and os.path.isdir(cand):
|
|
2708
2709
|
return cand
|
package/lib/autoresume_cross.py
CHANGED
|
@@ -110,7 +110,8 @@ def revalidate(watcher, plan):
|
|
|
110
110
|
history = plan['history']
|
|
111
111
|
current = validate_source(watcher.provider, watcher.sid, watcher.transcript,
|
|
112
112
|
watcher.session_cwd(), inode=history['inode'])
|
|
113
|
-
return (current is not None and current['offset'] >= history['offset'] and
|
|
113
|
+
return (current is not None and current['offset'] >= history['offset'] and current['cwd'] == history['cwd']
|
|
114
|
+
and not shared_source(watcher)
|
|
114
115
|
and all(enabled(watcher, provider, root) for provider, root in plan['roots'].items()))
|
|
115
116
|
|
|
116
117
|
|
|
@@ -12,6 +12,8 @@ import os
|
|
|
12
12
|
import re
|
|
13
13
|
import stat
|
|
14
14
|
|
|
15
|
+
from codex_resume_cwd import codex_resume_cwd
|
|
16
|
+
|
|
15
17
|
|
|
16
18
|
PROVIDERS = ("claude", "codex")
|
|
17
19
|
SID_RE = re.compile(r"^[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$")
|
|
@@ -48,8 +50,8 @@ def _read_source(path, inode):
|
|
|
48
50
|
return None
|
|
49
51
|
|
|
50
52
|
|
|
51
|
-
def _codex_identity(data, sid, cwd):
|
|
52
|
-
"""
|
|
53
|
+
def _codex_identity(data, sid, cwd, path, identity):
|
|
54
|
+
"""Bind main-thread identity and its saved cwd at the validated snapshot boundary."""
|
|
53
55
|
try:
|
|
54
56
|
record = json.loads(data.split(b"\n", 1)[0])
|
|
55
57
|
except (ValueError, UnicodeError):
|
|
@@ -63,8 +65,14 @@ def _codex_identity(data, sid, cwd):
|
|
|
63
65
|
return False
|
|
64
66
|
if meta.get("originator") not in (None, "codex-tui", "codex_cli_rs", "codex_cli"):
|
|
65
67
|
return False
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
try:
|
|
69
|
+
directory = codex_resume_cwd(path, sid, upto=identity.st_size)
|
|
70
|
+
after = os.stat(path)
|
|
71
|
+
except OSError:
|
|
72
|
+
return False
|
|
73
|
+
return (directory is not None and os.path.realpath(directory) == cwd
|
|
74
|
+
and (after.st_dev, after.st_ino) == (identity.st_dev, identity.st_ino)
|
|
75
|
+
and after.st_size >= identity.st_size)
|
|
68
76
|
|
|
69
77
|
|
|
70
78
|
def _claude_identity(data, sid, cwd):
|
|
@@ -110,7 +118,7 @@ def validate_source(provider, sid, path, cwd, inode=None):
|
|
|
110
118
|
if provider == "codex":
|
|
111
119
|
if not name.startswith("rollout-") or not name.endswith("-" + sid + ".jsonl"):
|
|
112
120
|
return None
|
|
113
|
-
valid = _codex_identity(data, sid, directory)
|
|
121
|
+
valid = _codex_identity(data, sid, directory, resolved, identity)
|
|
114
122
|
else:
|
|
115
123
|
valid = name == sid + ".jsonl" and _claude_identity(data, sid, directory)
|
|
116
124
|
if not valid:
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Exit 0 only for a known `codex exec ... -` prompt that itself requires stdin EOF.
|
|
3
|
+
# Unknown options/subcommands and early exits fail closed into ordinary passthrough.
|
|
4
|
+
set -u
|
|
5
|
+
|
|
6
|
+
seen_exec=0
|
|
7
|
+
for arg in "$@"; do
|
|
8
|
+
case "$arg" in -h|--help|-V|--version) exit 1 ;; esac
|
|
9
|
+
done
|
|
10
|
+
while [ "$#" -gt 0 ]; do
|
|
11
|
+
arg="$1"; shift
|
|
12
|
+
case "$arg" in
|
|
13
|
+
--)
|
|
14
|
+
[ "$seen_exec" = 1 ] && [ "$#" = 1 ] && [ "$1" = - ]
|
|
15
|
+
exit $? ;;
|
|
16
|
+
-)
|
|
17
|
+
[ "$seen_exec" = 1 ] && [ "$#" = 0 ]
|
|
18
|
+
exit $? ;;
|
|
19
|
+
-c|--config|--enable|--disable|--remote|--remote-auth-token-env|-m|--model|--local-provider|\
|
|
20
|
+
-p|--profile|-s|--sandbox|-C|--cd|--add-dir|-a|--ask-for-approval|--thread-source|\
|
|
21
|
+
--cyber-access-program|--output-schema|--color|-o|--output-last-message)
|
|
22
|
+
[ "$#" -gt 0 ] || exit 1
|
|
23
|
+
case "$1" in -*) exit 1 ;; esac
|
|
24
|
+
shift ;;
|
|
25
|
+
-i|--image)
|
|
26
|
+
[ "$#" -gt 0 ] || exit 1
|
|
27
|
+
case "$1" in -*) exit 1 ;; esac
|
|
28
|
+
while [ "$#" -gt 0 ]; do
|
|
29
|
+
case "$1" in -*) break ;; esac
|
|
30
|
+
shift
|
|
31
|
+
done ;;
|
|
32
|
+
--config=*|--enable=*|--disable=*|--remote=*|--remote-auth-token-env=*|--model=*|--local-provider=*|\
|
|
33
|
+
--profile=*|--sandbox=*|--cd=*|--add-dir=*|--ask-for-approval=*|--thread-source=*|\
|
|
34
|
+
--cyber-access-program=*|--output-schema=*|--color=*|--output-last-message=*|--image=*|\
|
|
35
|
+
-c?*|-m?*|-p?*|-s?*|-C?*|-a?*|-o?*|-i?*) ;;
|
|
36
|
+
--strict-config|--oss|--approve-for-me|--dangerously-bypass-approvals-and-sandbox|\
|
|
37
|
+
--dangerously-bypass-hook-trust|--worktree|--skip-git-repo-check|--ephemeral|\
|
|
38
|
+
--ignore-user-config|--ignore-rules|--json|--no-daemon) ;;
|
|
39
|
+
exec|e)
|
|
40
|
+
[ "$seen_exec" = 0 ] || exit 1
|
|
41
|
+
seen_exec=1 ;;
|
|
42
|
+
*) exit 1 ;;
|
|
43
|
+
esac
|
|
44
|
+
done
|
|
45
|
+
exit 1
|