claude-multiacc 2.0.58 → 2.0.60

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 (151) hide show
  1. package/README.md +125 -4
  2. package/bin/claude +389 -94
  3. package/bin/claude-accounts +276 -42
  4. package/bin/codex +69 -5
  5. package/bin/codex-accounts +33 -4
  6. package/docs/ACCOUNT_OPERATIONS.md +62 -2
  7. package/docs/AUTORESUME-HANDOFF.md +28 -3
  8. package/docs/AUTORESUME.md +275 -18
  9. package/docs/CODEX.md +1 -0
  10. package/docs/INSTALLATION.md +7 -1
  11. package/docs/VERIFICATION.md +4 -0
  12. package/lib/__pycache__/audit.cpython-312.pyc +0 -0
  13. package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
  14. package/lib/__pycache__/autoresume_cross.cpython-312.pyc +0 -0
  15. package/lib/__pycache__/autoresume_cross_dispatch.cpython-312.pyc +0 -0
  16. package/lib/__pycache__/autoresume_cross_receipt.cpython-312.pyc +0 -0
  17. package/lib/__pycache__/autoresume_handoff.cpython-312.pyc +0 -0
  18. package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
  19. package/lib/__pycache__/autoresume_receipt.cpython-312.pyc +0 -0
  20. package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
  21. package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
  22. package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
  23. package/lib/__pycache__/codex_autoresume_mode.cpython-312.pyc +0 -0
  24. package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
  25. package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
  26. package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
  27. package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
  28. package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
  29. package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
  30. package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
  31. package/lib/__pycache__/codex_trust.cpython-312.pyc +0 -0
  32. package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
  33. package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
  34. package/lib/__pycache__/mcp_oauth.cpython-312.pyc +0 -0
  35. package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
  36. package/lib/__pycache__/model_catalog.cpython-312.pyc +0 -0
  37. package/lib/__pycache__/model_match.cpython-312.pyc +0 -0
  38. package/lib/__pycache__/report.cpython-312.pyc +0 -0
  39. package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
  40. package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
  41. package/lib/__pycache__/selector_funding.cpython-312.pyc +0 -0
  42. package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
  43. package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
  44. package/lib/__pycache__/session_settings.cpython-312.pyc +0 -0
  45. package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
  46. package/lib/audit.py +88 -22
  47. package/lib/autoresume.py +1197 -82
  48. package/lib/autoresume_cross.py +3 -1
  49. package/lib/autoresume_handoff.py +81 -9
  50. package/lib/autoresume_probe.py +2 -2
  51. package/lib/claude_client_error.py +9 -4
  52. package/lib/codex_trust.py +263 -0
  53. package/lib/common.sh +146 -3
  54. package/lib/credential.py +5 -2
  55. package/lib/install_actions.sh +19 -0
  56. package/lib/keychain.py +180 -36
  57. package/lib/mcp_oauth.py +1748 -0
  58. package/lib/mcp_registry.py +230 -8
  59. package/lib/model_catalog.json +404 -0
  60. package/lib/model_catalog.py +201 -0
  61. package/lib/model_match.py +104 -0
  62. package/lib/report.py +8 -5
  63. package/lib/session_settings.py +123 -0
  64. package/package.json +2 -2
  65. package/tests/__pycache__/autoresume_cross_receipt_support.cpython-312.pyc +0 -0
  66. package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-312.pyc +0 -0
  67. package/tests/__pycache__/autoresume_cross_tmux_support.cpython-312.pyc +0 -0
  68. package/tests/__pycache__/autoresume_cross_unit_support.cpython-312.pyc +0 -0
  69. package/tests/__pycache__/autoresume_tmux_support.cpython-312.pyc +0 -0
  70. package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
  71. package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
  72. package/tests/__pycache__/test_autoresume_catalog.cpython-312.pyc +0 -0
  73. package/tests/__pycache__/test_autoresume_catalog_cli.cpython-312.pyc +0 -0
  74. package/tests/__pycache__/test_autoresume_codex_records.cpython-312.pyc +0 -0
  75. package/tests/__pycache__/test_autoresume_codex_trust.cpython-312.pyc +0 -0
  76. package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-312.pyc +0 -0
  77. package/tests/__pycache__/test_autoresume_cross_guards.cpython-312.pyc +0 -0
  78. package/tests/__pycache__/test_autoresume_cross_holders.cpython-312.pyc +0 -0
  79. package/tests/__pycache__/test_autoresume_cross_origin.cpython-312.pyc +0 -0
  80. package/tests/__pycache__/test_autoresume_cross_policy.cpython-312.pyc +0 -0
  81. package/tests/__pycache__/test_autoresume_cross_receipt.cpython-312.pyc +0 -0
  82. package/tests/__pycache__/test_autoresume_cross_tmux.cpython-312.pyc +0 -0
  83. package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-312.pyc +0 -0
  84. package/tests/__pycache__/test_autoresume_cross_tokens.cpython-312.pyc +0 -0
  85. package/tests/__pycache__/test_autoresume_cursor.cpython-312.pyc +0 -0
  86. package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
  87. package/tests/__pycache__/test_autoresume_handoff_argv.cpython-312.pyc +0 -0
  88. package/tests/__pycache__/test_autoresume_handoff_history.cpython-312.pyc +0 -0
  89. package/tests/__pycache__/test_autoresume_handoff_origins.cpython-312.pyc +0 -0
  90. package/tests/__pycache__/test_autoresume_memory.cpython-312.pyc +0 -0
  91. package/tests/__pycache__/test_autoresume_memory_tmux.cpython-312.pyc +0 -0
  92. package/tests/__pycache__/test_autoresume_mode.cpython-312.pyc +0 -0
  93. package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
  94. package/tests/__pycache__/test_autoresume_receipt.cpython-312.pyc +0 -0
  95. package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-312.pyc +0 -0
  96. package/tests/__pycache__/test_autoresume_settings.cpython-312.pyc +0 -0
  97. package/tests/__pycache__/test_autoresume_sidechain.cpython-312.pyc +0 -0
  98. package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-312.pyc +0 -0
  99. package/tests/__pycache__/test_autoresume_tmux.cpython-312.pyc +0 -0
  100. package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
  101. package/tests/__pycache__/test_autoresume_trust_tmux.cpython-312.pyc +0 -0
  102. package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
  103. package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
  104. package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
  105. package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
  106. package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
  107. package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
  108. package/tests/__pycache__/test_codex_reset_fetch.cpython-312.pyc +0 -0
  109. package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
  110. package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
  111. package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
  112. package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
  113. package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
  114. package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
  115. package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
  116. package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
  117. package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
  118. package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
  119. package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
  120. package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
  121. package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
  122. package/tests/fake_codex_tui.py +25 -0
  123. package/tests/fake_security.sh +24 -2
  124. package/tests/fixtures/cross_provider_cli.py +109 -0
  125. package/tests/run-tests.sh +1171 -23
  126. package/tests/test_autoresume.py +215 -8
  127. package/tests/test_autoresume_catalog.py +226 -0
  128. package/tests/test_autoresume_catalog_cli.py +185 -0
  129. package/tests/test_autoresume_codex_records.py +16 -0
  130. package/tests/test_autoresume_codex_trust.py +247 -0
  131. package/tests/test_autoresume_cross_guards.py +28 -0
  132. package/tests/test_autoresume_cross_tmux.py +27 -2
  133. package/tests/test_autoresume_cross_tokens.py +32 -2
  134. package/tests/test_autoresume_handoff_argv.py +120 -16
  135. package/tests/test_autoresume_handoff_origins.py +14 -0
  136. package/tests/test_autoresume_memory.py +1141 -0
  137. package/tests/test_autoresume_memory_tmux.py +94 -0
  138. package/tests/test_autoresume_probe.py +2 -1
  139. package/tests/test_autoresume_relaunch_settings.py +317 -0
  140. package/tests/test_autoresume_settings.py +135 -0
  141. package/tests/test_autoresume_sidechain.py +1518 -0
  142. package/tests/test_autoresume_sidechain_tmux.py +79 -0
  143. package/tests/test_autoresume_trust_tmux.py +63 -0
  144. package/tests/test_claude_client_error.py +10 -7
  145. package/tests/test_claude_org_block.py +1 -0
  146. package/tests/test_claude_pool_prepare.py +4 -1
  147. package/tests/test_claude_reset.py +3 -0
  148. package/tests/test_keychain_nologin.py +340 -0
  149. package/tests/test_mcp_oauth.py +1414 -0
  150. package/tests/test_mcp_registry.py +234 -3
  151. package/tests/test_packaged_commands.py +41 -0
@@ -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)
@@ -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" >> "$FAKE_SECURITY_LOG"
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()))