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.
Files changed (137) hide show
  1. package/README.md +6 -2
  2. package/bin/codex +30 -7
  3. package/bin/codex-accounts +12 -4
  4. package/docs/ACCOUNT_OPERATIONS.md +2 -2
  5. package/docs/AUTORESUME.md +6 -1
  6. package/docs/CODEX.md +19 -2
  7. package/docs/CODEX_MCP_AUTH.md +58 -0
  8. package/lib/__pycache__/audit.cpython-312.pyc +0 -0
  9. package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
  10. package/lib/__pycache__/autoresume_cross.cpython-312.pyc +0 -0
  11. package/lib/__pycache__/autoresume_cross_dispatch.cpython-312.pyc +0 -0
  12. package/lib/__pycache__/autoresume_cross_receipt.cpython-312.pyc +0 -0
  13. package/lib/__pycache__/autoresume_handoff.cpython-312.pyc +0 -0
  14. package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
  15. package/lib/__pycache__/autoresume_receipt.cpython-312.pyc +0 -0
  16. package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
  17. package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
  18. package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
  19. package/lib/__pycache__/codex_autoresume_mode.cpython-312.pyc +0 -0
  20. package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
  21. package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
  22. package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
  23. package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
  24. package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
  25. package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
  26. package/lib/__pycache__/codex_mcp_oauth.cpython-312.pyc +0 -0
  27. package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
  28. package/lib/__pycache__/codex_resume_cwd.cpython-312.pyc +0 -0
  29. package/lib/__pycache__/codex_trust.cpython-312.pyc +0 -0
  30. package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
  31. package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
  32. package/lib/__pycache__/mcp_mirror.cpython-312.pyc +0 -0
  33. package/lib/__pycache__/mcp_oauth.cpython-312.pyc +0 -0
  34. package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
  35. package/lib/__pycache__/model_catalog.cpython-312.pyc +0 -0
  36. package/lib/__pycache__/model_match.cpython-312.pyc +0 -0
  37. package/lib/__pycache__/report.cpython-312.pyc +0 -0
  38. package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
  39. package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
  40. package/lib/__pycache__/selector_funding.cpython-312.pyc +0 -0
  41. package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
  42. package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
  43. package/lib/__pycache__/session_settings.cpython-312.pyc +0 -0
  44. package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
  45. package/lib/autoresume.py +6 -5
  46. package/lib/autoresume_cross.py +2 -1
  47. package/lib/autoresume_handoff.py +13 -5
  48. package/lib/codex_exec_stdin.sh +45 -0
  49. package/lib/codex_mcp_oauth.py +204 -0
  50. package/lib/codex_mcp_oauth.sh +29 -0
  51. package/lib/codex_mcp_oauth_locks.py +158 -0
  52. package/lib/codex_mcp_oauth_native.py +102 -0
  53. package/lib/codex_mcp_oauth_store.py +149 -0
  54. package/lib/codex_resume_cwd.py +57 -0
  55. package/lib/codex_retry_process.sh +37 -0
  56. package/lib/common.sh +3 -0
  57. package/package.json +9 -4
  58. package/tests/__pycache__/autoresume_cross_receipt_support.cpython-312.pyc +0 -0
  59. package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-312.pyc +0 -0
  60. package/tests/__pycache__/autoresume_cross_tmux_support.cpython-312.pyc +0 -0
  61. package/tests/__pycache__/autoresume_cross_unit_support.cpython-312.pyc +0 -0
  62. package/tests/__pycache__/autoresume_tmux_support.cpython-312.pyc +0 -0
  63. package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
  64. package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
  65. package/tests/__pycache__/test_autoresume_catalog.cpython-312.pyc +0 -0
  66. package/tests/__pycache__/test_autoresume_catalog_cli.cpython-312.pyc +0 -0
  67. package/tests/__pycache__/test_autoresume_codex_records.cpython-312.pyc +0 -0
  68. package/tests/__pycache__/test_autoresume_codex_trust.cpython-312.pyc +0 -0
  69. package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-312.pyc +0 -0
  70. package/tests/__pycache__/test_autoresume_cross_guards.cpython-312.pyc +0 -0
  71. package/tests/__pycache__/test_autoresume_cross_holders.cpython-312.pyc +0 -0
  72. package/tests/__pycache__/test_autoresume_cross_origin.cpython-312.pyc +0 -0
  73. package/tests/__pycache__/test_autoresume_cross_policy.cpython-312.pyc +0 -0
  74. package/tests/__pycache__/test_autoresume_cross_receipt.cpython-312.pyc +0 -0
  75. package/tests/__pycache__/test_autoresume_cross_tmux.cpython-312.pyc +0 -0
  76. package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-312.pyc +0 -0
  77. package/tests/__pycache__/test_autoresume_cross_tokens.cpython-312.pyc +0 -0
  78. package/tests/__pycache__/test_autoresume_cursor.cpython-312.pyc +0 -0
  79. package/tests/__pycache__/test_autoresume_cwd.cpython-312.pyc +0 -0
  80. package/tests/__pycache__/test_autoresume_cwd_handoff.cpython-312.pyc +0 -0
  81. package/tests/__pycache__/test_autoresume_cwd_reader.cpython-312.pyc +0 -0
  82. package/tests/__pycache__/test_autoresume_cwd_tmux.cpython-312.pyc +0 -0
  83. package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
  84. package/tests/__pycache__/test_autoresume_handoff_argv.cpython-312.pyc +0 -0
  85. package/tests/__pycache__/test_autoresume_handoff_history.cpython-312.pyc +0 -0
  86. package/tests/__pycache__/test_autoresume_handoff_origins.cpython-312.pyc +0 -0
  87. package/tests/__pycache__/test_autoresume_memory.cpython-312.pyc +0 -0
  88. package/tests/__pycache__/test_autoresume_memory_tmux.cpython-312.pyc +0 -0
  89. package/tests/__pycache__/test_autoresume_mode.cpython-312.pyc +0 -0
  90. package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
  91. package/tests/__pycache__/test_autoresume_receipt.cpython-312.pyc +0 -0
  92. package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-312.pyc +0 -0
  93. package/tests/__pycache__/test_autoresume_settings.cpython-312.pyc +0 -0
  94. package/tests/__pycache__/test_autoresume_sidechain.cpython-312.pyc +0 -0
  95. package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-312.pyc +0 -0
  96. package/tests/__pycache__/test_autoresume_tmux.cpython-312.pyc +0 -0
  97. package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
  98. package/tests/__pycache__/test_autoresume_trust_tmux.cpython-312.pyc +0 -0
  99. package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
  100. package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
  101. package/tests/__pycache__/test_codex_exec_stdin.cpython-312.pyc +0 -0
  102. package/tests/__pycache__/test_codex_mcp_oauth.cpython-312.pyc +0 -0
  103. package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
  104. package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
  105. package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
  106. package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
  107. package/tests/__pycache__/test_codex_reset_fetch.cpython-312.pyc +0 -0
  108. package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
  109. package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
  110. package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
  111. package/tests/__pycache__/test_mcp_mirror.cpython-312.pyc +0 -0
  112. package/tests/__pycache__/test_mcp_mirror_args.cpython-312.pyc +0 -0
  113. package/tests/__pycache__/test_mcp_oauth.cpython-312.pyc +0 -0
  114. package/tests/__pycache__/test_mcp_registry.cpython-312.pyc +0 -0
  115. package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
  116. package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
  117. package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
  118. package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
  119. package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
  120. package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
  121. package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
  122. package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
  123. package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
  124. package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
  125. package/tests/fake_codex_tui.py +23 -1
  126. package/tests/test_autoresume_cwd.py +71 -0
  127. package/tests/test_autoresume_cwd_handoff.py +92 -0
  128. package/tests/test_autoresume_cwd_reader.py +105 -0
  129. package/tests/test_autoresume_cwd_tmux.py +54 -0
  130. package/tests/test_codex_exec_stdin.py +178 -0
  131. package/tests/test_codex_mcp_oauth.py +146 -0
  132. package/tests/test_codex_mcp_oauth_adopt.py +87 -0
  133. package/tests/test_codex_mcp_oauth_migration.py +141 -0
  134. package/tests/test_codex_mcp_oauth_native.py +109 -0
  135. package/tests/test_codex_mcp_oauth_safety.py +130 -0
  136. package/tests/test_codex_mcp_oauth_shim.py +188 -0
  137. 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. Claude pool only: Codex keeps MCP
689
- 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.
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 only when stdin is finite (tty, regular file, or char device like /dev/null).
1595
- # A service-spawned pipe that never EOFs must take the plain exec path, or the
1596
- # 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.
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. A pipe that never EOFs would hang the pre-buffer,
1607
- # so only finite stdin (regular file, /dev/null-style char device) takes the retry
1608
- # 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.
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" "$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"
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
@@ -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