claude-multiacc 2.0.60 → 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.
Files changed (144) hide show
  1. package/README.md +12 -3
  2. package/bin/claude +14 -3
  3. package/bin/codex +42 -10
  4. package/bin/codex-accounts +12 -4
  5. package/docs/ACCOUNT_OPERATIONS.md +2 -2
  6. package/docs/AUTORESUME.md +6 -1
  7. package/docs/CODEX.md +19 -2
  8. package/docs/CODEX_MCP_AUTH.md +58 -0
  9. package/lib/__pycache__/audit.cpython-312.pyc +0 -0
  10. package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
  11. package/lib/__pycache__/autoresume_cross.cpython-312.pyc +0 -0
  12. package/lib/__pycache__/autoresume_cross_dispatch.cpython-312.pyc +0 -0
  13. package/lib/__pycache__/autoresume_cross_receipt.cpython-312.pyc +0 -0
  14. package/lib/__pycache__/autoresume_handoff.cpython-312.pyc +0 -0
  15. package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
  16. package/lib/__pycache__/autoresume_receipt.cpython-312.pyc +0 -0
  17. package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
  18. package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
  19. package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
  20. package/lib/__pycache__/codex_autoresume_mode.cpython-312.pyc +0 -0
  21. package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
  22. package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
  23. package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
  24. package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
  25. package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
  26. package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
  27. package/lib/__pycache__/codex_mcp_oauth.cpython-312.pyc +0 -0
  28. package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
  29. package/lib/__pycache__/codex_resume_cwd.cpython-312.pyc +0 -0
  30. package/lib/__pycache__/codex_trust.cpython-312.pyc +0 -0
  31. package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
  32. package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
  33. package/lib/__pycache__/mcp_mirror.cpython-312.pyc +0 -0
  34. package/lib/__pycache__/mcp_oauth.cpython-312.pyc +0 -0
  35. package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
  36. package/lib/__pycache__/model_catalog.cpython-312.pyc +0 -0
  37. package/lib/__pycache__/model_match.cpython-312.pyc +0 -0
  38. package/lib/__pycache__/report.cpython-312.pyc +0 -0
  39. package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
  40. package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
  41. package/lib/__pycache__/selector_funding.cpython-312.pyc +0 -0
  42. package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
  43. package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
  44. package/lib/__pycache__/session_settings.cpython-312.pyc +0 -0
  45. package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
  46. package/lib/autoresume.py +6 -5
  47. package/lib/autoresume_cross.py +2 -1
  48. package/lib/autoresume_handoff.py +13 -5
  49. package/lib/codex_exec_stdin.sh +45 -0
  50. package/lib/codex_mcp_oauth.py +204 -0
  51. package/lib/codex_mcp_oauth.sh +29 -0
  52. package/lib/codex_mcp_oauth_locks.py +158 -0
  53. package/lib/codex_mcp_oauth_native.py +102 -0
  54. package/lib/codex_mcp_oauth_store.py +149 -0
  55. package/lib/codex_resume_cwd.py +57 -0
  56. package/lib/codex_retry_process.sh +37 -0
  57. package/lib/common.sh +3 -0
  58. package/lib/mcp_mirror.py +49 -0
  59. package/lib/mcp_mirror.sh +47 -0
  60. package/lib/mcp_registry.py +7 -2
  61. package/package.json +9 -4
  62. package/tests/__pycache__/autoresume_cross_receipt_support.cpython-312.pyc +0 -0
  63. package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-312.pyc +0 -0
  64. package/tests/__pycache__/autoresume_cross_tmux_support.cpython-312.pyc +0 -0
  65. package/tests/__pycache__/autoresume_cross_unit_support.cpython-312.pyc +0 -0
  66. package/tests/__pycache__/autoresume_tmux_support.cpython-312.pyc +0 -0
  67. package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
  68. package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
  69. package/tests/__pycache__/test_autoresume_catalog.cpython-312.pyc +0 -0
  70. package/tests/__pycache__/test_autoresume_catalog_cli.cpython-312.pyc +0 -0
  71. package/tests/__pycache__/test_autoresume_codex_records.cpython-312.pyc +0 -0
  72. package/tests/__pycache__/test_autoresume_codex_trust.cpython-312.pyc +0 -0
  73. package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-312.pyc +0 -0
  74. package/tests/__pycache__/test_autoresume_cross_guards.cpython-312.pyc +0 -0
  75. package/tests/__pycache__/test_autoresume_cross_holders.cpython-312.pyc +0 -0
  76. package/tests/__pycache__/test_autoresume_cross_origin.cpython-312.pyc +0 -0
  77. package/tests/__pycache__/test_autoresume_cross_policy.cpython-312.pyc +0 -0
  78. package/tests/__pycache__/test_autoresume_cross_receipt.cpython-312.pyc +0 -0
  79. package/tests/__pycache__/test_autoresume_cross_tmux.cpython-312.pyc +0 -0
  80. package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-312.pyc +0 -0
  81. package/tests/__pycache__/test_autoresume_cross_tokens.cpython-312.pyc +0 -0
  82. package/tests/__pycache__/test_autoresume_cursor.cpython-312.pyc +0 -0
  83. package/tests/__pycache__/test_autoresume_cwd.cpython-312.pyc +0 -0
  84. package/tests/__pycache__/test_autoresume_cwd_handoff.cpython-312.pyc +0 -0
  85. package/tests/__pycache__/test_autoresume_cwd_reader.cpython-312.pyc +0 -0
  86. package/tests/__pycache__/test_autoresume_cwd_tmux.cpython-312.pyc +0 -0
  87. package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
  88. package/tests/__pycache__/test_autoresume_handoff_argv.cpython-312.pyc +0 -0
  89. package/tests/__pycache__/test_autoresume_handoff_history.cpython-312.pyc +0 -0
  90. package/tests/__pycache__/test_autoresume_handoff_origins.cpython-312.pyc +0 -0
  91. package/tests/__pycache__/test_autoresume_memory.cpython-312.pyc +0 -0
  92. package/tests/__pycache__/test_autoresume_memory_tmux.cpython-312.pyc +0 -0
  93. package/tests/__pycache__/test_autoresume_mode.cpython-312.pyc +0 -0
  94. package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
  95. package/tests/__pycache__/test_autoresume_receipt.cpython-312.pyc +0 -0
  96. package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-312.pyc +0 -0
  97. package/tests/__pycache__/test_autoresume_settings.cpython-312.pyc +0 -0
  98. package/tests/__pycache__/test_autoresume_sidechain.cpython-312.pyc +0 -0
  99. package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-312.pyc +0 -0
  100. package/tests/__pycache__/test_autoresume_tmux.cpython-312.pyc +0 -0
  101. package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
  102. package/tests/__pycache__/test_autoresume_trust_tmux.cpython-312.pyc +0 -0
  103. package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
  104. package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
  105. package/tests/__pycache__/test_codex_exec_stdin.cpython-312.pyc +0 -0
  106. package/tests/__pycache__/test_codex_mcp_oauth.cpython-312.pyc +0 -0
  107. package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
  108. package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
  109. package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
  110. package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
  111. package/tests/__pycache__/test_codex_reset_fetch.cpython-312.pyc +0 -0
  112. package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
  113. package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
  114. package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
  115. package/tests/__pycache__/test_mcp_mirror.cpython-312.pyc +0 -0
  116. package/tests/__pycache__/test_mcp_mirror_args.cpython-312.pyc +0 -0
  117. package/tests/__pycache__/test_mcp_oauth.cpython-312.pyc +0 -0
  118. package/tests/__pycache__/test_mcp_registry.cpython-312.pyc +0 -0
  119. package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
  120. package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
  121. package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
  122. package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
  123. package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
  124. package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
  125. package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
  126. package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
  127. package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
  128. package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
  129. package/tests/fake_codex_tui.py +23 -1
  130. package/tests/run-tests.sh +3 -1
  131. package/tests/test_autoresume_cwd.py +71 -0
  132. package/tests/test_autoresume_cwd_handoff.py +92 -0
  133. package/tests/test_autoresume_cwd_reader.py +105 -0
  134. package/tests/test_autoresume_cwd_tmux.py +54 -0
  135. package/tests/test_codex_exec_stdin.py +178 -0
  136. package/tests/test_codex_mcp_oauth.py +146 -0
  137. package/tests/test_codex_mcp_oauth_adopt.py +87 -0
  138. package/tests/test_codex_mcp_oauth_migration.py +141 -0
  139. package/tests/test_codex_mcp_oauth_native.py +109 -0
  140. package/tests/test_codex_mcp_oauth_safety.py +130 -0
  141. package/tests/test_codex_mcp_oauth_shim.py +188 -0
  142. package/tests/test_codex_retry_process.py +143 -0
  143. package/tests/test_mcp_mirror.py +148 -0
  144. package/tests/test_mcp_mirror_args.py +129 -0
package/README.md CHANGED
@@ -571,7 +571,12 @@ claude-accounts mcp apply # re-apply now (repair drift)
571
571
  config dir outside the pool is plain passthrough. Global options in front of `mcp` are
572
572
  looked past the way the real parser does (`alias claude='claude
573
573
  --dangerously-skip-permissions'` puts one before every typed `claude mcp add`; until
574
- 2026-10-22 that kept motionsites in 2 of 11 accounts).
574
+ 2026-10-22 that kept motionsites in 2 of 11 accounts). Mirroring is limited to the server
575
+ named by the command, including its project approvals: a concurrent runner update to
576
+ another server cannot become a synced override or tombstone. An unrecognized command
577
+ option leaves the native command intact and warns that its change was not mirrored.
578
+ Claude's desktop bulk import mirrors changes except names and retirements belonging to
579
+ another machine-local overlay owner; an explicit named command can still override one.
575
580
  - **Adoption**: whatever reached an account by another route — the real binary run
576
581
  directly, a hand edit, another tool's installer — is adopted by the next reconcile: a
577
582
  server (user scope or a project entry) or a project's `.mcp.json` approval that no section
@@ -680,8 +685,12 @@ claude-accounts mcp auth-forget higgsfield # log it out of EVERY account on th
680
685
  cannot open) stays ignored and is cleared by a later pass.
681
686
  - **Kill switches**: `CLAUDE_MULTIACC_MCP_OAUTH=0` stops sharing (the shim's merge and the
682
687
  limits pass's sync); `CLAUDE_MULTIACC_MCP_OAUTH_REFRESH=0` never refreshes — and so shares
683
- nothing. `auth-status` and `auth-forget` always run. Claude pool only: Codex keeps MCP
684
- OAuth in its own store, and `codex-accounts mcp auth-…` says so.
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.
685
694
 
686
695
  app-robot tasks are different: the panel launches Claude with `--strict-mcp-config` and a
687
696
  merged MCP document (`agent_mcp.servers_for`) and Codex with the same document as `-c`
package/bin/claude CHANGED
@@ -224,15 +224,26 @@ mcp_mutation_argv() {
224
224
  i="$(cli_positional_at "$@")" || return 1
225
225
  [ "${!i}" = "mcp" ] || return 1
226
226
  j=$((i + 1))
227
- case "${!j:-}" in add|add-json|add-from-claude-desktop|remove) return 0 ;; esac
227
+ case "${!j:-}" in add|add-json|add-from-claude-desktop|remove) MCP_MUTATION_AT="$j"; return 0 ;; esac
228
228
  return 1
229
229
  }
230
230
 
231
231
  mcp_mirror_run() { # $1 acct dir, then the user's argv — exits when it handled the run
232
- local d="$1" snap="" rc
232
+ local d="$1" snap="" rc parse_rc name="" mirror_args=()
233
233
  shift
234
234
  mcp_mutation_argv "$@" || return 1
235
235
  [ "${CLAUDE_MULTIACC_MCP:-1}" != "0" ] && [ -f "$MCP_PY" ] || return 1
236
+ if [ "${!MCP_MUTATION_AT}" != add-from-claude-desktop ]; then
237
+ if name="$(. "$SELF_DIR/../lib/mcp_mirror.sh" && mcp_mutation_name claude "${@:$MCP_MUTATION_AT}")"; then
238
+ mirror_args=("--name=$name")
239
+ else
240
+ parse_rc=$?
241
+ "$REAL" "$@"; rc=$?
242
+ [ "$rc" -ne 0 ] || [ "$parse_rc" = 2 ] \
243
+ || printf 'claude-multiacc: could not identify the MCP target; change not mirrored\n' >&2
244
+ exit "$rc"
245
+ fi
246
+ fi
236
247
  mcp_ensure "$d"
237
248
  snap="$(mktemp "${TMPDIR:-/tmp}/claude-mcp-snap.XXXXXX" 2>/dev/null)" || snap=""
238
249
  if [ -n "$snap" ] && ! "$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider claude snapshot \
@@ -243,7 +254,7 @@ mcp_mirror_run() { # $1 acct dir, then the user's argv — exits when it handled
243
254
  rc=$?
244
255
  if [ "$rc" -eq 0 ] && [ -n "$snap" ]; then
245
256
  "$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider claude learn --account-dir "$d" \
246
- --before "$snap" --project "$(pwd -P 2>/dev/null || pwd)" </dev/null \
257
+ --before "$snap" --project "$(pwd -P 2>/dev/null || pwd)" ${mirror_args[@]+"${mirror_args[@]}"} </dev/null \
247
258
  || printf 'claude-multiacc: could not mirror the MCP change to the other accounts — run: claude-accounts mcp apply\n' >&2
248
259
  elif [ "$rc" -eq 0 ]; then
249
260
  printf 'claude-multiacc: could not mirror the MCP change to the other accounts — run: claude-accounts mcp apply\n' >&2
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
@@ -165,15 +170,24 @@ mcp_mutation_argv() {
165
170
  *) break ;;
166
171
  esac
167
172
  done
168
- case "${!j:-}" in add|remove) return 0 ;; esac
173
+ case "${!j:-}" in add|remove) MCP_MUTATION_AT="$j"; return 0 ;; esac
169
174
  return 1
170
175
  }
171
176
 
172
177
  mcp_mirror_run() { # $1 acct dir, then the user's argv — exits when it handled the run
173
- local d="$1" snap="" rc
178
+ local d="$1" snap="" rc parse_rc name=""
174
179
  shift
175
180
  mcp_mutation_argv "$@" || return 1
176
181
  [ "${CODEX_MULTIACC_MCP:-1}" != "0" ] && [ -f "$MCP_PY" ] || return 1
182
+ if name="$(. "$SELF_DIR/../lib/mcp_mirror.sh" && mcp_mutation_name codex "${@:$MCP_MUTATION_AT}")"; then
183
+ :
184
+ else
185
+ parse_rc=$?
186
+ "$REAL" "$@"; rc=$?
187
+ [ "$rc" -ne 0 ] || [ "$parse_rc" = 2 ] \
188
+ || printf 'codex-multiacc: could not identify the MCP target; change not mirrored\n' >&2
189
+ exit "$rc"
190
+ fi
177
191
  mcp_ensure "$d"
178
192
  snap="$(mktemp "${TMPDIR:-/tmp}/codex-mcp-snap.XXXXXX" 2>/dev/null)" || snap=""
179
193
  if [ -n "$snap" ] && ! "$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider codex snapshot \
@@ -184,7 +198,7 @@ mcp_mirror_run() { # $1 acct dir, then the user's argv — exits when it handled
184
198
  rc=$?
185
199
  if [ "$rc" -eq 0 ] && [ -n "$snap" ]; then
186
200
  "$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider codex learn --account-dir "$d" \
187
- --before "$snap" </dev/null \
201
+ --before "$snap" "--name=$name" </dev/null \
188
202
  || printf 'codex-multiacc: could not mirror the MCP change to the other accounts — run: codex-accounts mcp apply\n' >&2
189
203
  elif [ "$rc" -eq 0 ]; then
190
204
  printf 'codex-multiacc: could not mirror the MCP change to the other accounts — run: codex-accounts mcp apply\n' >&2
@@ -234,6 +248,9 @@ if [ -n "${CODEX_HOME:-}" ] \
234
248
  printf 'codex-multiacc: auto-resume cannot enter account selection.\n' >&2
235
249
  exit 2
236
250
  fi
251
+ if [ "${CODEX_MULTIACC_DISABLE:-0}" != "1" ] && [ -n "${CODEX_HOME:-}" ]; then
252
+ mcp_oauth_ensure "$CODEX_HOME"
253
+ fi
237
254
  mcp_nested_mirror "$@" || true
238
255
  exec "$REAL" "$@"
239
256
  fi
@@ -1249,9 +1266,11 @@ if [ -n "${CODEX_ACCOUNT:-}" ] && [ "$AR_PROBE" != "1" ]; then
1249
1266
  share_state_index "$d"
1250
1267
  export CODEX_HOME="$d"
1251
1268
  export CODEX_SHIM_ACTIVE=1
1269
+ unset CODEX_MULTIACC_SELECTED_HOME
1252
1270
  # A pinned account carries every registered MCP server too, and a pinned
1253
1271
  # `codex mcp add|remove` is mirrored like any other (exits inside).
1254
1272
  mcp_ensure "$d"
1273
+ mcp_oauth_ensure "$d"
1255
1274
  mcp_mirror_run "$d" "$@" || true
1256
1275
  exec "$REAL" "$@"
1257
1276
  fi
@@ -1542,6 +1561,7 @@ if [ "$AR_TIER" = "credits" ]; then
1542
1561
  fi
1543
1562
  # The picked account carries every registered MCP server before the client starts.
1544
1563
  mcp_ensure "$pick"
1564
+ mcp_oauth_ensure "$pick"
1545
1565
 
1546
1566
  # Opportunistic limits refresh: non-blocking, throttled, backgrounded. The windows are
1547
1567
  # deliberately wide (10m, matching the 5m scheduled pass): the usage endpoint rate-limits
@@ -1572,6 +1592,9 @@ sel_log "$acct weekly=$(fresh_field "$pick" weekly_percent || echo '?')%" \
1572
1592
  "session-ok=${PICK_GATE_COUNT} pwd=$PWD${credit_funding_log}"
1573
1593
 
1574
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"
1575
1598
 
1576
1599
  # A `codex mcp add|remove` is run as a child under the picked account and mirrored to
1577
1600
  # every other one (exits inside); anything else falls through to exec.
@@ -1582,9 +1605,9 @@ if mcp_mutation_argv "$@"; then
1582
1605
  fi
1583
1606
 
1584
1607
  # Auto-retry applies only to `codex exec` runs with an alternative account available,
1585
- # and only when stdin is finite (tty, regular file, or char device like /dev/null).
1586
- # A service-spawned pipe that never EOFs must take the plain exec path, or the
1587
- # stdin pre-buffering below would hang the call.
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.
1588
1611
  wants_retry=0
1589
1612
  if [ "${CODEX_SHIM_RETRY:-1}" != "0" ] \
1590
1613
  && { [ "${#eligible[@]}" -ge 2 ] || [ "$AR_TIER" = "credits" ]; }; then
@@ -1594,19 +1617,26 @@ if [ "${CODEX_SHIM_RETRY:-1}" != "0" ] \
1594
1617
  if [ "$wants_retry" = "1" ]; then
1595
1618
  # A TTY cannot be buffered or replayed: `codex exec` with no prompt argument reads
1596
1619
  # stdin, and the retry path would hand it /dev/null. Plain exec instead —
1597
- # stdin is inherited untouched. A pipe that never EOFs would hang the pre-buffer,
1598
- # so only finite stdin (regular file, /dev/null-style char device) takes the retry
1599
- # path; everything else execs directly.
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.
1600
1623
  if [ -t 0 ]; then
1601
1624
  wants_retry=0
1602
1625
  elif [ -f /dev/fd/0 ] || [ -c /dev/fd/0 ]; then
1603
1626
  :
1627
+ elif bash "$SELF_DIR/../lib/codex_exec_stdin.sh" "$@" 2>/dev/null; then
1628
+ :
1604
1629
  else
1605
1630
  wants_retry=0
1606
1631
  fi
1607
1632
  fi
1608
1633
  fi
1609
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
+
1610
1640
  share_state_index "$pick"
1611
1641
  if [ "$wants_retry" = "0" ]; then
1612
1642
  export CODEX_HOME="$pick"
@@ -1678,7 +1708,8 @@ cur="$pick"
1678
1708
  rc=0
1679
1709
  while :; do
1680
1710
  if [ -n "$stdin_file" ]; then exec 3< "$stdin_file"; else exec 3< /dev/null; fi
1681
- CODEX_HOME="$cur" "$REAL" "$@" <&3 > "$tmpd/out" 2> "$tmpd/err"
1711
+ CODEX_HOME="$cur" CODEX_MULTIACC_SELECTED_HOME="$cur" \
1712
+ codex_retry_run "$REAL" "$@" <&3 > "$tmpd/out" 2> "$tmpd/err"
1682
1713
  rc=$?
1683
1714
  exec 3<&-
1684
1715
  if [ "$rc" -ne 0 ] && [ "$attempt" -eq 1 ] \
@@ -1740,6 +1771,7 @@ while :; do
1740
1771
  # away from — not the one that bounced.
1741
1772
  remember_pick "$cur"
1742
1773
  mcp_ensure "$cur"
1774
+ mcp_oauth_ensure "$cur"
1743
1775
  attempt=2
1744
1776
  continue
1745
1777
  fi
@@ -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
- # Sharing MCP OAuth logins across accounts is a claude-pool feature (lib/mcp_oauth.py):
2268
- # Codex keeps an MCP server's OAuth state in its own per-account store.
2269
- auth-status|auth-sync|auth-forget)
2270
- die "mcp $sub is not supported for the codex pool — MCP OAuth logins are shared across the claude pool only (claude-accounts mcp $sub)" ;;
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. `codex-accounts mcp auth-*` is not supported (Codex keeps MCP OAuth in
264
- its own store).
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
@@ -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 finite stdin, exactly like `-p` on the claude side. Interactive
22
- `codex` in tmux gets [auto-resume](AUTORESUME.md) instead, like `claude`.
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).
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
- """Where the probe and the relaunch run. claude: the registry's cwd first
2702
- (``--resume`` finds the transcript by it). codex: the launch directory first, so
2703
- the relaunch repeats the user's own invocation; the rollout's cwd after it."""
2704
- order = (self.cwd, self.st["cwd"]) if self.provider == "claude" \
2705
- else (self.st["cwd"], self.cwd)
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
@@ -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 not shared_source(watcher)
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
- """A main interactive rollout identifies itself in its first JSONL record."""
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
- directory = meta.get("cwd")
67
- return isinstance(directory, str) and os.path.isabs(directory) and os.path.realpath(directory) == cwd
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