claude-multiacc 2.0.61 → 2.0.65
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/claude-accounts +4 -4
- package/bin/codex +54 -13
- package/bin/codex-accounts +28 -7
- package/docs/ACCOUNT_OPERATIONS.md +2 -2
- package/docs/AUTORESUME.md +16 -2
- package/docs/CODEX.md +58 -2
- package/docs/CODEX_MCP_AUTH.md +58 -0
- package/lib/__pycache__/audit.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_cross.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_dispatch.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_receipt.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_handoff.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_probe.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_receipt.cpython-314.pyc +0 -0
- package/lib/__pycache__/claude_client_error.cpython-314.pyc +0 -0
- package/lib/__pycache__/claude_reset.cpython-314.pyc +0 -0
- package/lib/__pycache__/claude_token_identity.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_autoresume_mode.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_config_edit.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credit_controls.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credit_local_scope.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credit_selection.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credits.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_limits_lock.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_mcp_oauth.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_python.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_resume_cwd.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_speed.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_trust.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_usage_fetch.cpython-314.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-314.pyc +0 -0
- package/lib/__pycache__/mcp_mirror.cpython-314.pyc +0 -0
- package/lib/__pycache__/mcp_oauth.cpython-314.pyc +0 -0
- package/lib/__pycache__/mcp_registry.cpython-314.pyc +0 -0
- package/lib/__pycache__/model_catalog.cpython-314.pyc +0 -0
- package/lib/__pycache__/model_match.cpython-314.pyc +0 -0
- package/lib/__pycache__/report.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_credit_scope.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_credits.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_funding.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-314.pyc +0 -0
- package/lib/__pycache__/session_settings.cpython-314.pyc +0 -0
- package/lib/__pycache__/shim_path.cpython-314.pyc +0 -0
- package/lib/autoresume.py +34 -13
- 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/codex_speed.py +351 -0
- package/lib/codex_speed.sh +39 -0
- package/lib/common.sh +16 -3
- package/lib/model_match.py +4 -1
- package/lib/session_settings.py +25 -6
- package/lib/shim_path.py +6 -0
- package/package.json +10 -5
- package/tests/__pycache__/autoresume_cross_receipt_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_unit_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_tmux_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/packaged_command_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog_cli.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_records.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_trust.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_guards.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_holders.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_origin.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_policy.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_receipt.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tokens.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cursor.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_handoff.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_reader.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_fallback.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_argv.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_history.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_origins.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_mode.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_probe.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_receipt.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_settings.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux_modes.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_trust_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_claude_token_identity.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_exec_stdin.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_mcp_oauth.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_polling.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_report.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_fetch.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_mirror.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_mirror_args.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_oauth.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_registry.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_cached_credits.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_scope.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_shim.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_credits.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_spread.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_ceremony.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_integration.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_retry.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_sync.cpython-314.pyc +0 -0
- package/tests/fake_codex_tui.py +23 -1
- package/tests/run-tests.sh +23 -7
- 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/tests/test_codex_speed.py +236 -0
- package/tests/test_mutate_lock.py +107 -0
- package/tests/test_shim_path.py +44 -4
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_dispatch.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_receipt.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_handoff.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_receipt.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_autoresume_mode.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_trust.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_mirror.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_oauth.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
- package/lib/__pycache__/model_catalog.cpython-312.pyc +0 -0
- package/lib/__pycache__/model_match.cpython-312.pyc +0 -0
- package/lib/__pycache__/report.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_funding.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
- package/lib/__pycache__/session_settings.cpython-312.pyc +0 -0
- package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_receipt_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_unit_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_tmux_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog_cli.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_records.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_trust.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_guards.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_holders.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_origin.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_policy.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_receipt.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tokens.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cursor.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_argv.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_history.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_origins.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_mode.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_receipt.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_settings.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_trust_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_fetch.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_mcp_mirror.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_mcp_mirror_args.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_mcp_oauth.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_mcp_registry.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Native lock migration primitives; inspect only task-owned lock file metadata."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import ctypes
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from codex_mcp_oauth_store import Deferred
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def exchange_supported():
|
|
15
|
+
libc = ctypes.CDLL(None, use_errno=True)
|
|
16
|
+
name = 'renamex_np' if sys.platform == 'darwin' else 'renameat2'
|
|
17
|
+
if sys.platform not in ('darwin', 'linux') or not hasattr(libc, name):
|
|
18
|
+
raise Deferred('atomic native lock migration is unavailable')
|
|
19
|
+
if sys.platform == 'darwin' and not shutil.which('lsof'):
|
|
20
|
+
raise Deferred('native lock migration requires lsof')
|
|
21
|
+
if sys.platform == 'linux' and not Path('/proc/self/fd').is_dir():
|
|
22
|
+
raise Deferred('native lock migration requires process file metadata')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def exchange(source, target):
|
|
26
|
+
"""Swap a directory and prepared symlink without an absent native pathname."""
|
|
27
|
+
libc = ctypes.CDLL(None, use_errno=True)
|
|
28
|
+
left, right = os.fsencode(source), os.fsencode(target)
|
|
29
|
+
if sys.platform == 'darwin':
|
|
30
|
+
result = libc.renamex_np(ctypes.c_char_p(left), ctypes.c_char_p(right), ctypes.c_uint(2))
|
|
31
|
+
else:
|
|
32
|
+
result = libc.renameat2(-100, ctypes.c_char_p(left), -100, ctypes.c_char_p(right), ctypes.c_uint(2))
|
|
33
|
+
if result:
|
|
34
|
+
raise Deferred('atomic native lock migration failed')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def linux_handles(identities, owned_fds):
|
|
38
|
+
for process in Path('/proc').iterdir():
|
|
39
|
+
if not process.name.isdecimal():
|
|
40
|
+
continue
|
|
41
|
+
try:
|
|
42
|
+
if process.stat().st_uid != os.getuid():
|
|
43
|
+
continue
|
|
44
|
+
for descriptor in (process / 'fd').iterdir():
|
|
45
|
+
if int(process.name) == os.getpid() and int(descriptor.name) in owned_fds:
|
|
46
|
+
continue
|
|
47
|
+
try:
|
|
48
|
+
info = descriptor.stat()
|
|
49
|
+
if (info.st_dev, info.st_ino) in identities:
|
|
50
|
+
return True
|
|
51
|
+
except FileNotFoundError:
|
|
52
|
+
continue
|
|
53
|
+
except FileNotFoundError:
|
|
54
|
+
continue
|
|
55
|
+
except PermissionError as exc:
|
|
56
|
+
if exited_process(process):
|
|
57
|
+
continue
|
|
58
|
+
raise Deferred('native lock handles could not be inspected') from exc
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def exited_process(process):
|
|
63
|
+
"""A reaped or single-thread zombie process cannot retain a native lock fd."""
|
|
64
|
+
try:
|
|
65
|
+
fields = dict(line.split(':', 1) for line in (process / 'status').read_text().splitlines()
|
|
66
|
+
if ':' in line)
|
|
67
|
+
except FileNotFoundError:
|
|
68
|
+
return True
|
|
69
|
+
except OSError:
|
|
70
|
+
return False
|
|
71
|
+
# A zombie thread-group leader can still have living threads and open files.
|
|
72
|
+
state = fields.get('State', '').split()
|
|
73
|
+
return bool(state and state[0] in ('Z', 'X') and fields.get('Threads', '').strip() == '1')
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def mac_handles(paths, owned_fds):
|
|
77
|
+
try:
|
|
78
|
+
result = subprocess.run(['lsof', '-nP', '-Fpf', '--', *map(str, paths)],
|
|
79
|
+
capture_output=True, text=True, timeout=5, check=False)
|
|
80
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
81
|
+
raise Deferred('native lock handles could not be inspected') from exc
|
|
82
|
+
if result.returncode not in (0, 1) or result.returncode == 1 and result.stderr:
|
|
83
|
+
raise Deferred('native lock handles could not be inspected')
|
|
84
|
+
pid = None
|
|
85
|
+
for line in result.stdout.splitlines():
|
|
86
|
+
if line.startswith('p') and line[1:].isdecimal():
|
|
87
|
+
pid = int(line[1:])
|
|
88
|
+
elif line.startswith('f'):
|
|
89
|
+
number = line[1:].rstrip('ruw')
|
|
90
|
+
if pid != os.getpid() or not number.isdecimal() or int(number) not in owned_fds:
|
|
91
|
+
return True
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def external_handles(paths, owned_fds):
|
|
96
|
+
paths = list(paths)
|
|
97
|
+
if not paths:
|
|
98
|
+
return False
|
|
99
|
+
identities = {(path.stat().st_dev, path.stat().st_ino) for path in paths}
|
|
100
|
+
if sys.platform == 'linux':
|
|
101
|
+
return linux_handles(identities, owned_fds)
|
|
102
|
+
return mac_handles(paths, owned_fds)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Native Codex fallback document validation and shared-inode persistence."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import contextlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import stat
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
SENTINEL = '__multiacc_shared_store__|0000000000000000'
|
|
11
|
+
PLACEHOLDER = {
|
|
12
|
+
'server_name': '__multiacc_shared_store__', 'server_url': 'urn:claude-multiacc:shared-mcp-store',
|
|
13
|
+
'client_id': '', 'access_token': '', 'refresh_token': None, 'expires_at': 0, 'scopes': [],
|
|
14
|
+
}
|
|
15
|
+
MAX_BYTES = 4 << 20
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Deferred(Exception):
|
|
19
|
+
"""Leave an unsafe or contended store unchanged; never include credential values."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def read_json(path):
|
|
23
|
+
try:
|
|
24
|
+
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
|
|
25
|
+
except FileNotFoundError:
|
|
26
|
+
return {}
|
|
27
|
+
with os.fdopen(fd, 'r', encoding='utf-8') as stream:
|
|
28
|
+
if not stat.S_ISREG(os.fstat(stream.fileno()).st_mode):
|
|
29
|
+
raise Deferred('non-regular document')
|
|
30
|
+
raw = stream.read(MAX_BYTES + 1)
|
|
31
|
+
if len(raw) > MAX_BYTES:
|
|
32
|
+
raise Deferred('oversized document')
|
|
33
|
+
doc = json.loads(raw)
|
|
34
|
+
if not isinstance(doc, dict):
|
|
35
|
+
raise Deferred('invalid document')
|
|
36
|
+
return doc
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def validate(doc):
|
|
40
|
+
identities = {}
|
|
41
|
+
for key, entry in doc.items():
|
|
42
|
+
if key == SENTINEL:
|
|
43
|
+
if entry != PLACEHOLDER:
|
|
44
|
+
raise Deferred('reserved identity conflict')
|
|
45
|
+
continue
|
|
46
|
+
if not isinstance(entry, dict) or not isinstance(key, str):
|
|
47
|
+
raise Deferred('invalid credential entry')
|
|
48
|
+
for field in ('server_name', 'server_url', 'client_id', 'access_token'):
|
|
49
|
+
if not isinstance(entry.get(field), str):
|
|
50
|
+
raise Deferred('invalid credential fields')
|
|
51
|
+
if 'executor_owned' in entry and type(entry['executor_owned']) is not bool:
|
|
52
|
+
raise Deferred('invalid credential owner')
|
|
53
|
+
name = entry['server_name']
|
|
54
|
+
if entry.get('executor_owned') or name.startswith(('executor:', 'ema-idp:')) or key.startswith('ema-idp:'):
|
|
55
|
+
raise Deferred('scoped credential cannot be shared')
|
|
56
|
+
if name.removeprefix('local:') == PLACEHOLDER['server_name']:
|
|
57
|
+
raise Deferred('reserved identity conflict')
|
|
58
|
+
issuer = entry.get('issuer')
|
|
59
|
+
if issuer is not None and not isinstance(issuer, str):
|
|
60
|
+
raise Deferred('invalid issuer field')
|
|
61
|
+
refresh, expiry = entry.get('refresh_token'), entry.get('expires_at')
|
|
62
|
+
scopes = entry.get('scopes', [])
|
|
63
|
+
if (refresh is not None and not isinstance(refresh, str)
|
|
64
|
+
or expiry is not None and (type(expiry) is not int or expiry < 0)
|
|
65
|
+
or not isinstance(scopes, list) or not all(isinstance(value, str) for value in scopes)):
|
|
66
|
+
raise Deferred('invalid credential fields')
|
|
67
|
+
identity = (name.removeprefix('local:'), entry['server_url'])
|
|
68
|
+
if identity in identities and identities[identity] != (key, entry):
|
|
69
|
+
raise Deferred('duplicate credential identity')
|
|
70
|
+
identities[identity] = (key, entry)
|
|
71
|
+
return doc
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def merge(documents):
|
|
75
|
+
result = {}
|
|
76
|
+
grants = [{key: entry for key, entry in doc.items() if key != SENTINEL} for doc in documents]
|
|
77
|
+
populated = [doc for doc in grants if doc]
|
|
78
|
+
if populated and any(doc != populated[0] for doc in populated[1:]):
|
|
79
|
+
raise Deferred('independent credential documents require separate authorization')
|
|
80
|
+
for doc in documents:
|
|
81
|
+
validate(doc)
|
|
82
|
+
for key, entry in doc.items():
|
|
83
|
+
if key in result and result[key] != entry:
|
|
84
|
+
raise Deferred('conflicting credential grants')
|
|
85
|
+
result[key] = entry
|
|
86
|
+
validate(result)
|
|
87
|
+
result[SENTINEL] = dict(PLACEHOLDER)
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def write_shared(path, doc):
|
|
92
|
+
"""Native Codex truncates in place too; callers hold its file-store lock."""
|
|
93
|
+
data = json.dumps(doc, separators=(',', ':')).encode()
|
|
94
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_NOFOLLOW | os.O_NONBLOCK, 0o600)
|
|
95
|
+
with os.fdopen(fd, 'wb') as stream:
|
|
96
|
+
if not stat.S_ISREG(os.fstat(stream.fileno()).st_mode):
|
|
97
|
+
raise Deferred('non-regular shared document')
|
|
98
|
+
os.fchmod(stream.fileno(), 0o600)
|
|
99
|
+
stream.truncate(0)
|
|
100
|
+
stream.write(data)
|
|
101
|
+
stream.flush()
|
|
102
|
+
os.fsync(stream.fileno())
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@contextlib.contextmanager
|
|
106
|
+
def staged_links(source, accounts, locks):
|
|
107
|
+
"""Prove every hardlink and symlink can be created before changing native state."""
|
|
108
|
+
pending, scratch_paths = [], []
|
|
109
|
+
try:
|
|
110
|
+
for account in accounts:
|
|
111
|
+
if source.stat().st_dev != account.stat().st_dev:
|
|
112
|
+
raise Deferred('account credentials are on different filesystems')
|
|
113
|
+
target = account / '.credentials.json'
|
|
114
|
+
if not target.exists() or not os.path.samefile(source, target):
|
|
115
|
+
scratch = account / '.codex-mcp-oauth-link'
|
|
116
|
+
if scratch.is_symlink() or scratch.exists() and not os.path.samefile(source, scratch):
|
|
117
|
+
raise Deferred('foreign staged credential link')
|
|
118
|
+
if not scratch.exists():
|
|
119
|
+
os.link(source, scratch)
|
|
120
|
+
scratch_paths.append(scratch)
|
|
121
|
+
pending.append((scratch, target))
|
|
122
|
+
if not (account / 'mcp-oauth-locks').is_symlink():
|
|
123
|
+
scratch = account / '.codex-mcp-oauth-locks-link'
|
|
124
|
+
if scratch.is_symlink():
|
|
125
|
+
if scratch.resolve() != locks:
|
|
126
|
+
raise Deferred('foreign staged native lock link')
|
|
127
|
+
elif scratch.exists():
|
|
128
|
+
raise Deferred('foreign staged native lock link')
|
|
129
|
+
else:
|
|
130
|
+
scratch.symlink_to(locks, target_is_directory=True)
|
|
131
|
+
scratch_paths.append(scratch)
|
|
132
|
+
yield pending
|
|
133
|
+
finally:
|
|
134
|
+
for scratch in scratch_paths:
|
|
135
|
+
scratch.unlink(missing_ok=True)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def commit_links(pending):
|
|
139
|
+
for scratch, target in pending:
|
|
140
|
+
os.replace(scratch, target)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def save_receipt(path, accounts):
|
|
144
|
+
document = {'accounts': sorted(item.name for item in accounts)}
|
|
145
|
+
if read_json(path) == document:
|
|
146
|
+
return
|
|
147
|
+
scratch = path.with_name('managed.pending.json')
|
|
148
|
+
write_shared(scratch, document)
|
|
149
|
+
os.replace(scratch, path)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Recover Codex's saved working directory from its bound main-thread rollout."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from session_settings import _record, _reverse_lines
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _directory(value):
|
|
9
|
+
if isinstance(value, str) and os.path.isabs(value) and not any(c in value for c in "\0\n\r"):
|
|
10
|
+
return value
|
|
11
|
+
return None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _record_cwd(doc, own):
|
|
15
|
+
payload = doc.get("payload") if doc else None
|
|
16
|
+
if not isinstance(payload, dict) or payload.get("thread_id") not in (None, own):
|
|
17
|
+
return None
|
|
18
|
+
if doc.get("type") == "turn_context":
|
|
19
|
+
turn, root = payload.get("turn_id"), payload.get("root_turn_id")
|
|
20
|
+
if turn and root and turn != root:
|
|
21
|
+
return None
|
|
22
|
+
return _directory(payload.get("cwd"))
|
|
23
|
+
if doc.get("type") == "event_msg" and payload.get("type") == "thread_settings_applied":
|
|
24
|
+
settings = payload.get("thread_settings")
|
|
25
|
+
return _directory(settings.get("cwd")) if isinstance(settings, dict) else None
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def codex_resume_cwd(path, sid=None, upto=None):
|
|
30
|
+
"""Latest saved main-thread cwd, then its initial cwd; unknown/unreadable gives None.
|
|
31
|
+
|
|
32
|
+
Match the native resume picker: a settings change can update cwd before the next
|
|
33
|
+
turn_context. Read independently of the watcher's quota cursor so chained restarts
|
|
34
|
+
still recover settings written before that cursor. Only the first metadata record
|
|
35
|
+
identifies this thread; a fork can contain its parent's metadata later in the file.
|
|
36
|
+
``upto`` bounds history to a validated handoff snapshot's byte boundary.
|
|
37
|
+
"""
|
|
38
|
+
if not path:
|
|
39
|
+
return None
|
|
40
|
+
try:
|
|
41
|
+
with open(path, "rb") as fh:
|
|
42
|
+
meta = _record(fh.readline(4 << 20))
|
|
43
|
+
payload = meta.get("payload") if meta and meta.get("type") == "session_meta" else None
|
|
44
|
+
if not isinstance(payload, dict) or not isinstance(payload.get("id"), str):
|
|
45
|
+
return None
|
|
46
|
+
own = payload["id"]
|
|
47
|
+
if sid is not None and sid != own:
|
|
48
|
+
return None
|
|
49
|
+
for line in _reverse_lines(path, upto):
|
|
50
|
+
if b'"cwd"' not in line:
|
|
51
|
+
continue
|
|
52
|
+
directory = _record_cwd(_record(line), own)
|
|
53
|
+
if directory is not None:
|
|
54
|
+
return directory
|
|
55
|
+
return _directory(payload.get("cwd"))
|
|
56
|
+
except (OSError, ValueError):
|
|
57
|
+
return None
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Sourced only by the buffered exec retry path. The caller keeps prompt input on fd 3
|
|
3
|
+
# and owns stdout/stderr redirections and its EXIT cleanup; this helper preserves them.
|
|
4
|
+
|
|
5
|
+
codex_retry_signal() {
|
|
6
|
+
codex_retry_pending_signal="$1"
|
|
7
|
+
codex_retry_signal_status="$2"
|
|
8
|
+
# A signal can arrive between installing the traps and assigning the async PID.
|
|
9
|
+
# Remember it; codex_retry_run delivers it as soon as the owned PID is known.
|
|
10
|
+
[ -n "$codex_retry_child_pid" ] || return 0
|
|
11
|
+
trap '' TERM INT HUP
|
|
12
|
+
kill -s "$codex_retry_pending_signal" "$codex_retry_child_pid" 2>/dev/null || true
|
|
13
|
+
wait "$codex_retry_child_pid" 2>/dev/null || true
|
|
14
|
+
codex_retry_child_pid=""
|
|
15
|
+
exit "$codex_retry_signal_status"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
codex_retry_run() {
|
|
19
|
+
local codex_retry_child_pid="" codex_retry_pending_signal="" codex_retry_signal_status=0
|
|
20
|
+
local codex_retry_previous_traps rc
|
|
21
|
+
codex_retry_previous_traps="$(trap -p TERM INT HUP)"
|
|
22
|
+
trap 'codex_retry_signal TERM 143' TERM
|
|
23
|
+
trap 'codex_retry_signal INT 130' INT
|
|
24
|
+
trap 'codex_retry_signal HUP 129' HUP
|
|
25
|
+
[ -z "$codex_retry_pending_signal" ] || exit "$codex_retry_signal_status"
|
|
26
|
+
"$@" <&3 &
|
|
27
|
+
codex_retry_child_pid=$!
|
|
28
|
+
if [ -n "$codex_retry_pending_signal" ]; then
|
|
29
|
+
codex_retry_signal "$codex_retry_pending_signal" "$codex_retry_signal_status"
|
|
30
|
+
fi
|
|
31
|
+
if wait "$codex_retry_child_pid"; then rc=0; else rc=$?; fi
|
|
32
|
+
codex_retry_child_pid=""
|
|
33
|
+
trap - TERM INT HUP
|
|
34
|
+
# trap -p emits shell-quoted definitions from this process, never fetched input.
|
|
35
|
+
eval "$codex_retry_previous_traps"
|
|
36
|
+
return "$rc"
|
|
37
|
+
}
|