algo-cli-runtime 0.14.0__py3-none-any.whl

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 (237) hide show
  1. algo_cli/__init__.py +3 -0
  2. algo_cli/__main__.py +7 -0
  3. algo_cli/_internal/__init__.py +12 -0
  4. algo_cli/_internal/policy_chain.py +259 -0
  5. algo_cli/action_registry.py +1047 -0
  6. algo_cli/agent_blocks.py +550 -0
  7. algo_cli/agent_pipeline.py +1457 -0
  8. algo_cli/agent_threads.py +308 -0
  9. algo_cli/animations.py +316 -0
  10. algo_cli/cache_admission.py +209 -0
  11. algo_cli/capability_mask.py +66 -0
  12. algo_cli/chat_protocol.py +116 -0
  13. algo_cli/chatgpt_auth.py +510 -0
  14. algo_cli/chatgpt_client.py +657 -0
  15. algo_cli/code_rag.py +479 -0
  16. algo_cli/config.py +651 -0
  17. algo_cli/context_budget.py +679 -0
  18. algo_cli/credential_helpers.py +315 -0
  19. algo_cli/deliberation.py +29 -0
  20. algo_cli/display.py +1470 -0
  21. algo_cli/evals/__init__.py +21 -0
  22. algo_cli/evals/algorithm_effectiveness.py +560 -0
  23. algo_cli/evals/competitive_harness_rating.py +702 -0
  24. algo_cli/evals/cot_quality.py +220 -0
  25. algo_cli/evals/harness_retrieval_benchmark.py +401 -0
  26. algo_cli/evals/performance_regression.py +136 -0
  27. algo_cli/evals/scorecard_grading.py +308 -0
  28. algo_cli/evals/session_distribution.py +84 -0
  29. algo_cli/execution_guardrails.py +806 -0
  30. algo_cli/extensions_manifest.py +84 -0
  31. algo_cli/git_evidence.py +227 -0
  32. algo_cli/google_workspace.py +407 -0
  33. algo_cli/google_workspace_auth.py +523 -0
  34. algo_cli/harness.py +2587 -0
  35. algo_cli/identity.py +557 -0
  36. algo_cli/index_compute_lab.py +228 -0
  37. algo_cli/inference_harness.py +70 -0
  38. algo_cli/intelligence/__init__.py +1103 -0
  39. algo_cli/intelligence/acrobat_config.py +307 -0
  40. algo_cli/intelligence/acrobat_manifests.py +338 -0
  41. algo_cli/intelligence/acrobat_models.py +195 -0
  42. algo_cli/intelligence/acrobat_pipeline.py +295 -0
  43. algo_cli/intelligence/acrobat_runtime.py +302 -0
  44. algo_cli/intelligence/acrobat_security.py +261 -0
  45. algo_cli/intelligence/acrobat_workflows.py +226 -0
  46. algo_cli/intelligence/actionability.py +165 -0
  47. algo_cli/intelligence/adversarial_audit.py +136 -0
  48. algo_cli/intelligence/agent_arena.py +92 -0
  49. algo_cli/intelligence/agent_benchmark.py +236 -0
  50. algo_cli/intelligence/agent_runtime.py +171 -0
  51. algo_cli/intelligence/agents_as_tools.py +70 -0
  52. algo_cli/intelligence/artifact_binding.py +80 -0
  53. algo_cli/intelligence/autonomous_engineer.py +1976 -0
  54. algo_cli/intelligence/backpressure.py +99 -0
  55. algo_cli/intelligence/bloom_filter.py +186 -0
  56. algo_cli/intelligence/bonferroni.py +66 -0
  57. algo_cli/intelligence/boundary_compaction.py +98 -0
  58. algo_cli/intelligence/catalog_verifier.py +172 -0
  59. algo_cli/intelligence/cavecrew.py +118 -0
  60. algo_cli/intelligence/changelog.py +176 -0
  61. algo_cli/intelligence/checkpoint_resume.py +92 -0
  62. algo_cli/intelligence/circuit_breaker.py +88 -0
  63. algo_cli/intelligence/clarification_gate.py +101 -0
  64. algo_cli/intelligence/code_graph.py +180 -0
  65. algo_cli/intelligence/coderank.py +97 -0
  66. algo_cli/intelligence/consistent_hash.py +150 -0
  67. algo_cli/intelligence/consortium_synthesis.py +139 -0
  68. algo_cli/intelligence/construction/__init__.py +241 -0
  69. algo_cli/intelligence/construction/common.py +273 -0
  70. algo_cli/intelligence/construction/documents.py +496 -0
  71. algo_cli/intelligence/construction/labor_units.py +1395 -0
  72. algo_cli/intelligence/construction/payments.py +470 -0
  73. algo_cli/intelligence/construction/risk.py +784 -0
  74. algo_cli/intelligence/content_extractor.py +132 -0
  75. algo_cli/intelligence/context_adaptive.py +102 -0
  76. algo_cli/intelligence/context_ops.py +95 -0
  77. algo_cli/intelligence/count_min.py +145 -0
  78. algo_cli/intelligence/cow_state.py +103 -0
  79. algo_cli/intelligence/critic_loop.py +119 -0
  80. algo_cli/intelligence/cross_source.py +113 -0
  81. algo_cli/intelligence/daemon_mode.py +99 -0
  82. algo_cli/intelligence/dag_orchestration.py +151 -0
  83. algo_cli/intelligence/deep_research.py +155 -0
  84. algo_cli/intelligence/degenerate_detector.py +78 -0
  85. algo_cli/intelligence/delta_report.py +92 -0
  86. algo_cli/intelligence/discovery_event_log.py +92 -0
  87. algo_cli/intelligence/document_ingest.py +298 -0
  88. algo_cli/intelligence/dual_layer_validate.py +151 -0
  89. algo_cli/intelligence/echo_fidelity.py +73 -0
  90. algo_cli/intelligence/ema_tuning.py +104 -0
  91. algo_cli/intelligence/event_log.py +92 -0
  92. algo_cli/intelligence/evidence_graph.py +114 -0
  93. algo_cli/intelligence/extension_host.py +162 -0
  94. algo_cli/intelligence/extension_manifest.py +115 -0
  95. algo_cli/intelligence/falsification_suite.py +178 -0
  96. algo_cli/intelligence/finance/__init__.py +169 -0
  97. algo_cli/intelligence/finance/anomalies.py +135 -0
  98. algo_cli/intelligence/finance/ap_ar.py +351 -0
  99. algo_cli/intelligence/finance/cash.py +162 -0
  100. algo_cli/intelligence/finance/close.py +332 -0
  101. algo_cli/intelligence/finance/common.py +244 -0
  102. algo_cli/intelligence/finance/construction.py +135 -0
  103. algo_cli/intelligence/finance/controls.py +172 -0
  104. algo_cli/intelligence/finance/evidence.py +119 -0
  105. algo_cli/intelligence/finance/exceptions.py +157 -0
  106. algo_cli/intelligence/finance/reconciliations.py +254 -0
  107. algo_cli/intelligence/finance/revenue.py +109 -0
  108. algo_cli/intelligence/finance/tax.py +74 -0
  109. algo_cli/intelligence/finance/workpapers.py +111 -0
  110. algo_cli/intelligence/finding_record.py +120 -0
  111. algo_cli/intelligence/flow_dag.py +267 -0
  112. algo_cli/intelligence/gatherer.py +223 -0
  113. algo_cli/intelligence/golden_master.py +98 -0
  114. algo_cli/intelligence/graph_rag.py +195 -0
  115. algo_cli/intelligence/group_chat.py +143 -0
  116. algo_cli/intelligence/hash_dedup.py +145 -0
  117. algo_cli/intelligence/hyperloglog.py +128 -0
  118. algo_cli/intelligence/incremental_index.py +316 -0
  119. algo_cli/intelligence/index_store.py +16 -0
  120. algo_cli/intelligence/iteration_plan.py +133 -0
  121. algo_cli/intelligence/kernel_plugins.py +167 -0
  122. algo_cli/intelligence/lesson_catalog.py +135 -0
  123. algo_cli/intelligence/llm_fallback.py +169 -0
  124. algo_cli/intelligence/log2_histogram.py +267 -0
  125. algo_cli/intelligence/lsp_integration.py +147 -0
  126. algo_cli/intelligence/memory_evolution.py +117 -0
  127. algo_cli/intelligence/minhash_lsh.py +182 -0
  128. algo_cli/intelligence/multi_model_score.py +174 -0
  129. algo_cli/intelligence/multi_tier_grade.py +211 -0
  130. algo_cli/intelligence/negative_controls.py +113 -0
  131. algo_cli/intelligence/numeric_clamp.py +63 -0
  132. algo_cli/intelligence/occ_editor.py +66 -0
  133. algo_cli/intelligence/output_normalize.py +112 -0
  134. algo_cli/intelligence/parallel_delegation.py +98 -0
  135. algo_cli/intelligence/parallel_fanout.py +104 -0
  136. algo_cli/intelligence/permission_modes.py +105 -0
  137. algo_cli/intelligence/pre_push_gate.py +68 -0
  138. algo_cli/intelligence/prefetch.py +171 -0
  139. algo_cli/intelligence/process_framework.py +217 -0
  140. algo_cli/intelligence/project_graph.py +387 -0
  141. algo_cli/intelligence/query_expansion.py +146 -0
  142. algo_cli/intelligence/ralph_loop.py +117 -0
  143. algo_cli/intelligence/rate_limiter.py +153 -0
  144. algo_cli/intelligence/refactor_transaction.py +94 -0
  145. algo_cli/intelligence/research_workspace.py +108 -0
  146. algo_cli/intelligence/retraction_ledger.py +72 -0
  147. algo_cli/intelligence/saga_pattern.py +88 -0
  148. algo_cli/intelligence/session_fork.py +100 -0
  149. algo_cli/intelligence/shadow_editor.py +67 -0
  150. algo_cli/intelligence/shell_session.py +213 -0
  151. algo_cli/intelligence/source_registry.py +143 -0
  152. algo_cli/intelligence/spawn_scales.py +99 -0
  153. algo_cli/intelligence/stat_stability.py +104 -0
  154. algo_cli/intelligence/structural_validator.py +148 -0
  155. algo_cli/intelligence/subagent_spawner.py +111 -0
  156. algo_cli/intelligence/symmetric_verify.py +70 -0
  157. algo_cli/intelligence/task_classifier.py +129 -0
  158. algo_cli/intelligence/team_execution.py +122 -0
  159. algo_cli/intelligence/tiered_access.py +121 -0
  160. algo_cli/intelligence/utility_registry.py +159 -0
  161. algo_cli/intuition_engine.py +560 -0
  162. algo_cli/intuition_injector.py +82 -0
  163. algo_cli/kernels/__init__.py +5 -0
  164. algo_cli/kernels/manifest.py +763 -0
  165. algo_cli/main.py +3903 -0
  166. algo_cli/memory_candidates.py +541 -0
  167. algo_cli/memory_echo_veil.py +394 -0
  168. algo_cli/memory_runtime.py +112 -0
  169. algo_cli/model_info.py +548 -0
  170. algo_cli/model_profile.py +160 -0
  171. algo_cli/model_routing.py +74 -0
  172. algo_cli/oneshot.py +331 -0
  173. algo_cli/perf_telemetry.py +389 -0
  174. algo_cli/plugins.py +245 -0
  175. algo_cli/private_event_store.py +654 -0
  176. algo_cli/quantization/__init__.py +24 -0
  177. algo_cli/quantization/lloyd_max.py +98 -0
  178. algo_cli/quantization/turbo_quant.py +308 -0
  179. algo_cli/reasoning/__init__.py +46 -0
  180. algo_cli/reasoning/combinatorial.py +356 -0
  181. algo_cli/reasoning/graph_of_thought.py +297 -0
  182. algo_cli/reasoning/mcts.py +220 -0
  183. algo_cli/reasoning/neuro_symbolic.py +250 -0
  184. algo_cli/reasoning/react.py +246 -0
  185. algo_cli/reasoning/reflexion.py +225 -0
  186. algo_cli/reasoning/tree_of_thought.py +241 -0
  187. algo_cli/reasoning_bridge.py +150 -0
  188. algo_cli/reconciliation.py +284 -0
  189. algo_cli/reflex.py +385 -0
  190. algo_cli/resources/docs/ALGO.md +13958 -0
  191. algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
  192. algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
  193. algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
  194. algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
  195. algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
  196. algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
  197. algo_cli/resources/docs/main-split-map.md +35 -0
  198. algo_cli/resources/docs/privacy-and-context.md +48 -0
  199. algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
  200. algo_cli/resources/skills/README.md +26 -0
  201. algo_cli/resources/skills/algo-cli.md +59 -0
  202. algo_cli/resources/skills/edit-file-precision.md +49 -0
  203. algo_cli/resources/skills/harness-search-first.md +47 -0
  204. algo_cli/resources/skills/memory-recall-ritual.md +51 -0
  205. algo_cli/resources/skills/qol-algorithms.md +224 -0
  206. algo_cli/resources/skills/smart-error-recovery.md +56 -0
  207. algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
  208. algo_cli/retrieval_algorithms.py +127 -0
  209. algo_cli/runtime_qos.py +236 -0
  210. algo_cli/runtime_services.py +320 -0
  211. algo_cli/session_commands.py +95 -0
  212. algo_cli/session_mode.py +113 -0
  213. algo_cli/skills.py +430 -0
  214. algo_cli/slash_dispatch.py +1265 -0
  215. algo_cli/small_context.py +206 -0
  216. algo_cli/spawn_budget.py +89 -0
  217. algo_cli/task_ledger.py +84 -0
  218. algo_cli/task_router.py +197 -0
  219. algo_cli/tool_context.py +94 -0
  220. algo_cli/tool_contract.py +99 -0
  221. algo_cli/tool_policy.py +357 -0
  222. algo_cli/tool_runtime.py +647 -0
  223. algo_cli/tools.py +3056 -0
  224. algo_cli/url_scheme.py +174 -0
  225. algo_cli/verify.py +154 -0
  226. algo_cli/version_manifest.py +178 -0
  227. algo_cli/vision_screenshot_verify.py +76 -0
  228. algo_cli/workspace_resolver.py +68 -0
  229. algo_cli/x_account.py +209 -0
  230. algo_cli/xai_auth.py +374 -0
  231. algo_cli/xai_client.py +600 -0
  232. algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
  233. algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
  234. algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
  235. algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
  236. algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
  237. ollama_cli/__init__.py +67 -0
@@ -0,0 +1,510 @@
1
+ """ChatGPT/OpenAI OAuth 2.0 + PKCE helpers.
2
+
3
+ OpenAI's public OAuth configuration can vary by application. Algo CLI defaults
4
+ to the Codex browser OAuth client used by the subscription runtime, while still
5
+ allowing provider configuration through environment variables:
6
+
7
+ - OPENAI_OAUTH_CLIENT_ID (optional override for /chatgpt-login)
8
+ - OPENAI_CODEX_CLIENT_ID (optional override for the bundled Codex client)
9
+ - OPENAI_OAUTH_AUTHORIZE_URL (optional)
10
+ - OPENAI_OAUTH_TOKEN_URL (optional)
11
+ - OPENAI_API_BASE (optional, defaults to https://api.openai.com/v1)
12
+
13
+ The runtime shape intentionally mirrors xai_auth.py so tests and user-facing
14
+ commands behave consistently across cloud providers.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import base64
19
+ import hashlib
20
+ import http.server
21
+ import json
22
+ import os
23
+ import secrets
24
+ import shutil
25
+ import socket
26
+ import subprocess
27
+ import time
28
+ import urllib.parse
29
+ import urllib.request
30
+ import webbrowser
31
+ from typing import Any
32
+
33
+ from .config import CONFIG_DIR, _atomic_write_text
34
+
35
+ CHATGPT_CLIENT_ID = os.environ.get("OPENAI_OAUTH_CLIENT_ID", "").strip()
36
+ CHATGPT_AUTHORIZE_URL = os.environ.get("OPENAI_OAUTH_AUTHORIZE_URL", "https://auth.openai.com/oauth/authorize")
37
+ CHATGPT_TOKEN_URL = os.environ.get("OPENAI_OAUTH_TOKEN_URL", "https://auth.openai.com/oauth/token")
38
+ CHATGPT_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1").rstrip("/")
39
+ CHATGPT_CODEX_CLIENT_ID = os.environ.get("OPENAI_CODEX_CLIENT_ID", "app_EMoamEEZ73f0CkXaXp7hrann").strip()
40
+ CHATGPT_REDIRECT_HOST = "localhost"
41
+ CHATGPT_REDIRECT_PORT = 1455
42
+ CHATGPT_REDIRECT_PORT_RANGE = range(1455, 1456)
43
+ CHATGPT_REDIRECT_PATH = "/auth/callback"
44
+ CHATGPT_REDIRECT_URI = f"http://{CHATGPT_REDIRECT_HOST}:{CHATGPT_REDIRECT_PORT}{CHATGPT_REDIRECT_PATH}"
45
+ CHATGPT_DEFAULT_SCOPE = os.environ.get("OPENAI_OAUTH_SCOPE", "openid offline_access profile email")
46
+ CODEX_DEVICE_VERIFY_URL = "https://auth.openai.com/codex/device"
47
+ CODEX_AUTH_HOME = CONFIG_DIR / "codex-chatgpt"
48
+ AUTH_FILE = CONFIG_DIR / "chatgpt_auth.json"
49
+ _REFRESH_WINDOW_SECONDS = 60
50
+ _CALLBACK_TIMEOUT_SECONDS = 300.0
51
+
52
+
53
+ def _client_id() -> str:
54
+ return (CHATGPT_CLIENT_ID or os.environ.get("OPENAI_OAUTH_CLIENT_ID", "") or CHATGPT_CODEX_CLIENT_ID).strip()
55
+
56
+
57
+ def resolve_codex_bin() -> str | None:
58
+ """Find Codex even when a long-running shell has stale PATH."""
59
+ override = os.environ.get("CODEX_BIN", "").strip()
60
+ if override:
61
+ return override
62
+ for name in ("codex", "codex.cmd", "codex.exe", "codex.ps1"):
63
+ found = shutil.which(name)
64
+ if found:
65
+ return found
66
+ candidate_dirs: list[str] = []
67
+ appdata = os.environ.get("APPDATA", "").strip()
68
+ if appdata:
69
+ candidate_dirs.append(os.path.join(appdata, "npm"))
70
+ userprofile = os.environ.get("USERPROFILE", "").strip()
71
+ if userprofile:
72
+ candidate_dirs.append(os.path.join(userprofile, "AppData", "Roaming", "npm"))
73
+ home = os.path.expanduser("~")
74
+ if home and home != "~":
75
+ candidate_dirs.append(os.path.join(home, "AppData", "Roaming", "npm"))
76
+ candidate_dirs.append(os.path.join(home, ".npm-global", "bin"))
77
+ for directory in candidate_dirs:
78
+ for filename in ("codex.cmd", "codex.exe", "codex", "codex.ps1"):
79
+ candidate = os.path.join(directory, filename)
80
+ if os.path.exists(candidate):
81
+ return candidate
82
+ return None
83
+
84
+
85
+ def _b64url(data: bytes) -> str:
86
+ return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
87
+
88
+
89
+ def generate_pkce_pair() -> tuple[str, str]:
90
+ verifier = _b64url(secrets.token_bytes(32))
91
+ challenge = _b64url(hashlib.sha256(verifier.encode("ascii")).digest())
92
+ return verifier, challenge
93
+
94
+
95
+ def redirect_uri_for_port(port: int = CHATGPT_REDIRECT_PORT) -> str:
96
+ return f"http://{CHATGPT_REDIRECT_HOST}:{port}{CHATGPT_REDIRECT_PATH}"
97
+
98
+
99
+ def build_authorize_url(
100
+ *,
101
+ state: str,
102
+ code_challenge: str,
103
+ scope: str = CHATGPT_DEFAULT_SCOPE,
104
+ redirect_uri: str | None = None,
105
+ ) -> str:
106
+ client_id = _client_id()
107
+ if not client_id:
108
+ raise RuntimeError("ChatGPT OAuth client id is empty. Set OPENAI_OAUTH_CLIENT_ID or OPENAI_CODEX_CLIENT_ID.")
109
+ params = {
110
+ "response_type": "code",
111
+ "client_id": client_id,
112
+ "redirect_uri": redirect_uri or CHATGPT_REDIRECT_URI,
113
+ "scope": scope,
114
+ "state": state,
115
+ "code_challenge": code_challenge,
116
+ "code_challenge_method": "S256",
117
+ "id_token_add_organizations": "true",
118
+ "codex_cli_simplified_flow": "true",
119
+ "originator": os.environ.get("OPENAI_CODEX_ORIGINATOR", "pi").strip() or "pi",
120
+ }
121
+ return f"{CHATGPT_AUTHORIZE_URL}?{urllib.parse.urlencode(params)}"
122
+
123
+
124
+ class _CallbackHandler(http.server.BaseHTTPRequestHandler):
125
+ received: dict[str, str] = {}
126
+
127
+ def do_GET(self) -> None: # noqa: N802
128
+ parsed = urllib.parse.urlparse(self.path)
129
+ if parsed.path not in {"/callback", CHATGPT_REDIRECT_PATH}:
130
+ self.send_response(404)
131
+ self.end_headers()
132
+ return
133
+ qs = urllib.parse.parse_qs(parsed.query)
134
+ type(self).received = {k: v[0] for k, v in qs.items() if v}
135
+ self.send_response(200)
136
+ self.send_header("Content-Type", "text/html; charset=utf-8")
137
+ self.end_headers()
138
+ ok = "code" in type(self).received and "error" not in type(self).received
139
+ title = "ChatGPT login complete." if ok else "ChatGPT login failed."
140
+ self.wfile.write(
141
+ f"<html><body style='font-family:sans-serif;padding:2em'><h2>{title}</h2>"
142
+ "<p>You can close this tab and return to algo-cli.</p></body></html>".encode("utf-8")
143
+ )
144
+
145
+ def log_message(self, format: str, *args: Any) -> None:
146
+ return
147
+
148
+
149
+ def run_loopback_capture(*, timeout: float = _CALLBACK_TIMEOUT_SECONDS, redirect_port: int = CHATGPT_REDIRECT_PORT) -> dict[str, str]:
150
+ received: dict[str, str] = {}
151
+ Handler = type("_ChatGptLoopbackCallbackHandler", (_CallbackHandler,), {"received": received})
152
+ try:
153
+ server = http.server.HTTPServer((CHATGPT_REDIRECT_HOST, redirect_port), Handler)
154
+ except OSError:
155
+ return {}
156
+ server.timeout = 1.0
157
+ deadline = time.time() + timeout
158
+ try:
159
+ while not Handler.received and time.time() < deadline:
160
+ server.handle_request()
161
+ finally:
162
+ server.server_close()
163
+ return dict(Handler.received)
164
+
165
+
166
+ def _post_token_endpoint(form: dict[str, str]) -> dict[str, Any]:
167
+ data = urllib.parse.urlencode(form).encode("ascii")
168
+ req = urllib.request.Request(
169
+ CHATGPT_TOKEN_URL,
170
+ data=data,
171
+ headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"},
172
+ method="POST",
173
+ )
174
+ with urllib.request.urlopen(req, timeout=30) as resp:
175
+ return json.loads(resp.read().decode("utf-8"))
176
+
177
+
178
+ def _normalize_token_response(payload: dict[str, Any], *, fallback_refresh: str | None = None) -> dict[str, Any]:
179
+ if "access_token" not in payload:
180
+ raise RuntimeError(f"Token endpoint returned no access_token: {payload}")
181
+ now = int(time.time())
182
+ try:
183
+ expires_in = int(payload.get("expires_in", 3600))
184
+ except (TypeError, ValueError):
185
+ expires_in = 3600
186
+ expires_at = payload.get("expires_at")
187
+ try:
188
+ expires_at_int = int(expires_at) if expires_at is not None else now + expires_in
189
+ except (TypeError, ValueError):
190
+ expires_at_int = now + expires_in
191
+ normalized = {
192
+ "access_token": str(payload["access_token"]),
193
+ "refresh_token": payload.get("refresh_token") or fallback_refresh,
194
+ "token_type": payload.get("token_type", "Bearer"),
195
+ "expires_at": expires_at_int,
196
+ "scope": payload.get("scope", ""),
197
+ "obtained_at": int(payload.get("obtained_at", now) or now),
198
+ }
199
+ account_id = (
200
+ payload.get("account_id")
201
+ or payload.get("accountId")
202
+ or payload.get("chatgpt_account_id")
203
+ or _extract_chatgpt_account_id(str(payload["access_token"]))
204
+ )
205
+ if account_id:
206
+ normalized["account_id"] = account_id
207
+ return normalized
208
+
209
+
210
+ def _decode_jwt_payload(token: str) -> dict[str, Any]:
211
+ parts = str(token or "").split(".")
212
+ if len(parts) < 2:
213
+ return {}
214
+ payload = parts[1]
215
+ payload += "=" * (-len(payload) % 4)
216
+ try:
217
+ decoded = base64.urlsafe_b64decode(payload.encode("ascii"))
218
+ data = json.loads(decoded.decode("utf-8"))
219
+ except Exception:
220
+ return {}
221
+ return data if isinstance(data, dict) else {}
222
+
223
+
224
+ def _extract_chatgpt_account_id(token: str) -> str | None:
225
+ claims = _decode_jwt_payload(token)
226
+ auth_claim = claims.get("https://api.openai.com/auth")
227
+ if isinstance(auth_claim, dict):
228
+ value = auth_claim.get("chatgpt_account_id") or auth_claim.get("account_id")
229
+ if value:
230
+ return str(value)
231
+ for key in ("chatgpt_account_id", "account_id"):
232
+ value = claims.get(key)
233
+ if value:
234
+ return str(value)
235
+ return None
236
+
237
+
238
+ def _extract_codex_tokens(payload: dict[str, Any]) -> dict[str, Any]:
239
+ candidates: list[dict[str, Any]] = []
240
+ for key in ("tokens", "chatgptAuthTokens", "chatgpt_auth_tokens"):
241
+ value = payload.get(key)
242
+ if isinstance(value, dict):
243
+ candidates.append(value)
244
+ candidates.append(payload)
245
+
246
+ for candidate in candidates:
247
+ access = candidate.get("access_token") or candidate.get("accessToken")
248
+ if not access:
249
+ continue
250
+ token_payload = {
251
+ "access_token": access,
252
+ "refresh_token": candidate.get("refresh_token") or candidate.get("refreshToken"),
253
+ "token_type": candidate.get("token_type") or candidate.get("tokenType") or "Bearer",
254
+ "scope": candidate.get("scope") or payload.get("scope") or "",
255
+ "obtained_at": candidate.get("obtained_at") or candidate.get("obtainedAt") or payload.get("obtained_at"),
256
+ }
257
+ if candidate.get("expires_at") is not None:
258
+ token_payload["expires_at"] = candidate.get("expires_at")
259
+ elif candidate.get("expiresAt") is not None:
260
+ token_payload["expires_at"] = candidate.get("expiresAt")
261
+ elif candidate.get("expires_in") is not None:
262
+ token_payload["expires_in"] = candidate.get("expires_in")
263
+ elif candidate.get("expiresIn") is not None:
264
+ token_payload["expires_in"] = candidate.get("expiresIn")
265
+ normalized = _normalize_token_response(token_payload)
266
+ normalized["provider"] = "chatgpt-codex"
267
+ account_id = (
268
+ payload.get("account_id")
269
+ or payload.get("accountId")
270
+ or payload.get("chatgpt_account_id")
271
+ or candidate.get("account_id")
272
+ or candidate.get("accountId")
273
+ or candidate.get("chatgpt_account_id")
274
+ or _extract_chatgpt_account_id(str(access))
275
+ )
276
+ if account_id:
277
+ normalized["account_id"] = account_id
278
+ return normalized
279
+ raise RuntimeError("Codex auth.json did not contain ChatGPT access tokens.")
280
+
281
+
282
+ def import_codex_auth_file(path: str | os.PathLike[str] | None = None) -> dict[str, Any]:
283
+ auth_path = os.fspath(path or (CODEX_AUTH_HOME / "auth.json"))
284
+ try:
285
+ payload = json.loads(open(auth_path, "r", encoding="utf-8").read())
286
+ except FileNotFoundError as exc:
287
+ raise RuntimeError(f"Codex auth file was not created at {auth_path}.") from exc
288
+ except (OSError, json.JSONDecodeError) as exc:
289
+ raise RuntimeError(f"Could not read Codex auth file at {auth_path}: {exc}") from exc
290
+ if not isinstance(payload, dict):
291
+ raise RuntimeError(f"Codex auth file at {auth_path} did not contain a JSON object.")
292
+ tokens = _extract_codex_tokens(payload)
293
+ save_tokens(tokens)
294
+ return tokens
295
+
296
+
297
+ def run_codex_device_login(*, codex_bin: str | None = None, runner: Any = subprocess.run) -> dict[str, Any]:
298
+ resolved_codex_bin = codex_bin or resolve_codex_bin()
299
+ if not resolved_codex_bin:
300
+ raise RuntimeError(
301
+ "Codex CLI is not installed or not discoverable. Install it with: "
302
+ "npm install -g @openai/codex. If it is already installed, restart "
303
+ "your terminal or set CODEX_BIN to the full path to codex.cmd."
304
+ )
305
+ CODEX_AUTH_HOME.mkdir(parents=True, exist_ok=True)
306
+ env = os.environ.copy()
307
+ env["CODEX_HOME"] = str(CODEX_AUTH_HOME)
308
+ cmd = [resolved_codex_bin, "login", "--device-auth", "-c", 'cli_auth_credentials_store="file"']
309
+ try:
310
+ result = runner(cmd, env=env, check=False)
311
+ except FileNotFoundError as exc:
312
+ raise RuntimeError(
313
+ "Codex CLI is not installed or not discoverable. Install it with: "
314
+ "npm install -g @openai/codex. If it is already installed, restart "
315
+ "your terminal or set CODEX_BIN to the full path to codex.cmd."
316
+ ) from exc
317
+ returncode = getattr(result, "returncode", 0)
318
+ if returncode != 0:
319
+ raise RuntimeError(f"Codex device-code login failed with exit code {returncode}.")
320
+ return import_codex_auth_file(CODEX_AUTH_HOME / "auth.json")
321
+
322
+
323
+ def exchange_code(code: str, code_verifier: str, *, redirect_uri: str | None = None) -> dict[str, Any]:
324
+ payload = _post_token_endpoint(
325
+ {
326
+ "grant_type": "authorization_code",
327
+ "client_id": _client_id(),
328
+ "code": code,
329
+ "redirect_uri": redirect_uri or CHATGPT_REDIRECT_URI,
330
+ "code_verifier": code_verifier,
331
+ }
332
+ )
333
+ return _normalize_token_response(payload)
334
+
335
+
336
+ def refresh_access_token(
337
+ refresh_token: str,
338
+ *,
339
+ scope: str | None = CHATGPT_DEFAULT_SCOPE,
340
+ client_id: str | None = None,
341
+ ) -> dict[str, Any]:
342
+ form = {
343
+ "grant_type": "refresh_token",
344
+ "client_id": client_id or _client_id(),
345
+ "refresh_token": refresh_token,
346
+ }
347
+ if scope is not None:
348
+ form["scope"] = scope
349
+ payload = _post_token_endpoint(form)
350
+ return _normalize_token_response(payload, fallback_refresh=refresh_token)
351
+
352
+
353
+ def refresh_codex_access_token(refresh_token: str) -> dict[str, Any]:
354
+ tokens = refresh_access_token(refresh_token, scope=None, client_id=CHATGPT_CODEX_CLIENT_ID)
355
+ tokens["provider"] = "chatgpt-codex"
356
+ return tokens
357
+
358
+
359
+ def save_tokens(tokens: dict[str, Any]) -> None:
360
+ AUTH_FILE.parent.mkdir(parents=True, exist_ok=True)
361
+ _atomic_write_text(AUTH_FILE, json.dumps(tokens, indent=2))
362
+ try:
363
+ os.chmod(AUTH_FILE, 0o600)
364
+ except (OSError, NotImplementedError):
365
+ pass
366
+
367
+
368
+ def load_tokens() -> dict[str, Any] | None:
369
+ if not AUTH_FILE.exists():
370
+ return None
371
+ try:
372
+ return json.loads(AUTH_FILE.read_text(encoding="utf-8"))
373
+ except (OSError, json.JSONDecodeError):
374
+ return None
375
+
376
+
377
+ def clear_tokens() -> bool:
378
+ if not AUTH_FILE.exists():
379
+ return False
380
+ try:
381
+ AUTH_FILE.unlink()
382
+ return True
383
+ except OSError:
384
+ return False
385
+
386
+
387
+ def is_token_expired(tokens: dict[str, Any], *, window: int = _REFRESH_WINDOW_SECONDS) -> bool:
388
+ return int(tokens.get("expires_at", 0)) - window <= int(time.time())
389
+
390
+
391
+ def get_valid_token() -> str | None:
392
+ tokens = load_tokens()
393
+ if not tokens:
394
+ return None
395
+ if not is_token_expired(tokens):
396
+ return tokens.get("access_token")
397
+ refresh = tokens.get("refresh_token")
398
+ if not refresh:
399
+ return None
400
+ try:
401
+ if tokens.get("provider") == "chatgpt-codex":
402
+ new_tokens = refresh_codex_access_token(refresh)
403
+ else:
404
+ new_tokens = refresh_access_token(refresh)
405
+ except Exception:
406
+ return None
407
+ save_tokens(new_tokens)
408
+ return new_tokens.get("access_token")
409
+
410
+
411
+ def force_refresh_token() -> str | None:
412
+ tokens = load_tokens()
413
+ if not tokens or not tokens.get("refresh_token"):
414
+ return None
415
+ try:
416
+ if tokens.get("provider") == "chatgpt-codex":
417
+ new_tokens = refresh_codex_access_token(str(tokens["refresh_token"]))
418
+ else:
419
+ new_tokens = refresh_access_token(str(tokens["refresh_token"]))
420
+ except Exception:
421
+ return None
422
+ save_tokens(new_tokens)
423
+ return new_tokens.get("access_token")
424
+
425
+
426
+ def get_chatgpt_account_id() -> str | None:
427
+ tokens = load_tokens()
428
+ if not tokens:
429
+ return None
430
+ account_id = tokens.get("account_id") or tokens.get("accountId") or tokens.get("chatgpt_account_id")
431
+ if account_id:
432
+ return str(account_id)
433
+ access_token = tokens.get("access_token")
434
+ if not access_token:
435
+ return None
436
+ account_id = _extract_chatgpt_account_id(str(access_token))
437
+ if account_id:
438
+ tokens["account_id"] = account_id
439
+ save_tokens(tokens)
440
+ return account_id
441
+
442
+
443
+ def auth_status() -> dict[str, Any]:
444
+ tokens = load_tokens()
445
+ if not tokens:
446
+ return {"authenticated": False, "client_configured": bool(_client_id())}
447
+ expires_at = int(tokens.get("expires_at", 0))
448
+ return {
449
+ "authenticated": True,
450
+ "client_configured": bool(_client_id()),
451
+ "expires_at": expires_at,
452
+ "expires_in": max(0, expires_at - int(time.time())),
453
+ "scope": tokens.get("scope", ""),
454
+ "has_refresh_token": bool(tokens.get("refresh_token")),
455
+ "token_type": tokens.get("token_type", "Bearer"),
456
+ "has_account_id": bool(tokens.get("account_id") or tokens.get("accountId") or tokens.get("chatgpt_account_id")),
457
+ }
458
+
459
+
460
+ def port_is_free(port: int = CHATGPT_REDIRECT_PORT) -> bool:
461
+ sock = socket.socket()
462
+ sock.settimeout(0.2)
463
+ try:
464
+ return sock.connect_ex((CHATGPT_REDIRECT_HOST, port)) != 0
465
+ except OSError:
466
+ return False
467
+ finally:
468
+ sock.close()
469
+
470
+
471
+ def select_redirect_port() -> int | None:
472
+ for port in CHATGPT_REDIRECT_PORT_RANGE:
473
+ if port_is_free(port):
474
+ return port
475
+ return None
476
+
477
+
478
+ def begin_login(*, no_browser: bool = False, redirect_port: int = CHATGPT_REDIRECT_PORT) -> dict[str, str]:
479
+ state = secrets.token_urlsafe(32)
480
+ verifier, challenge = generate_pkce_pair()
481
+ redirect_uri = redirect_uri_for_port(redirect_port)
482
+ url = build_authorize_url(state=state, code_challenge=challenge, redirect_uri=redirect_uri)
483
+ if not no_browser:
484
+ opened = webbrowser.open(url)
485
+ if not opened:
486
+ no_browser = True
487
+ return {
488
+ "state": state,
489
+ "code_verifier": verifier,
490
+ "auth_url": url,
491
+ "redirect_uri": redirect_uri,
492
+ "redirect_port": str(redirect_port),
493
+ "ssh_tunnel_cmd": f"ssh -N -L {redirect_port}:{CHATGPT_REDIRECT_HOST}:{redirect_port} you@remote-host",
494
+ "browser_opened": not no_browser,
495
+ }
496
+
497
+
498
+ def complete_login(code_verifier: str, state: str, callback: dict[str, str] | None) -> dict[str, Any]:
499
+ if not callback:
500
+ raise RuntimeError("Timed out waiting for OAuth callback.")
501
+ if "error" in callback:
502
+ raise RuntimeError(f"OAuth error: {callback.get('error_description', callback['error'])}")
503
+ if callback.get("state") != state:
504
+ raise RuntimeError("OAuth state mismatch - possible CSRF attack.")
505
+ code = callback.get("code")
506
+ if not code:
507
+ raise RuntimeError("No authorization code in callback.")
508
+ tokens = exchange_code(code, code_verifier, redirect_uri=callback.get("redirect_uri"))
509
+ save_tokens(tokens)
510
+ return tokens