claude-multiacc 2.0.58 → 2.0.59
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 +111 -3
- package/bin/claude +323 -84
- package/bin/claude-accounts +273 -42
- package/bin/codex +15 -0
- package/bin/codex-accounts +31 -4
- package/docs/ACCOUNT_OPERATIONS.md +62 -2
- package/docs/AUTORESUME-HANDOFF.md +28 -3
- package/docs/AUTORESUME.md +275 -18
- package/docs/CODEX.md +1 -0
- package/docs/INSTALLATION.md +7 -1
- package/docs/VERIFICATION.md +4 -0
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_dispatch.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_receipt.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_handoff.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_receipt.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_autoresume_mode.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_trust.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_oauth.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
- package/lib/__pycache__/model_catalog.cpython-312.pyc +0 -0
- package/lib/__pycache__/model_match.cpython-312.pyc +0 -0
- package/lib/__pycache__/report.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_funding.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
- package/lib/__pycache__/session_settings.cpython-312.pyc +0 -0
- package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
- package/lib/audit.py +88 -22
- package/lib/autoresume.py +1197 -82
- package/lib/autoresume_cross.py +3 -1
- package/lib/autoresume_handoff.py +81 -9
- package/lib/autoresume_probe.py +2 -2
- package/lib/claude_client_error.py +9 -4
- package/lib/codex_trust.py +263 -0
- package/lib/common.sh +131 -3
- package/lib/credential.py +5 -2
- package/lib/install_actions.sh +19 -0
- package/lib/keychain.py +180 -36
- package/lib/mcp_oauth.py +1748 -0
- package/lib/model_catalog.json +404 -0
- package/lib/model_catalog.py +201 -0
- package/lib/model_match.py +104 -0
- package/lib/report.py +8 -5
- package/lib/session_settings.py +123 -0
- package/package.json +2 -2
- package/tests/__pycache__/autoresume_cross_receipt_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_unit_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_tmux_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog_cli.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_records.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_trust.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_guards.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_holders.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_origin.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_policy.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_receipt.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tokens.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cursor.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_argv.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_history.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_origins.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_mode.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_receipt.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_settings.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_trust_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_fetch.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
- package/tests/fake_codex_tui.py +25 -0
- package/tests/fake_security.sh +24 -2
- package/tests/fixtures/cross_provider_cli.py +109 -0
- package/tests/run-tests.sh +1028 -23
- package/tests/test_autoresume.py +215 -8
- package/tests/test_autoresume_catalog.py +226 -0
- package/tests/test_autoresume_catalog_cli.py +185 -0
- package/tests/test_autoresume_codex_records.py +16 -0
- package/tests/test_autoresume_codex_trust.py +247 -0
- package/tests/test_autoresume_cross_guards.py +28 -0
- package/tests/test_autoresume_cross_tmux.py +27 -2
- package/tests/test_autoresume_cross_tokens.py +32 -2
- package/tests/test_autoresume_handoff_argv.py +120 -16
- package/tests/test_autoresume_handoff_origins.py +14 -0
- package/tests/test_autoresume_memory.py +1141 -0
- package/tests/test_autoresume_memory_tmux.py +94 -0
- package/tests/test_autoresume_probe.py +2 -1
- package/tests/test_autoresume_relaunch_settings.py +317 -0
- package/tests/test_autoresume_settings.py +135 -0
- package/tests/test_autoresume_sidechain.py +1518 -0
- package/tests/test_autoresume_sidechain_tmux.py +79 -0
- package/tests/test_autoresume_trust_tmux.py +63 -0
- package/tests/test_claude_client_error.py +10 -7
- package/tests/test_claude_org_block.py +1 -0
- package/tests/test_claude_pool_prepare.py +4 -1
- package/tests/test_claude_reset.py +3 -0
- package/tests/test_keychain_nologin.py +340 -0
- package/tests/test_mcp_oauth.py +1414 -0
- package/tests/test_packaged_commands.py +41 -0
package/lib/report.py
CHANGED
|
@@ -32,8 +32,9 @@ Account status vocabulary (the panel's contract):
|
|
|
32
32
|
Credential classes (what may be COPIED between machines):
|
|
33
33
|
portable claude setup-token (server.token) — safe to export/import anywhere
|
|
34
34
|
machine-local claude .credentials.json OR a macOS Keychain item (credentials.oauth_store
|
|
35
|
-
says which)
|
|
36
|
-
|
|
35
|
+
says which) carrying a claudeAiOauth login — one holding only MCP OAuth
|
|
36
|
+
state is no login — / codex auth.json — rotating refresh grant; copying it
|
|
37
|
+
makes two machines fight over one grant and breaks both
|
|
37
38
|
none nothing on disk (or in the Keychain) here
|
|
38
39
|
"""
|
|
39
40
|
|
|
@@ -46,7 +47,7 @@ import time
|
|
|
46
47
|
|
|
47
48
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
48
49
|
import keychain # noqa: E402
|
|
49
|
-
from audit import creds_doc_state, token_verified # noqa: E402
|
|
50
|
+
from audit import creds_doc_state, file_has_login, token_verified # noqa: E402
|
|
50
51
|
from claude_client_error import org_access_verified_at # noqa: E402
|
|
51
52
|
from claude_reset import marker_superseded, record_from # noqa: E402
|
|
52
53
|
from codex_credits import credit_view # noqa: E402
|
|
@@ -170,10 +171,12 @@ def _claude_credentials(d):
|
|
|
170
171
|
macOS Keychain (`oauth_store` says which; `keychain` says whether THIS process could
|
|
171
172
|
open it). A Keychain login this session cannot open is still a login on this Mac —
|
|
172
173
|
reporting it as `none` is what made a whole pool of working accounts read as
|
|
173
|
-
missing — so the class stays machine-local and the row's status says `locked`.
|
|
174
|
+
missing — so the class stays machine-local and the row's status says `locked`.
|
|
175
|
+
A file or item holding only the client's MCP OAuth state is no login at all
|
|
176
|
+
(audit.file_has_login, keychain 'nologin'): `oauth` stays false for it."""
|
|
174
177
|
cpath = os.path.join(d, '.credentials.json')
|
|
175
178
|
tpath = os.path.join(d, 'server.token')
|
|
176
|
-
has_oauth =
|
|
179
|
+
has_oauth = file_has_login(cpath)
|
|
177
180
|
has_token = _size(tpath) > 0
|
|
178
181
|
detail = {'oauth': has_oauth, 'token': has_token, 'oauth_store': None, 'keychain': None,
|
|
179
182
|
'oauth_expires_at': None, 'oauth_refresh_expires_at': None,
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The model and effort a session is running with, read from its own history (Python 3.8+).
|
|
3
|
+
|
|
4
|
+
claude: the last main-thread assistant record (not a sidechain, not meta, a real model): its
|
|
5
|
+
``requestedModel``, else ``message.model``; and ``effort``, else ``perTurnEffort``. A natively
|
|
6
|
+
1M model is recorded without ``[1m]``, which it does not need. A ``/model`` switch with no reply
|
|
7
|
+
after it is not visible: only assistant records carry the model.
|
|
8
|
+
codex: whichever comes last of a ``turn_context`` (``model``/``effort``) and a
|
|
9
|
+
``thread_settings_applied`` event for the file's own thread (``thread_settings.model``/
|
|
10
|
+
``reasoning_effort``) -- the rule Codex's own state db follows, so a mid-session ``/model`` or
|
|
11
|
+
effort change is honoured. An absent effort means the model's default.
|
|
12
|
+
|
|
13
|
+
Bounded: the file is read backwards in chunks (a transcript can be 100 MB) and the scan stops
|
|
14
|
+
at SCAN_LIMIT bytes; ``upto`` caps it at a byte boundary (a handoff's snapshot). The result is
|
|
15
|
+
``{"model": str|None, "effort": str|None}``; a file that cannot be read gives both None.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
|
|
22
|
+
CHUNK = 1 << 20
|
|
23
|
+
SCAN_LIMIT = 64 << 20
|
|
24
|
+
SYNTHETIC = "<synthetic>"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _reverse_lines(path, upto=None):
|
|
28
|
+
"""Complete lines of ``path`` from the last one backwards, at most SCAN_LIMIT bytes in."""
|
|
29
|
+
with open(path, "rb") as fh:
|
|
30
|
+
end = os.fstat(fh.fileno()).st_size if upto is None else min(upto, os.fstat(fh.fileno()).st_size)
|
|
31
|
+
rest, pos, scanned = b"", end, 0
|
|
32
|
+
while pos > 0 and scanned < SCAN_LIMIT:
|
|
33
|
+
step = min(CHUNK, pos)
|
|
34
|
+
pos -= step
|
|
35
|
+
fh.seek(pos)
|
|
36
|
+
block = fh.read(step) + rest
|
|
37
|
+
scanned += step
|
|
38
|
+
lines = block.split(b"\n")
|
|
39
|
+
rest = lines[0]
|
|
40
|
+
for line in reversed(lines[1:]):
|
|
41
|
+
if line.strip():
|
|
42
|
+
yield line
|
|
43
|
+
if pos == 0 and rest.strip():
|
|
44
|
+
yield rest
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _record(line):
|
|
48
|
+
try:
|
|
49
|
+
doc = json.loads(line.decode("utf-8", "replace"))
|
|
50
|
+
except ValueError:
|
|
51
|
+
return None
|
|
52
|
+
return doc if isinstance(doc, dict) else None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _text(value):
|
|
56
|
+
return value if isinstance(value, str) and value.strip() else None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _own_thread_id(path):
|
|
60
|
+
"""The id in the file's first line (its own session_meta; a fork copies its parent's at
|
|
61
|
+
line two), so another thread's settings snapshots are not mistaken for this one's."""
|
|
62
|
+
try:
|
|
63
|
+
with open(path, "rb") as fh:
|
|
64
|
+
doc = _record(fh.readline(1 << 20))
|
|
65
|
+
except OSError:
|
|
66
|
+
return None
|
|
67
|
+
payload = doc.get("payload") if doc else None
|
|
68
|
+
return _text(payload.get("id")) if isinstance(payload, dict) else None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def codex_settings(path, upto=None):
|
|
72
|
+
own = _own_thread_id(path)
|
|
73
|
+
for line in _reverse_lines(path, upto):
|
|
74
|
+
doc = _record(line)
|
|
75
|
+
payload = doc.get("payload") if doc else None
|
|
76
|
+
if not isinstance(payload, dict):
|
|
77
|
+
continue
|
|
78
|
+
if doc.get("type") == "turn_context" and _text(payload.get("model")):
|
|
79
|
+
return {"model": _text(payload.get("model")), "effort": _text(payload.get("effort"))}
|
|
80
|
+
if doc.get("type") == "event_msg" and payload.get("type") == "thread_settings_applied":
|
|
81
|
+
settings = payload.get("thread_settings")
|
|
82
|
+
thread = _text(payload.get("thread_id"))
|
|
83
|
+
if isinstance(settings, dict) and _text(settings.get("model")) and (own is None or thread in (None, own)):
|
|
84
|
+
return {"model": _text(settings.get("model")), "effort": _text(settings.get("reasoning_effort"))}
|
|
85
|
+
return {"model": None, "effort": None}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def claude_settings(path, upto=None):
|
|
89
|
+
for line in _reverse_lines(path, upto):
|
|
90
|
+
doc = _record(line)
|
|
91
|
+
if not doc or doc.get("type") != "assistant" or doc.get("isSidechain") or doc.get("isMeta"):
|
|
92
|
+
continue
|
|
93
|
+
message = doc.get("message")
|
|
94
|
+
model = _text(message.get("model")) if isinstance(message, dict) else None
|
|
95
|
+
if model is None or model == SYNTHETIC:
|
|
96
|
+
continue
|
|
97
|
+
return {"model": _text(doc.get("requestedModel")) or model,
|
|
98
|
+
"effort": _text(doc.get("effort")) or _text(doc.get("perTurnEffort"))}
|
|
99
|
+
return {"model": None, "effort": None}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def session_settings(provider, path, upto=None):
|
|
103
|
+
"""Fail-open: anything unreadable or unknown is ``{"model": None, "effort": None}``."""
|
|
104
|
+
reader = {"claude": claude_settings, "codex": codex_settings}.get(provider)
|
|
105
|
+
if reader is None or not path:
|
|
106
|
+
return {"model": None, "effort": None}
|
|
107
|
+
try:
|
|
108
|
+
return reader(path, upto)
|
|
109
|
+
except (OSError, ValueError):
|
|
110
|
+
return {"model": None, "effort": None}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def main(argv):
|
|
114
|
+
if len(argv) not in (2, 4) or argv[0] not in ("claude", "codex") or (len(argv) == 4 and argv[2] != "--upto"):
|
|
115
|
+
sys.stderr.write("usage: session_settings.py <claude|codex> <history.jsonl> [--upto <bytes>]\n")
|
|
116
|
+
return 2
|
|
117
|
+
upto = int(argv[3]) if len(argv) == 4 else None
|
|
118
|
+
print(json.dumps(session_settings(argv[0], argv[1], upto)))
|
|
119
|
+
return 0
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == "__main__":
|
|
123
|
+
sys.exit(main(sys.argv[1:]))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-multiacc",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.59",
|
|
4
4
|
"description": "Unified Claude Code and OpenAI Codex subscription pooling with quota-aware selection.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"test": "npm run test:core && npm run test:seeding && npm run test:commands",
|
|
59
59
|
"test:core": "bash tests/run-tests.sh && npm run test:selector && python3 tests/test_shim_path.py",
|
|
60
60
|
"test:selector": "python3 -m unittest discover -s tests -p 'test_selector*.py'",
|
|
61
|
-
"test:seeding": "python3 tests/test_account_seeding.py && python3 tests/test_claude_pool_prepare.py && python3 tests/test_mcp_registry.py && npm run test:codex-settings",
|
|
61
|
+
"test:seeding": "python3 tests/test_account_seeding.py && python3 tests/test_claude_pool_prepare.py && python3 tests/test_mcp_registry.py && python3 tests/test_mcp_oauth.py && npm run test:codex-settings",
|
|
62
62
|
"test:codex-settings": "python3 tests/test_codex_config_edit.py && python3 tests/test_codex_settings.py",
|
|
63
63
|
"test:commands": "python3 tests/test_packaged_commands.py"
|
|
64
64
|
},
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/tests/fake_codex_tui.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import re
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
import select
|
|
7
8
|
import signal
|
|
@@ -50,6 +51,29 @@ def initialize_rollout():
|
|
|
50
51
|
return rollout
|
|
51
52
|
|
|
52
53
|
|
|
54
|
+
def require_resume_trust():
|
|
55
|
+
if os.environ.get("FAKE_REQUIRE_TRUST") != "1" or "resume" not in sys.argv:
|
|
56
|
+
return
|
|
57
|
+
try:
|
|
58
|
+
import tomllib
|
|
59
|
+
except ImportError: # stock python: the same answer from the written line itself
|
|
60
|
+
tomllib = None
|
|
61
|
+
|
|
62
|
+
path = Path(os.environ["CODEX_HOME"]) / "config.toml"
|
|
63
|
+
text = path.read_text() if path.exists() else ""
|
|
64
|
+
if tomllib:
|
|
65
|
+
trusted = tomllib.loads(text).get("projects", {}).get(os.getcwd(), {}).get("trust_level") == "trusted"
|
|
66
|
+
else:
|
|
67
|
+
table = re.escape(json.dumps(os.getcwd()))
|
|
68
|
+
trusted = re.search(r'^\s*\[projects\.' + table + r'\]\s*\n(?:(?!\s*\[).*\n)*?\s*trust_level\s*=\s*"trusted"',
|
|
69
|
+
text, re.M) is not None
|
|
70
|
+
record("trust", trusted=trusted)
|
|
71
|
+
if not trusted:
|
|
72
|
+
print("Folder access\nTrust this folder?\n› 1. Trust and continue\n 2. Quit", flush=True)
|
|
73
|
+
while True:
|
|
74
|
+
time.sleep(0.1)
|
|
75
|
+
|
|
76
|
+
|
|
53
77
|
def main():
|
|
54
78
|
if sys.argv[1:] == ["--help"]:
|
|
55
79
|
print("Usage: codex [OPTIONS]\n" + (" --no-daemon Run locally" if "FAKE_LEGACY" not in os.environ else ""))
|
|
@@ -61,6 +85,7 @@ def main():
|
|
|
61
85
|
settings = termios.tcgetattr(0)
|
|
62
86
|
signal.signal(signal.SIGTERM, stop)
|
|
63
87
|
signal.signal(signal.SIGHUP, stop)
|
|
88
|
+
require_resume_trust()
|
|
64
89
|
rollout = initialize_rollout()
|
|
65
90
|
try:
|
|
66
91
|
tty.setraw(0)
|
package/tests/fake_security.sh
CHANGED
|
@@ -19,7 +19,28 @@
|
|
|
19
19
|
# cannot open the keychain (ssh/tmux/background). FAKE_SECURITY_LOG=<file> appends one
|
|
20
20
|
# line per call — verb, -a name ("-" without -a), whether the secret was asked for —
|
|
21
21
|
# so a test can count calls; the secret and the -X payload are never logged.
|
|
22
|
+
#
|
|
23
|
+
# `security -i` (how Claude Code writes its item, so the secret stays off argv) runs one
|
|
24
|
+
# command per stdin line, its words split the way the tool's own reader splits them
|
|
25
|
+
# (blanks, "double quotes", backslash escapes), and exits with the LAST command's status,
|
|
26
|
+
# as the real tool does. Its reader takes a line into a 4096-byte buffer, so a longer
|
|
27
|
+
# line is refused here (the real tool would cut it into garbage commands) — which is why
|
|
28
|
+
# the client, and lib/keychain.py write_doc, switch to argv above 4032 bytes. Each such
|
|
29
|
+
# command is logged like an argv call, plus ` via=stdin`.
|
|
22
30
|
KC="${FAKE_KEYCHAIN_DIR:-/nonexistent-keychain}"
|
|
31
|
+
if [ "${1:-}" = "-i" ]; then
|
|
32
|
+
rc=0
|
|
33
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
34
|
+
[ -n "$line" ] || continue
|
|
35
|
+
if [ "${#line}" -ge 4096 ]; then rc=1; line=""; continue; fi
|
|
36
|
+
words=()
|
|
37
|
+
while IFS= read -r -d '' w; do words+=("$w"); done < <(printf '%s' "$line" \
|
|
38
|
+
| python3 -c 'import shlex, sys; sys.stdout.write("".join(w + "\0" for w in shlex.split(sys.stdin.read())))')
|
|
39
|
+
FAKE_SECURITY_VIA=stdin "$0" ${words[@]+"${words[@]}"} </dev/null; rc=$?
|
|
40
|
+
line=""
|
|
41
|
+
done
|
|
42
|
+
exit "$rc"
|
|
43
|
+
fi
|
|
23
44
|
cmd="${1:-}"; shift || true
|
|
24
45
|
svc=""; acct=""; have_acct=0; want_pw=0; update=0; hexdata=""
|
|
25
46
|
while [ $# -gt 0 ]; do
|
|
@@ -32,8 +53,9 @@ while [ $# -gt 0 ]; do
|
|
|
32
53
|
*) shift ;;
|
|
33
54
|
esac
|
|
34
55
|
done
|
|
35
|
-
[ -n "${FAKE_SECURITY_LOG:-}" ] && printf '%s a=%s w=%s\n' "$cmd" \
|
|
36
|
-
"$([ "$have_acct" = "1" ] && printf '%s' "$acct" || printf -- '-')" "$want_pw"
|
|
56
|
+
[ -n "${FAKE_SECURITY_LOG:-}" ] && printf '%s a=%s w=%s%s\n' "$cmd" \
|
|
57
|
+
"$([ "$have_acct" = "1" ] && printf '%s' "$acct" || printf -- '-')" "$want_pw" \
|
|
58
|
+
"${FAKE_SECURITY_VIA:+ via=$FAKE_SECURITY_VIA}" >> "$FAKE_SECURITY_LOG"
|
|
37
59
|
locked() { [ -n "${FAKE_KEYCHAIN_LOCKED:-}" ]; }
|
|
38
60
|
item_dir() { # the item a lookup names: -a picks its own, no -a picks the oldest
|
|
39
61
|
local d best="" bestseq="" s
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import re
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
import select
|
|
7
8
|
import signal
|
|
@@ -14,6 +15,10 @@ import uuid
|
|
|
14
15
|
PROVIDER = Path(sys.argv[0]).name
|
|
15
16
|
HOME = Path(os.environ["FAKE_HOME"])
|
|
16
17
|
SIDS = {"codex": "01a0cb0f-a719-7461-bba6-a59bdf2412cc", "claude": "f8470bf4-f810-4492-b564-1712a7c4930e"}
|
|
18
|
+
# This launch's transcript (claude) or rollout (codex), once it is open.
|
|
19
|
+
HISTORY = None
|
|
20
|
+
# The model/effort a launch without pins runs with; a pinned launch records its own pins.
|
|
21
|
+
DEFAULT_SETTINGS = {"codex": ("gpt-6-astra", "ultra"), "claude": ("claude-fable-5-1", "max")}
|
|
17
22
|
|
|
18
23
|
|
|
19
24
|
def record(kind, **fields):
|
|
@@ -23,14 +28,60 @@ def record(kind, **fields):
|
|
|
23
28
|
|
|
24
29
|
def stop(signum, _frame):
|
|
25
30
|
record("signal", signal=signum)
|
|
31
|
+
if signum == signal.SIGTERM and os.environ.get("FAKE_IGNORE_TERM") == PROVIDER:
|
|
32
|
+
return # a client SIGTERM does not stop (the 8.3GB claude of 2026-10-20): only KILL
|
|
26
33
|
target = os.environ.get("FAKE_PARK_TARGET_ON_TERM")
|
|
27
34
|
if signum == signal.SIGTERM and target in SIDS and target != PROVIDER:
|
|
28
35
|
for account in (HOME / (target + "-pool")).glob("acct-*"):
|
|
29
36
|
(account / ".limited").write_text(
|
|
30
37
|
f"{int(time.time()) + 3600}\nbucket=error-cooldown reason=error-cooldown\n")
|
|
38
|
+
agent = os.environ.get("FAKE_AGENT_LIMIT_ON_TERM")
|
|
39
|
+
if signum == signal.SIGTERM and agent and PROVIDER == "claude" and HISTORY:
|
|
40
|
+
agent_limit(agent)
|
|
31
41
|
raise SystemExit(0)
|
|
32
42
|
|
|
33
43
|
|
|
44
|
+
def agent_limit(agent):
|
|
45
|
+
"""FAKE_AGENT_LIMIT_ON_TERM=<agent id>: one more of this session's workflow agents meets
|
|
46
|
+
the five-hour limit while the client is being stopped, recorded to the millisecond in that
|
|
47
|
+
agent's own transcript, as claude 2.1.303 records it."""
|
|
48
|
+
now = time.time()
|
|
49
|
+
path = (Path(HISTORY).with_suffix("") / "subagents" / "workflows" / "wf_term"
|
|
50
|
+
/ ("agent-" + agent + ".jsonl"))
|
|
51
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
stamp = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(now)) + ".%03dZ" % int(now % 1 * 1000)
|
|
53
|
+
rejection = {"type": "assistant", "isSidechain": True, "agentId": agent, "timestamp": stamp,
|
|
54
|
+
"isApiErrorMessage": True, "error": "rate_limit",
|
|
55
|
+
"apiError": "usage_limit_reached", "apiErrorStatus": 429,
|
|
56
|
+
"quotaLimits": {"status": "rejected", "resetsAt": int(now) + 3600,
|
|
57
|
+
"rateLimitType": "five_hour"},
|
|
58
|
+
"message": {"model": "<synthetic>", "role": "assistant", "content": [
|
|
59
|
+
{"type": "text", "text": "You've hit your session limit"}]}}
|
|
60
|
+
with path.open("a") as target:
|
|
61
|
+
target.write(json.dumps(rejection, separators=(",", ":")) + "\n")
|
|
62
|
+
record("agent-limit", agent=agent, timestamp=stamp)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def draw_prompt():
|
|
66
|
+
"""FAKE_MEMORY_BANNER=<provider>: draw the prompt the way claude 2.1.304 does -- its box
|
|
67
|
+
between two rules, its row the mode indicator and a no-break space, the footer below -- a
|
|
68
|
+
first launch with the client's critical-memory banner at the footer's right, a resumed one
|
|
69
|
+
without it. Drawn before the session registry exists, so no watcher can read the screen
|
|
70
|
+
before it is there."""
|
|
71
|
+
if os.environ.get("FAKE_MEMORY_BANNER") != PROVIDER:
|
|
72
|
+
return
|
|
73
|
+
banner = not any(flag in sys.argv[1:] for flag in ("--resume", "-r"))
|
|
74
|
+
rule = "─" * 60
|
|
75
|
+
footer = " ⏵⏵ bypass permissions on"
|
|
76
|
+
if banner:
|
|
77
|
+
footer += (" Critical memory usage (8.3GB) — restart and resume with claude "
|
|
78
|
+
"--continue, or run /compact")
|
|
79
|
+
text = "\n".join(["⏺ Started the release workflows.", "", rule, "❯\xa0", rule, footer])
|
|
80
|
+
sys.stdout.buffer.write((text + "\n").encode("utf-8"))
|
|
81
|
+
sys.stdout.flush()
|
|
82
|
+
record("screen", banner=banner)
|
|
83
|
+
|
|
84
|
+
|
|
34
85
|
def timestamp():
|
|
35
86
|
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
36
87
|
|
|
@@ -58,6 +109,36 @@ def user_record(sid, prompt):
|
|
|
58
109
|
"message": {"role": "user", "content": prompt}}
|
|
59
110
|
|
|
60
111
|
|
|
112
|
+
def session_settings():
|
|
113
|
+
"""The model and effort this launch asked for (``--model``/``-m``, ``--effort`` or
|
|
114
|
+
``-c model_reasoning_effort=``), else the fixture defaults; never a prompt or a session id."""
|
|
115
|
+
model, effort = DEFAULT_SETTINGS[PROVIDER]
|
|
116
|
+
arguments = sys.argv[1:-1] if len(sys.argv) > 1 and " " in sys.argv[-1] else sys.argv[1:]
|
|
117
|
+
for index, flag in enumerate(arguments):
|
|
118
|
+
value = arguments[index + 1] if index + 1 < len(arguments) else ""
|
|
119
|
+
if flag in ("--model", "-m"):
|
|
120
|
+
model = value
|
|
121
|
+
elif flag.startswith("--model="):
|
|
122
|
+
model = flag.split("=", 1)[1]
|
|
123
|
+
elif flag == "--effort" and PROVIDER == "claude":
|
|
124
|
+
effort = value
|
|
125
|
+
elif flag == "-c" and value.startswith("model_reasoning_effort=") and PROVIDER == "codex":
|
|
126
|
+
effort = value.split("=", 1)[1]
|
|
127
|
+
return model, effort
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def settings_record(sid):
|
|
131
|
+
"""What the native CLI persists about its model: a Codex turn context, a Claude reply."""
|
|
132
|
+
model, effort = session_settings()
|
|
133
|
+
if PROVIDER == "codex":
|
|
134
|
+
return {"timestamp": timestamp(), "type": "turn_context", "payload": {
|
|
135
|
+
"cwd": os.getcwd(), "approval_policy": "on-request", "model": model, "effort": effort}}
|
|
136
|
+
return {"timestamp": timestamp(), "type": "assistant", "sessionId": sid, "cwd": os.getcwd(),
|
|
137
|
+
"isSidechain": False, "uuid": "7c2b5a51-4f0e-4f43-9d1f-5f3f7d0c2a11", "effort": effort,
|
|
138
|
+
"message": {"role": "assistant", "model": model, "type": "message",
|
|
139
|
+
"content": [{"type": "text", "text": "Working on the synthetic task."}]}}
|
|
140
|
+
|
|
141
|
+
|
|
61
142
|
def initialize_history(account, sid):
|
|
62
143
|
if PROVIDER == "codex":
|
|
63
144
|
directory = account / "sessions/2026/10/11"
|
|
@@ -78,6 +159,7 @@ def initialize_history(account, sid):
|
|
|
78
159
|
"cwd": os.getcwd(), "source": os.environ.get("FAKE_CODEX_SOURCE", "cli"),
|
|
79
160
|
"thread_source": "user", "originator": "codex-tui"}}) + "\n")
|
|
80
161
|
history.write(json.dumps(user_record(sid, "Complete the synthetic acceptance task.")) + "\n")
|
|
162
|
+
history.write(json.dumps(settings_record(sid)) + "\n")
|
|
81
163
|
prompt = sys.argv[-1] if len(sys.argv) > 1 and " " in sys.argv[-1] else ""
|
|
82
164
|
if prompt and os.environ.get("FAKE_DROP_PROMPT") != PROVIDER:
|
|
83
165
|
user = user_record(sid, prompt)
|
|
@@ -89,6 +171,29 @@ def initialize_history(account, sid):
|
|
|
89
171
|
return history
|
|
90
172
|
|
|
91
173
|
|
|
174
|
+
def require_codex_trust(account):
|
|
175
|
+
if PROVIDER != "codex" or os.environ.get("FAKE_REQUIRE_TRUST") != "1":
|
|
176
|
+
return
|
|
177
|
+
try:
|
|
178
|
+
import tomllib
|
|
179
|
+
except ImportError: # stock python: the same answer from the written line itself
|
|
180
|
+
tomllib = None
|
|
181
|
+
|
|
182
|
+
path = account / "config.toml"
|
|
183
|
+
text = path.read_text() if path.exists() else ""
|
|
184
|
+
if tomllib:
|
|
185
|
+
trusted = tomllib.loads(text).get("projects", {}).get(os.getcwd(), {}).get("trust_level") == "trusted"
|
|
186
|
+
else:
|
|
187
|
+
table = re.escape(json.dumps(os.getcwd()))
|
|
188
|
+
trusted = re.search(r'^\s*\[projects\.' + table + r'\]\s*\n(?:(?!\s*\[).*\n)*?\s*trust_level\s*=\s*"trusted"',
|
|
189
|
+
text, re.M) is not None
|
|
190
|
+
record("trust", trusted=trusted)
|
|
191
|
+
if not trusted:
|
|
192
|
+
print("Folder access\nTrust this folder?\n› 1. Trust and continue\n 2. Quit", flush=True)
|
|
193
|
+
while True:
|
|
194
|
+
time.sleep(0.1)
|
|
195
|
+
|
|
196
|
+
|
|
92
197
|
def main():
|
|
93
198
|
if sys.argv[1:] == ["--help"]:
|
|
94
199
|
print("Usage: " + PROVIDER + " [OPTIONS]\n --no-daemon Run locally\n --session-id UUID")
|
|
@@ -102,7 +207,11 @@ def main():
|
|
|
102
207
|
settings = termios.tcgetattr(0)
|
|
103
208
|
signal.signal(signal.SIGTERM, stop)
|
|
104
209
|
signal.signal(signal.SIGHUP, stop)
|
|
210
|
+
require_codex_trust(account)
|
|
211
|
+
draw_prompt()
|
|
105
212
|
history = initialize_history(account, sid)
|
|
213
|
+
global HISTORY
|
|
214
|
+
HISTORY = history.name
|
|
106
215
|
try:
|
|
107
216
|
tty.setraw(0)
|
|
108
217
|
record("ready", sid=sid, path=str(Path(history.name).resolve()))
|