codex-autorunner 0.1.1__py3-none-any.whl → 1.0.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 (226) hide show
  1. codex_autorunner/__main__.py +4 -0
  2. codex_autorunner/agents/__init__.py +20 -0
  3. codex_autorunner/agents/base.py +2 -2
  4. codex_autorunner/agents/codex/harness.py +1 -1
  5. codex_autorunner/agents/opencode/__init__.py +4 -0
  6. codex_autorunner/agents/opencode/agent_config.py +104 -0
  7. codex_autorunner/agents/opencode/client.py +305 -28
  8. codex_autorunner/agents/opencode/harness.py +71 -20
  9. codex_autorunner/agents/opencode/logging.py +225 -0
  10. codex_autorunner/agents/opencode/run_prompt.py +261 -0
  11. codex_autorunner/agents/opencode/runtime.py +1202 -132
  12. codex_autorunner/agents/opencode/supervisor.py +194 -68
  13. codex_autorunner/agents/registry.py +258 -0
  14. codex_autorunner/agents/types.py +2 -2
  15. codex_autorunner/api.py +25 -0
  16. codex_autorunner/bootstrap.py +19 -40
  17. codex_autorunner/cli.py +234 -151
  18. codex_autorunner/core/about_car.py +44 -32
  19. codex_autorunner/core/adapter_utils.py +21 -0
  20. codex_autorunner/core/app_server_events.py +15 -6
  21. codex_autorunner/core/app_server_logging.py +55 -15
  22. codex_autorunner/core/app_server_prompts.py +28 -259
  23. codex_autorunner/core/app_server_threads.py +15 -26
  24. codex_autorunner/core/circuit_breaker.py +183 -0
  25. codex_autorunner/core/codex_runner.py +6 -0
  26. codex_autorunner/core/config.py +555 -133
  27. codex_autorunner/core/docs.py +54 -9
  28. codex_autorunner/core/drafts.py +82 -0
  29. codex_autorunner/core/engine.py +828 -274
  30. codex_autorunner/core/exceptions.py +60 -0
  31. codex_autorunner/core/flows/__init__.py +25 -0
  32. codex_autorunner/core/flows/controller.py +178 -0
  33. codex_autorunner/core/flows/definition.py +82 -0
  34. codex_autorunner/core/flows/models.py +75 -0
  35. codex_autorunner/core/flows/runtime.py +351 -0
  36. codex_autorunner/core/flows/store.py +485 -0
  37. codex_autorunner/core/flows/transition.py +133 -0
  38. codex_autorunner/core/flows/worker_process.py +242 -0
  39. codex_autorunner/core/hub.py +21 -13
  40. codex_autorunner/core/locks.py +118 -1
  41. codex_autorunner/core/logging_utils.py +9 -6
  42. codex_autorunner/core/path_utils.py +123 -0
  43. codex_autorunner/core/prompt.py +15 -7
  44. codex_autorunner/core/redaction.py +29 -0
  45. codex_autorunner/core/retry.py +61 -0
  46. codex_autorunner/core/review.py +888 -0
  47. codex_autorunner/core/review_context.py +161 -0
  48. codex_autorunner/core/run_index.py +223 -0
  49. codex_autorunner/core/runner_controller.py +44 -1
  50. codex_autorunner/core/runner_process.py +30 -1
  51. codex_autorunner/core/sqlite_utils.py +32 -0
  52. codex_autorunner/core/state.py +273 -44
  53. codex_autorunner/core/static_assets.py +55 -0
  54. codex_autorunner/core/supervisor_utils.py +67 -0
  55. codex_autorunner/core/text_delta_coalescer.py +43 -0
  56. codex_autorunner/core/update.py +20 -11
  57. codex_autorunner/core/update_runner.py +2 -0
  58. codex_autorunner/core/usage.py +107 -75
  59. codex_autorunner/core/utils.py +167 -3
  60. codex_autorunner/discovery.py +3 -3
  61. codex_autorunner/flows/ticket_flow/__init__.py +3 -0
  62. codex_autorunner/flows/ticket_flow/definition.py +91 -0
  63. codex_autorunner/integrations/agents/__init__.py +27 -0
  64. codex_autorunner/integrations/agents/agent_backend.py +142 -0
  65. codex_autorunner/integrations/agents/codex_backend.py +307 -0
  66. codex_autorunner/integrations/agents/opencode_backend.py +325 -0
  67. codex_autorunner/integrations/agents/run_event.py +71 -0
  68. codex_autorunner/integrations/app_server/client.py +708 -153
  69. codex_autorunner/integrations/app_server/supervisor.py +59 -33
  70. codex_autorunner/integrations/telegram/adapter.py +474 -185
  71. codex_autorunner/integrations/telegram/api_schemas.py +120 -0
  72. codex_autorunner/integrations/telegram/config.py +239 -1
  73. codex_autorunner/integrations/telegram/constants.py +19 -1
  74. codex_autorunner/integrations/telegram/dispatch.py +44 -8
  75. codex_autorunner/integrations/telegram/doctor.py +47 -0
  76. codex_autorunner/integrations/telegram/handlers/approvals.py +12 -10
  77. codex_autorunner/integrations/telegram/handlers/callbacks.py +15 -1
  78. codex_autorunner/integrations/telegram/handlers/commands/__init__.py +29 -0
  79. codex_autorunner/integrations/telegram/handlers/commands/approvals.py +173 -0
  80. codex_autorunner/integrations/telegram/handlers/commands/execution.py +2595 -0
  81. codex_autorunner/integrations/telegram/handlers/commands/files.py +1408 -0
  82. codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
  83. codex_autorunner/integrations/telegram/handlers/commands/formatting.py +81 -0
  84. codex_autorunner/integrations/telegram/handlers/commands/github.py +1688 -0
  85. codex_autorunner/integrations/telegram/handlers/commands/shared.py +190 -0
  86. codex_autorunner/integrations/telegram/handlers/commands/voice.py +112 -0
  87. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +2043 -0
  88. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +954 -5689
  89. codex_autorunner/integrations/telegram/handlers/{commands.py → commands_spec.py} +11 -4
  90. codex_autorunner/integrations/telegram/handlers/messages.py +374 -49
  91. codex_autorunner/integrations/telegram/handlers/questions.py +389 -0
  92. codex_autorunner/integrations/telegram/handlers/selections.py +6 -4
  93. codex_autorunner/integrations/telegram/handlers/utils.py +171 -0
  94. codex_autorunner/integrations/telegram/helpers.py +90 -18
  95. codex_autorunner/integrations/telegram/notifications.py +126 -35
  96. codex_autorunner/integrations/telegram/outbox.py +214 -43
  97. codex_autorunner/integrations/telegram/progress_stream.py +42 -19
  98. codex_autorunner/integrations/telegram/runtime.py +24 -13
  99. codex_autorunner/integrations/telegram/service.py +500 -129
  100. codex_autorunner/integrations/telegram/state.py +1278 -330
  101. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
  102. codex_autorunner/integrations/telegram/transport.py +37 -4
  103. codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
  104. codex_autorunner/integrations/telegram/types.py +22 -2
  105. codex_autorunner/integrations/telegram/voice.py +14 -15
  106. codex_autorunner/manifest.py +2 -0
  107. codex_autorunner/plugin_api.py +22 -0
  108. codex_autorunner/routes/__init__.py +25 -14
  109. codex_autorunner/routes/agents.py +18 -78
  110. codex_autorunner/routes/analytics.py +239 -0
  111. codex_autorunner/routes/base.py +142 -113
  112. codex_autorunner/routes/file_chat.py +836 -0
  113. codex_autorunner/routes/flows.py +980 -0
  114. codex_autorunner/routes/messages.py +459 -0
  115. codex_autorunner/routes/repos.py +17 -0
  116. codex_autorunner/routes/review.py +148 -0
  117. codex_autorunner/routes/sessions.py +16 -8
  118. codex_autorunner/routes/settings.py +22 -0
  119. codex_autorunner/routes/shared.py +33 -3
  120. codex_autorunner/routes/system.py +22 -1
  121. codex_autorunner/routes/usage.py +87 -0
  122. codex_autorunner/routes/voice.py +5 -13
  123. codex_autorunner/routes/workspace.py +271 -0
  124. codex_autorunner/server.py +2 -1
  125. codex_autorunner/static/agentControls.js +9 -1
  126. codex_autorunner/static/agentEvents.js +248 -0
  127. codex_autorunner/static/app.js +27 -22
  128. codex_autorunner/static/autoRefresh.js +29 -1
  129. codex_autorunner/static/bootstrap.js +1 -0
  130. codex_autorunner/static/bus.js +1 -0
  131. codex_autorunner/static/cache.js +1 -0
  132. codex_autorunner/static/constants.js +20 -4
  133. codex_autorunner/static/dashboard.js +162 -150
  134. codex_autorunner/static/diffRenderer.js +37 -0
  135. codex_autorunner/static/docChatCore.js +324 -0
  136. codex_autorunner/static/docChatStorage.js +65 -0
  137. codex_autorunner/static/docChatVoice.js +65 -0
  138. codex_autorunner/static/docEditor.js +133 -0
  139. codex_autorunner/static/env.js +1 -0
  140. codex_autorunner/static/eventSummarizer.js +166 -0
  141. codex_autorunner/static/fileChat.js +182 -0
  142. codex_autorunner/static/health.js +155 -0
  143. codex_autorunner/static/hub.js +67 -126
  144. codex_autorunner/static/index.html +788 -807
  145. codex_autorunner/static/liveUpdates.js +59 -0
  146. codex_autorunner/static/loader.js +1 -0
  147. codex_autorunner/static/messages.js +470 -0
  148. codex_autorunner/static/mobileCompact.js +2 -1
  149. codex_autorunner/static/settings.js +24 -205
  150. codex_autorunner/static/styles.css +7577 -3758
  151. codex_autorunner/static/tabs.js +28 -5
  152. codex_autorunner/static/terminal.js +14 -0
  153. codex_autorunner/static/terminalManager.js +53 -59
  154. codex_autorunner/static/ticketChatActions.js +333 -0
  155. codex_autorunner/static/ticketChatEvents.js +16 -0
  156. codex_autorunner/static/ticketChatStorage.js +16 -0
  157. codex_autorunner/static/ticketChatStream.js +264 -0
  158. codex_autorunner/static/ticketEditor.js +750 -0
  159. codex_autorunner/static/ticketVoice.js +9 -0
  160. codex_autorunner/static/tickets.js +1315 -0
  161. codex_autorunner/static/utils.js +32 -3
  162. codex_autorunner/static/voice.js +21 -7
  163. codex_autorunner/static/workspace.js +672 -0
  164. codex_autorunner/static/workspaceApi.js +53 -0
  165. codex_autorunner/static/workspaceFileBrowser.js +504 -0
  166. codex_autorunner/tickets/__init__.py +20 -0
  167. codex_autorunner/tickets/agent_pool.py +377 -0
  168. codex_autorunner/tickets/files.py +85 -0
  169. codex_autorunner/tickets/frontmatter.py +55 -0
  170. codex_autorunner/tickets/lint.py +102 -0
  171. codex_autorunner/tickets/models.py +95 -0
  172. codex_autorunner/tickets/outbox.py +232 -0
  173. codex_autorunner/tickets/replies.py +179 -0
  174. codex_autorunner/tickets/runner.py +823 -0
  175. codex_autorunner/tickets/spec_ingest.py +77 -0
  176. codex_autorunner/voice/capture.py +7 -7
  177. codex_autorunner/voice/service.py +51 -9
  178. codex_autorunner/web/app.py +419 -199
  179. codex_autorunner/web/hub_jobs.py +13 -2
  180. codex_autorunner/web/middleware.py +47 -13
  181. codex_autorunner/web/pty_session.py +26 -13
  182. codex_autorunner/web/schemas.py +114 -109
  183. codex_autorunner/web/static_assets.py +55 -42
  184. codex_autorunner/web/static_refresh.py +86 -0
  185. codex_autorunner/workspace/__init__.py +40 -0
  186. codex_autorunner/workspace/paths.py +319 -0
  187. {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +20 -21
  188. codex_autorunner-1.0.0.dist-info/RECORD +251 -0
  189. {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
  190. codex_autorunner/core/doc_chat.py +0 -1415
  191. codex_autorunner/core/snapshot.py +0 -580
  192. codex_autorunner/integrations/github/chatops.py +0 -268
  193. codex_autorunner/integrations/github/pr_flow.py +0 -1314
  194. codex_autorunner/routes/docs.py +0 -381
  195. codex_autorunner/routes/github.py +0 -327
  196. codex_autorunner/routes/runs.py +0 -118
  197. codex_autorunner/spec_ingest.py +0 -788
  198. codex_autorunner/static/docChatActions.js +0 -279
  199. codex_autorunner/static/docChatEvents.js +0 -300
  200. codex_autorunner/static/docChatRender.js +0 -205
  201. codex_autorunner/static/docChatStream.js +0 -361
  202. codex_autorunner/static/docs.js +0 -20
  203. codex_autorunner/static/docsClipboard.js +0 -69
  204. codex_autorunner/static/docsCrud.js +0 -257
  205. codex_autorunner/static/docsDocUpdates.js +0 -62
  206. codex_autorunner/static/docsDrafts.js +0 -16
  207. codex_autorunner/static/docsElements.js +0 -69
  208. codex_autorunner/static/docsInit.js +0 -274
  209. codex_autorunner/static/docsParse.js +0 -160
  210. codex_autorunner/static/docsSnapshot.js +0 -87
  211. codex_autorunner/static/docsSpecIngest.js +0 -263
  212. codex_autorunner/static/docsState.js +0 -127
  213. codex_autorunner/static/docsThreadRegistry.js +0 -44
  214. codex_autorunner/static/docsUi.js +0 -153
  215. codex_autorunner/static/docsVoice.js +0 -56
  216. codex_autorunner/static/github.js +0 -442
  217. codex_autorunner/static/logs.js +0 -640
  218. codex_autorunner/static/runs.js +0 -409
  219. codex_autorunner/static/snapshot.js +0 -124
  220. codex_autorunner/static/state.js +0 -86
  221. codex_autorunner/static/todoPreview.js +0 -27
  222. codex_autorunner/workspace.py +0 -16
  223. codex_autorunner-0.1.1.dist-info/RECORD +0 -191
  224. {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
  225. {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
  226. {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,47 @@
1
+ """Telegram integration doctor checks."""
2
+
3
+ from typing import Any, Dict, Union
4
+
5
+ from ...core.config import HubConfig, RepoConfig
6
+ from ...core.engine import DoctorCheck
7
+ from ...core.optional_dependencies import missing_optional_dependencies
8
+
9
+
10
+ def telegram_doctor_checks(
11
+ config: Union[HubConfig, RepoConfig, Dict[str, Any]],
12
+ ) -> list[DoctorCheck]:
13
+ """Run Telegram-specific doctor checks.
14
+
15
+ Returns a list of DoctorCheck objects for Telegram integration.
16
+ Works with HubConfig, RepoConfig, or raw dict.
17
+ """
18
+ checks: list[DoctorCheck] = []
19
+ telegram_cfg = None
20
+
21
+ if isinstance(config, dict):
22
+ telegram_cfg = config.get("telegram_bot")
23
+ elif isinstance(config.raw, dict):
24
+ telegram_cfg = config.raw.get("telegram_bot")
25
+
26
+ if isinstance(telegram_cfg, dict) and telegram_cfg.get("enabled") is True:
27
+ missing_telegram = missing_optional_dependencies((("httpx", "httpx"),))
28
+ if missing_telegram:
29
+ deps_list = ", ".join(missing_telegram)
30
+ checks.append(
31
+ DoctorCheck(
32
+ check_id="telegram.dependencies",
33
+ status="error",
34
+ message=f"Telegram is enabled but missing optional deps: {deps_list}",
35
+ fix="Install with `pip install codex-autorunner[telegram]`.",
36
+ )
37
+ )
38
+ else:
39
+ checks.append(
40
+ DoctorCheck(
41
+ check_id="telegram.dependencies",
42
+ status="ok",
43
+ message="Telegram dependencies are installed.",
44
+ )
45
+ )
46
+
47
+ return checks
@@ -22,7 +22,7 @@ from ..types import PendingApproval
22
22
 
23
23
  class TelegramApprovalHandlers:
24
24
  async def _restore_pending_approvals(self) -> None:
25
- state = self._store.load()
25
+ state = await self._store.load()
26
26
  if not state.pending_approvals:
27
27
  return
28
28
  grouped: dict[tuple[int, int | None], list[PendingApprovalRecord]] = {}
@@ -35,7 +35,7 @@ class TelegramApprovalHandlers:
35
35
  age = _approval_age_seconds(record.created_at)
36
36
  age_label = f"{age}s" if isinstance(age, int) else "unknown age"
37
37
  items.append(f"{record.request_id} ({age_label})")
38
- self._store.clear_pending_approval(record.request_id)
38
+ await self._store.clear_pending_approval(record.request_id)
39
39
  message = (
40
40
  "Cleared stale approval requests from a previous session. "
41
41
  "Re-run the request or use /interrupt if the turn is still active.\n"
@@ -83,7 +83,7 @@ class TelegramApprovalHandlers:
83
83
  created_at=created_at,
84
84
  topic_key=ctx.topic_key,
85
85
  )
86
- self._store.upsert_pending_approval(approval_record)
86
+ await self._store.upsert_pending_approval(approval_record)
87
87
  log_event(
88
88
  self._logger,
89
89
  logging.INFO,
@@ -102,7 +102,7 @@ class TelegramApprovalHandlers:
102
102
  "telegram.approval.callback_too_long",
103
103
  request_id=request_id,
104
104
  )
105
- self._store.clear_pending_approval(request_id)
105
+ await self._store.clear_pending_approval(request_id)
106
106
  return "cancel"
107
107
  payload_text, parse_mode = self._prepare_outgoing_text(
108
108
  prompt,
@@ -132,7 +132,7 @@ class TelegramApprovalHandlers:
132
132
  thread_id=ctx.thread_id,
133
133
  exc=exc,
134
134
  )
135
- self._store.clear_pending_approval(request_id)
135
+ await self._store.clear_pending_approval(request_id)
136
136
  try:
137
137
  await self._send_message(
138
138
  ctx.chat_id,
@@ -147,7 +147,7 @@ class TelegramApprovalHandlers:
147
147
  message_id = response.get("message_id") if isinstance(response, dict) else None
148
148
  if isinstance(message_id, int):
149
149
  approval_record.message_id = message_id
150
- self._store.upsert_pending_approval(approval_record)
150
+ await self._store.upsert_pending_approval(approval_record)
151
151
  loop = asyncio.get_running_loop()
152
152
  future: asyncio.Future[ApprovalDecision] = loop.create_future()
153
153
  pending = PendingApproval(
@@ -171,7 +171,7 @@ class TelegramApprovalHandlers:
171
171
  )
172
172
  except asyncio.TimeoutError:
173
173
  self._pending_approvals.pop(request_id, None)
174
- self._store.clear_pending_approval(request_id)
174
+ await self._store.clear_pending_approval(request_id)
175
175
  runtime.pending_request_id = None
176
176
  log_event(
177
177
  self._logger,
@@ -193,14 +193,14 @@ class TelegramApprovalHandlers:
193
193
  return "cancel"
194
194
  except asyncio.CancelledError:
195
195
  self._pending_approvals.pop(request_id, None)
196
- self._store.clear_pending_approval(request_id)
196
+ await self._store.clear_pending_approval(request_id)
197
197
  runtime.pending_request_id = None
198
198
  raise
199
199
 
200
200
  async def _handle_approval_callback(
201
201
  self, callback: TelegramCallbackQuery, parsed: ApprovalCallback
202
202
  ) -> None:
203
- self._store.clear_pending_approval(parsed.request_id)
203
+ await self._store.clear_pending_approval(parsed.request_id)
204
204
  pending = self._pending_approvals.pop(parsed.request_id, None)
205
205
  if pending is None:
206
206
  await self._answer_callback(callback, "Approval already handled")
@@ -215,7 +215,9 @@ class TelegramApprovalHandlers:
215
215
  elif pending.topic_key:
216
216
  runtime_key = pending.topic_key
217
217
  else:
218
- runtime_key = self._resolve_topic_key(pending.chat_id, pending.thread_id)
218
+ runtime_key = await self._resolve_topic_key(
219
+ pending.chat_id, pending.thread_id
220
+ )
219
221
  runtime = self._router.runtime_for(runtime_key)
220
222
  runtime.pending_request_id = None
221
223
  log_event(
@@ -11,6 +11,10 @@ from ..adapter import (
11
11
  EffortCallback,
12
12
  ModelCallback,
13
13
  PageCallback,
14
+ QuestionCancelCallback,
15
+ QuestionCustomCallback,
16
+ QuestionDoneCallback,
17
+ QuestionOptionCallback,
14
18
  ResumeCallback,
15
19
  ReviewCommitCallback,
16
20
  TelegramCallbackQuery,
@@ -30,9 +34,19 @@ async def handle_callback(handlers: Any, callback: TelegramCallbackQuery) -> Non
30
34
  return
31
35
  key = None
32
36
  if callback.chat_id is not None:
33
- key = handlers._resolve_topic_key(callback.chat_id, callback.thread_id)
37
+ key = await handlers._resolve_topic_key(callback.chat_id, callback.thread_id)
34
38
  if isinstance(parsed, ApprovalCallback):
35
39
  await handlers._handle_approval_callback(callback, parsed)
40
+ elif isinstance(
41
+ parsed,
42
+ (
43
+ QuestionOptionCallback,
44
+ QuestionCancelCallback,
45
+ QuestionCustomCallback,
46
+ QuestionDoneCallback,
47
+ ),
48
+ ):
49
+ await handlers._handle_question_callback(callback, parsed)
36
50
  elif isinstance(parsed, ResumeCallback):
37
51
  if key:
38
52
  state = handlers._resume_options.get(key)
@@ -0,0 +1,29 @@
1
+ """Command handler modules for Telegram integration.
2
+
3
+ This package contains focused modules for handling different categories of Telegram commands.
4
+ """
5
+
6
+ from ..commands_spec import CommandSpec, build_command_specs
7
+ from .approvals import ApprovalsCommands
8
+ from .execution import ExecutionCommands
9
+ from .files import FilesCommands
10
+ from .flows import FlowCommands
11
+ from .formatting import FormattingHelpers
12
+ from .github import GitHubCommands
13
+ from .shared import SharedHelpers
14
+ from .voice import VoiceCommands
15
+ from .workspace import WorkspaceCommands
16
+
17
+ __all__ = [
18
+ "SharedHelpers",
19
+ "WorkspaceCommands",
20
+ "GitHubCommands",
21
+ "FilesCommands",
22
+ "VoiceCommands",
23
+ "FlowCommands",
24
+ "ExecutionCommands",
25
+ "ApprovalsCommands",
26
+ "FormattingHelpers",
27
+ "CommandSpec",
28
+ "build_command_specs",
29
+ ]
@@ -0,0 +1,173 @@
1
+ import asyncio
2
+ from typing import Any, Optional
3
+
4
+ from ....app_server.client import CodexAppServerError
5
+ from ...adapter import TelegramMessage
6
+ from ...config import AppServerUnavailableError
7
+ from ...constants import APPROVAL_POLICY_VALUES, APPROVAL_PRESETS
8
+ from ...helpers import (
9
+ _clear_policy_overrides,
10
+ _extract_rate_limits,
11
+ _format_persist_note,
12
+ _format_sandbox_policy,
13
+ _normalize_approval_preset,
14
+ _set_policy_overrides,
15
+ )
16
+ from .shared import SharedHelpers
17
+
18
+
19
+ class ApprovalsCommands(SharedHelpers):
20
+ async def _read_rate_limits(
21
+ self, workspace_path: Optional[str], *, agent: str
22
+ ) -> Optional[dict[str, Any]]:
23
+ if self._agent_rate_limit_source(agent) != "app_server":
24
+ return None
25
+ try:
26
+ client = await self._client_for_workspace(workspace_path)
27
+ except AppServerUnavailableError:
28
+ return None
29
+ if client is None:
30
+ return None
31
+ for method in ("account/rateLimits/read", "account/read"):
32
+ try:
33
+ result = await client.request(method, params=None, timeout=5.0)
34
+ except (CodexAppServerError, asyncio.TimeoutError):
35
+ continue
36
+ rate_limits = _extract_rate_limits(result)
37
+ if rate_limits:
38
+ return rate_limits
39
+ return None
40
+
41
+ async def _handle_approvals(
42
+ self, message: TelegramMessage, args: str, _runtime: Optional[Any] = None
43
+ ) -> None:
44
+ argv = self._parse_command_args(args)
45
+ record = await self._router.ensure_topic(message.chat_id, message.thread_id)
46
+ argv, persist = self._extract_persist_flag(argv)
47
+ if not argv:
48
+ await self._send_approval_status(message, record)
49
+ return
50
+ mode = argv[0].lower()
51
+ if mode in ("yolo", "off", "disable", "disabled"):
52
+ await self._set_approval_mode(message, "yolo", persist=persist)
53
+ return
54
+ if mode in ("safe", "on", "enable", "enabled"):
55
+ await self._set_approval_mode(message, "safe", persist=persist)
56
+ return
57
+ preset = _normalize_approval_preset(mode)
58
+ if mode == "preset" and len(argv) > 1:
59
+ preset = _normalize_approval_preset(argv[1])
60
+ if preset:
61
+ await self._apply_preset_policy(message, preset, persist=persist)
62
+ return
63
+ approval_policy = argv[0] if argv[0] in APPROVAL_POLICY_VALUES else None
64
+ if approval_policy:
65
+ sandbox_policy = argv[1] if len(argv) > 1 else None
66
+ await self._apply_direct_policy(
67
+ message, approval_policy, sandbox_policy, persist=persist
68
+ )
69
+ return
70
+ await self._send_approval_usage(message)
71
+
72
+ def _extract_persist_flag(self, argv: list[str]) -> tuple[list[str], bool]:
73
+ """Return argv without the persist flag and whether the flag was provided."""
74
+ if "--persist" not in argv:
75
+ return argv, False
76
+ return [arg for arg in argv if arg != "--persist"], True
77
+
78
+ async def _send_approval_status(
79
+ self, message: TelegramMessage, record: Any
80
+ ) -> None:
81
+ """Send the current approval mode and policy to the user."""
82
+ approval_policy, sandbox_policy = self._effective_policies(record)
83
+ await self._send_message(
84
+ message.chat_id,
85
+ "\n".join(
86
+ [
87
+ f"Approval mode: {record.approval_mode}",
88
+ f"Approval policy: {approval_policy or 'default'}",
89
+ f"Sandbox policy: {_format_sandbox_policy(sandbox_policy)}",
90
+ "Usage: /approvals yolo|safe|read-only|auto|full-access",
91
+ ]
92
+ ),
93
+ thread_id=message.thread_id,
94
+ reply_to=message.message_id,
95
+ )
96
+
97
+ async def _send_approval_usage(self, message: TelegramMessage) -> None:
98
+ """Send the usage hint for the /approvals command."""
99
+ await self._send_message(
100
+ message.chat_id,
101
+ "Usage: /approvals yolo|safe|read-only|auto|full-access",
102
+ thread_id=message.thread_id,
103
+ reply_to=message.message_id,
104
+ )
105
+
106
+ async def _set_approval_mode(
107
+ self, message: TelegramMessage, mode: str, *, persist: bool
108
+ ) -> None:
109
+ """Switch between safe and yolo modes and clear overrides."""
110
+ await self._router.set_approval_mode(message.chat_id, message.thread_id, mode)
111
+ await self._router.update_topic(
112
+ message.chat_id,
113
+ message.thread_id,
114
+ lambda record: _clear_policy_overrides(record),
115
+ )
116
+ await self._send_message(
117
+ message.chat_id,
118
+ _format_persist_note(f"Approval mode set to {mode}.", persist=persist),
119
+ thread_id=message.thread_id,
120
+ reply_to=message.message_id,
121
+ )
122
+
123
+ async def _apply_preset_policy(
124
+ self, message: TelegramMessage, preset: str, *, persist: bool
125
+ ) -> None:
126
+ """Apply an approval preset and persist if requested."""
127
+ approval_policy, sandbox_policy = APPROVAL_PRESETS[preset]
128
+ await self._router.update_topic(
129
+ message.chat_id,
130
+ message.thread_id,
131
+ lambda record: _set_policy_overrides(
132
+ record,
133
+ approval_policy=approval_policy,
134
+ sandbox_policy=sandbox_policy,
135
+ ),
136
+ )
137
+ await self._send_message(
138
+ message.chat_id,
139
+ _format_persist_note(
140
+ f"Approval policy set to {approval_policy} with sandbox {sandbox_policy}.",
141
+ persist=persist,
142
+ ),
143
+ thread_id=message.thread_id,
144
+ reply_to=message.message_id,
145
+ )
146
+
147
+ async def _apply_direct_policy(
148
+ self,
149
+ message: TelegramMessage,
150
+ approval_policy: str,
151
+ sandbox_policy: Optional[str],
152
+ *,
153
+ persist: bool,
154
+ ) -> None:
155
+ """Set explicit approval and sandbox policies."""
156
+ await self._router.update_topic(
157
+ message.chat_id,
158
+ message.thread_id,
159
+ lambda record: _set_policy_overrides(
160
+ record,
161
+ approval_policy=approval_policy,
162
+ sandbox_policy=sandbox_policy,
163
+ ),
164
+ )
165
+ await self._send_message(
166
+ message.chat_id,
167
+ _format_persist_note(
168
+ f"Approval policy set to {approval_policy} with sandbox {sandbox_policy or 'default'}.",
169
+ persist=persist,
170
+ ),
171
+ thread_id=message.thread_id,
172
+ reply_to=message.message_id,
173
+ )