codex-autorunner 1.0.0__py3-none-any.whl → 1.2.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 (227) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/agents/codex/harness.py +1 -1
  3. codex_autorunner/agents/opencode/client.py +113 -4
  4. codex_autorunner/agents/opencode/constants.py +3 -0
  5. codex_autorunner/agents/opencode/harness.py +6 -1
  6. codex_autorunner/agents/opencode/runtime.py +59 -18
  7. codex_autorunner/agents/opencode/supervisor.py +4 -0
  8. codex_autorunner/agents/registry.py +36 -7
  9. codex_autorunner/bootstrap.py +226 -4
  10. codex_autorunner/cli.py +5 -1174
  11. codex_autorunner/codex_cli.py +20 -84
  12. codex_autorunner/core/__init__.py +20 -0
  13. codex_autorunner/core/about_car.py +119 -1
  14. codex_autorunner/core/app_server_ids.py +59 -0
  15. codex_autorunner/core/app_server_threads.py +17 -2
  16. codex_autorunner/core/app_server_utils.py +165 -0
  17. codex_autorunner/core/archive.py +349 -0
  18. codex_autorunner/core/codex_runner.py +6 -2
  19. codex_autorunner/core/config.py +433 -4
  20. codex_autorunner/core/context_awareness.py +38 -0
  21. codex_autorunner/core/docs.py +0 -122
  22. codex_autorunner/core/drafts.py +58 -4
  23. codex_autorunner/core/exceptions.py +4 -0
  24. codex_autorunner/core/filebox.py +265 -0
  25. codex_autorunner/core/flows/controller.py +96 -2
  26. codex_autorunner/core/flows/models.py +13 -0
  27. codex_autorunner/core/flows/reasons.py +52 -0
  28. codex_autorunner/core/flows/reconciler.py +134 -0
  29. codex_autorunner/core/flows/runtime.py +57 -4
  30. codex_autorunner/core/flows/store.py +142 -7
  31. codex_autorunner/core/flows/transition.py +27 -15
  32. codex_autorunner/core/flows/ux_helpers.py +272 -0
  33. codex_autorunner/core/flows/worker_process.py +32 -6
  34. codex_autorunner/core/git_utils.py +62 -0
  35. codex_autorunner/core/hub.py +291 -20
  36. codex_autorunner/core/lifecycle_events.py +253 -0
  37. codex_autorunner/core/notifications.py +14 -2
  38. codex_autorunner/core/path_utils.py +2 -1
  39. codex_autorunner/core/pma_audit.py +224 -0
  40. codex_autorunner/core/pma_context.py +496 -0
  41. codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
  42. codex_autorunner/core/pma_lifecycle.py +527 -0
  43. codex_autorunner/core/pma_queue.py +367 -0
  44. codex_autorunner/core/pma_safety.py +221 -0
  45. codex_autorunner/core/pma_state.py +115 -0
  46. codex_autorunner/core/ports/__init__.py +28 -0
  47. codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
  48. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  49. codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
  50. codex_autorunner/core/prompt.py +0 -80
  51. codex_autorunner/core/prompts.py +56 -172
  52. codex_autorunner/core/redaction.py +0 -4
  53. codex_autorunner/core/review_context.py +11 -9
  54. codex_autorunner/core/runner_controller.py +35 -33
  55. codex_autorunner/core/runner_state.py +147 -0
  56. codex_autorunner/core/runtime.py +829 -0
  57. codex_autorunner/core/sqlite_utils.py +13 -4
  58. codex_autorunner/core/state.py +7 -10
  59. codex_autorunner/core/state_roots.py +62 -0
  60. codex_autorunner/core/supervisor_protocol.py +15 -0
  61. codex_autorunner/core/templates/__init__.py +39 -0
  62. codex_autorunner/core/templates/git_mirror.py +234 -0
  63. codex_autorunner/core/templates/provenance.py +56 -0
  64. codex_autorunner/core/templates/scan_cache.py +120 -0
  65. codex_autorunner/core/text_delta_coalescer.py +54 -0
  66. codex_autorunner/core/ticket_linter_cli.py +218 -0
  67. codex_autorunner/core/ticket_manager_cli.py +494 -0
  68. codex_autorunner/core/time_utils.py +11 -0
  69. codex_autorunner/core/types.py +18 -0
  70. codex_autorunner/core/update.py +4 -5
  71. codex_autorunner/core/update_paths.py +28 -0
  72. codex_autorunner/core/usage.py +164 -12
  73. codex_autorunner/core/utils.py +125 -15
  74. codex_autorunner/flows/review/__init__.py +17 -0
  75. codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
  76. codex_autorunner/flows/ticket_flow/definition.py +52 -3
  77. codex_autorunner/integrations/agents/__init__.py +11 -19
  78. codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
  79. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  80. codex_autorunner/integrations/agents/codex_backend.py +177 -25
  81. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  82. codex_autorunner/integrations/agents/opencode_backend.py +305 -32
  83. codex_autorunner/integrations/agents/runner.py +86 -0
  84. codex_autorunner/integrations/agents/wiring.py +279 -0
  85. codex_autorunner/integrations/app_server/client.py +7 -60
  86. codex_autorunner/integrations/app_server/env.py +2 -107
  87. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  88. codex_autorunner/integrations/telegram/adapter.py +65 -0
  89. codex_autorunner/integrations/telegram/config.py +46 -0
  90. codex_autorunner/integrations/telegram/constants.py +1 -1
  91. codex_autorunner/integrations/telegram/doctor.py +228 -6
  92. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
  93. codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
  94. codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
  95. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1496 -71
  96. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
  97. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
  98. codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
  99. codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
  100. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  101. codex_autorunner/integrations/telegram/helpers.py +22 -1
  102. codex_autorunner/integrations/telegram/runtime.py +9 -4
  103. codex_autorunner/integrations/telegram/service.py +45 -10
  104. codex_autorunner/integrations/telegram/state.py +38 -0
  105. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
  106. codex_autorunner/integrations/telegram/transport.py +13 -4
  107. codex_autorunner/integrations/templates/__init__.py +27 -0
  108. codex_autorunner/integrations/templates/scan_agent.py +312 -0
  109. codex_autorunner/routes/__init__.py +37 -76
  110. codex_autorunner/routes/agents.py +2 -137
  111. codex_autorunner/routes/analytics.py +2 -238
  112. codex_autorunner/routes/app_server.py +2 -131
  113. codex_autorunner/routes/base.py +2 -596
  114. codex_autorunner/routes/file_chat.py +4 -833
  115. codex_autorunner/routes/flows.py +4 -977
  116. codex_autorunner/routes/messages.py +4 -456
  117. codex_autorunner/routes/repos.py +2 -196
  118. codex_autorunner/routes/review.py +2 -147
  119. codex_autorunner/routes/sessions.py +2 -175
  120. codex_autorunner/routes/settings.py +2 -168
  121. codex_autorunner/routes/shared.py +2 -275
  122. codex_autorunner/routes/system.py +4 -193
  123. codex_autorunner/routes/usage.py +2 -86
  124. codex_autorunner/routes/voice.py +2 -119
  125. codex_autorunner/routes/workspace.py +2 -270
  126. codex_autorunner/server.py +4 -4
  127. codex_autorunner/static/agentControls.js +61 -16
  128. codex_autorunner/static/app.js +126 -14
  129. codex_autorunner/static/archive.js +826 -0
  130. codex_autorunner/static/archiveApi.js +37 -0
  131. codex_autorunner/static/autoRefresh.js +7 -7
  132. codex_autorunner/static/chatUploads.js +137 -0
  133. codex_autorunner/static/dashboard.js +224 -171
  134. codex_autorunner/static/docChatCore.js +185 -13
  135. codex_autorunner/static/fileChat.js +68 -40
  136. codex_autorunner/static/fileboxUi.js +159 -0
  137. codex_autorunner/static/hub.js +114 -131
  138. codex_autorunner/static/index.html +375 -49
  139. codex_autorunner/static/messages.js +568 -87
  140. codex_autorunner/static/notifications.js +255 -0
  141. codex_autorunner/static/pma.js +1167 -0
  142. codex_autorunner/static/preserve.js +17 -0
  143. codex_autorunner/static/settings.js +128 -6
  144. codex_autorunner/static/smartRefresh.js +52 -0
  145. codex_autorunner/static/streamUtils.js +57 -0
  146. codex_autorunner/static/styles.css +9798 -6143
  147. codex_autorunner/static/tabs.js +152 -11
  148. codex_autorunner/static/templateReposSettings.js +225 -0
  149. codex_autorunner/static/terminal.js +18 -0
  150. codex_autorunner/static/ticketChatActions.js +165 -3
  151. codex_autorunner/static/ticketChatStream.js +17 -119
  152. codex_autorunner/static/ticketEditor.js +137 -15
  153. codex_autorunner/static/ticketTemplates.js +798 -0
  154. codex_autorunner/static/tickets.js +821 -98
  155. codex_autorunner/static/turnEvents.js +27 -0
  156. codex_autorunner/static/turnResume.js +33 -0
  157. codex_autorunner/static/utils.js +39 -0
  158. codex_autorunner/static/workspace.js +389 -82
  159. codex_autorunner/static/workspaceFileBrowser.js +15 -13
  160. codex_autorunner/surfaces/__init__.py +5 -0
  161. codex_autorunner/surfaces/cli/__init__.py +6 -0
  162. codex_autorunner/surfaces/cli/cli.py +2534 -0
  163. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  164. codex_autorunner/surfaces/cli/pma_cli.py +817 -0
  165. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  166. codex_autorunner/surfaces/web/__init__.py +1 -0
  167. codex_autorunner/surfaces/web/app.py +2223 -0
  168. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  169. codex_autorunner/surfaces/web/middleware.py +587 -0
  170. codex_autorunner/surfaces/web/pty_session.py +370 -0
  171. codex_autorunner/surfaces/web/review.py +6 -0
  172. codex_autorunner/surfaces/web/routes/__init__.py +82 -0
  173. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  174. codex_autorunner/surfaces/web/routes/analytics.py +284 -0
  175. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  176. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  177. codex_autorunner/surfaces/web/routes/base.py +615 -0
  178. codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
  179. codex_autorunner/surfaces/web/routes/filebox.py +227 -0
  180. codex_autorunner/surfaces/web/routes/flows.py +1354 -0
  181. codex_autorunner/surfaces/web/routes/messages.py +490 -0
  182. codex_autorunner/surfaces/web/routes/pma.py +1652 -0
  183. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  184. codex_autorunner/surfaces/web/routes/review.py +148 -0
  185. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  186. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  187. codex_autorunner/surfaces/web/routes/shared.py +277 -0
  188. codex_autorunner/surfaces/web/routes/system.py +196 -0
  189. codex_autorunner/surfaces/web/routes/templates.py +634 -0
  190. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  191. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  192. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  193. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  194. codex_autorunner/surfaces/web/schemas.py +469 -0
  195. codex_autorunner/surfaces/web/static_assets.py +490 -0
  196. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  197. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  198. codex_autorunner/tickets/__init__.py +8 -1
  199. codex_autorunner/tickets/agent_pool.py +53 -4
  200. codex_autorunner/tickets/files.py +37 -16
  201. codex_autorunner/tickets/lint.py +50 -0
  202. codex_autorunner/tickets/models.py +6 -1
  203. codex_autorunner/tickets/outbox.py +50 -2
  204. codex_autorunner/tickets/runner.py +396 -57
  205. codex_autorunner/web/__init__.py +5 -1
  206. codex_autorunner/web/app.py +2 -1949
  207. codex_autorunner/web/hub_jobs.py +2 -191
  208. codex_autorunner/web/middleware.py +2 -586
  209. codex_autorunner/web/pty_session.py +2 -369
  210. codex_autorunner/web/runner_manager.py +2 -24
  211. codex_autorunner/web/schemas.py +2 -376
  212. codex_autorunner/web/static_assets.py +4 -441
  213. codex_autorunner/web/static_refresh.py +2 -85
  214. codex_autorunner/web/terminal_sessions.py +2 -77
  215. codex_autorunner/workspace/paths.py +49 -33
  216. codex_autorunner-1.2.0.dist-info/METADATA +150 -0
  217. codex_autorunner-1.2.0.dist-info/RECORD +339 -0
  218. codex_autorunner/core/adapter_utils.py +0 -21
  219. codex_autorunner/core/engine.py +0 -2653
  220. codex_autorunner/core/static_assets.py +0 -55
  221. codex_autorunner-1.0.0.dist-info/METADATA +0 -246
  222. codex_autorunner-1.0.0.dist-info/RECORD +0 -251
  223. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  224. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
  225. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
  226. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
  227. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,312 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import json
5
+ from datetime import datetime, timezone
6
+ from typing import Any, Iterable, Optional
7
+
8
+ from ...core.config import RepoConfig, load_hub_config
9
+ from ...core.ports.backend_orchestrator import BackendOrchestrator
10
+ from ...core.ports.run_event import Completed, Failed, OutputDelta, RunEvent
11
+ from ...core.prompts import TEMPLATE_SCAN_PROMPT
12
+ from ...core.templates import FetchedTemplate, TemplateScanRecord, write_scan_record
13
+
14
+ _FORMAT_ERROR_HINT = (
15
+ "FORMAT ERROR: Output must be EXACTLY ONE LINE of JSON in the specified schema. "
16
+ "Do not include markdown, code fences, or extra text."
17
+ )
18
+
19
+ _APPROVE_TOOL = "template_scan_approve"
20
+ _REJECT_TOOL = "template_scan_reject"
21
+ _ALLOWED_TOOLS = {_APPROVE_TOOL, _REJECT_TOOL}
22
+ _APPROVE_SEVERITIES = {"low", "medium"}
23
+ _REJECT_SEVERITIES = {"high"}
24
+
25
+
26
+ class TemplateScanError(RuntimeError):
27
+ pass
28
+
29
+
30
+ class TemplateScanFormatError(TemplateScanError):
31
+ pass
32
+
33
+
34
+ class TemplateScanRejectedError(TemplateScanError):
35
+ def __init__(self, record: TemplateScanRecord, message: str) -> None:
36
+ super().__init__(message)
37
+ self.record = record
38
+
39
+
40
+ class TemplateScanBackendError(TemplateScanError):
41
+ pass
42
+
43
+
44
+ @dataclasses.dataclass(frozen=True)
45
+ class TemplateScanDecision:
46
+ tool: str
47
+ blob_sha: str
48
+ severity: str
49
+ reason: str
50
+ evidence: Optional[list[str]]
51
+
52
+
53
+ def build_template_scan_prompt(template: FetchedTemplate) -> str:
54
+ prompt = TEMPLATE_SCAN_PROMPT
55
+ replacements = {
56
+ "repo_id": template.repo_id,
57
+ "repo_url": template.url,
58
+ "trusted_repo": str(template.trusted),
59
+ "path": template.path,
60
+ "ref": template.ref,
61
+ "commit_sha": template.commit_sha,
62
+ "blob_sha": template.blob_sha,
63
+ "template_content": template.content,
64
+ }
65
+ for key, value in replacements.items():
66
+ prompt = prompt.replace(f"{{{{{key}}}}}", str(value))
67
+ return prompt
68
+
69
+
70
+ def _extract_output_text(events: Iterable[RunEvent]) -> str:
71
+ deltas: list[str] = []
72
+ final_message: Optional[str] = None
73
+ for event in events:
74
+ if isinstance(event, Completed):
75
+ final_message = event.final_message
76
+ continue
77
+ if isinstance(event, OutputDelta) and event.delta_type in {
78
+ "assistant_message",
79
+ "assistant_stream",
80
+ }:
81
+ if event.content:
82
+ deltas.append(event.content)
83
+ if final_message:
84
+ return final_message
85
+ if deltas:
86
+ return "".join(deltas)
87
+ return ""
88
+
89
+
90
+ def _normalize_line(text: str) -> str:
91
+ line = text.strip()
92
+ if "\n" in line or "\r" in line:
93
+ raise TemplateScanFormatError("Output must be a single line")
94
+ return line
95
+
96
+
97
+ def _parse_json_line(text: str) -> dict[str, Any]:
98
+ try:
99
+ payload = json.loads(text)
100
+ except json.JSONDecodeError as exc:
101
+ raise TemplateScanFormatError(f"Invalid JSON: {exc}") from exc
102
+ if not isinstance(payload, dict):
103
+ raise TemplateScanFormatError("Output JSON must be an object")
104
+ return payload
105
+
106
+
107
+ def _coerce_evidence(value: Any) -> Optional[list[str]]:
108
+ if value is None or value == []:
109
+ return None
110
+ if isinstance(value, list):
111
+ evidence = [str(item) for item in value][:3]
112
+ return evidence or None
113
+ return [str(value)]
114
+
115
+
116
+ def parse_template_scan_output(
117
+ raw: str, *, expected_blob_sha: str
118
+ ) -> TemplateScanDecision:
119
+ line = _normalize_line(raw)
120
+ if not line:
121
+ raise TemplateScanFormatError("Empty output from scan agent")
122
+
123
+ payload = _parse_json_line(line)
124
+ tool = payload.get("tool")
125
+ blob_sha = payload.get("blob_sha")
126
+ severity = payload.get("severity")
127
+ reason = payload.get("reason")
128
+ evidence = _coerce_evidence(payload.get("evidence"))
129
+
130
+ if tool not in _ALLOWED_TOOLS:
131
+ raise TemplateScanFormatError("Missing or invalid tool field")
132
+ if not isinstance(blob_sha, str) or not blob_sha:
133
+ raise TemplateScanFormatError("Missing blob_sha field")
134
+ if blob_sha != expected_blob_sha:
135
+ raise TemplateScanFormatError("blob_sha mismatch")
136
+ if not isinstance(severity, str) or not severity:
137
+ raise TemplateScanFormatError("Missing severity field")
138
+ if not isinstance(reason, str) or not reason:
139
+ raise TemplateScanFormatError("Missing reason field")
140
+
141
+ if tool == _APPROVE_TOOL and severity not in _APPROVE_SEVERITIES:
142
+ raise TemplateScanFormatError("Invalid severity for approve decision")
143
+ if tool == _REJECT_TOOL and severity not in _REJECT_SEVERITIES:
144
+ raise TemplateScanFormatError("Invalid severity for reject decision")
145
+
146
+ return TemplateScanDecision(
147
+ tool=tool,
148
+ blob_sha=blob_sha,
149
+ severity=severity,
150
+ reason=reason,
151
+ evidence=evidence,
152
+ )
153
+
154
+
155
+ def _decision_to_record(
156
+ template: FetchedTemplate,
157
+ decision: TemplateScanDecision,
158
+ *,
159
+ scanner: Optional[dict[str, str]] = None,
160
+ ) -> TemplateScanRecord:
161
+ scanned_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
162
+ return TemplateScanRecord(
163
+ blob_sha=template.blob_sha,
164
+ repo_id=template.repo_id,
165
+ path=template.path,
166
+ ref=template.ref,
167
+ commit_sha=template.commit_sha,
168
+ trusted=template.trusted,
169
+ decision="approve" if decision.tool == _APPROVE_TOOL else "reject",
170
+ severity=decision.severity,
171
+ reason=decision.reason,
172
+ evidence=decision.evidence,
173
+ scanned_at=scanned_at,
174
+ scanner=scanner,
175
+ )
176
+
177
+
178
+ def _scanner_metadata(config: Optional[RepoConfig]) -> Optional[dict[str, str]]:
179
+ if config is None:
180
+ return None
181
+ model = config.codex_model or ""
182
+ reasoning = config.codex_reasoning or ""
183
+ metadata: dict[str, str] = {"agent": "codex"}
184
+ if model:
185
+ metadata["model"] = model
186
+ if reasoning:
187
+ metadata["reasoning"] = reasoning
188
+ return metadata or None
189
+
190
+
191
+ def format_template_scan_rejection(record: TemplateScanRecord) -> str:
192
+ lines = [
193
+ "Template scan rejected this template.",
194
+ f"repo_id={record.repo_id}",
195
+ f"path={record.path}",
196
+ f"ref={record.ref}",
197
+ f"commit_sha={record.commit_sha}",
198
+ f"blob_sha={record.blob_sha}",
199
+ f"reason={record.reason}",
200
+ "Pause and notify the user; do not continue with this template.",
201
+ ]
202
+ if record.evidence:
203
+ lines.append("evidence:")
204
+ lines.extend([f"- {item}" for item in record.evidence])
205
+ return "\n".join(lines)
206
+
207
+
208
+ async def run_template_scan_with_orchestrator(
209
+ *,
210
+ config: RepoConfig,
211
+ backend_orchestrator: BackendOrchestrator,
212
+ template: FetchedTemplate,
213
+ hub_root: Any,
214
+ max_attempts: int = 2,
215
+ ) -> TemplateScanRecord:
216
+ if max_attempts < 1:
217
+ raise ValueError("max_attempts must be >= 1")
218
+
219
+ prompt = build_template_scan_prompt(template)
220
+ state = _build_scan_state()
221
+ scanner = _scanner_metadata(config)
222
+ last_error: Optional[str] = None
223
+
224
+ for attempt in range(max_attempts):
225
+ if attempt > 0:
226
+ prompt = f"{_FORMAT_ERROR_HINT}\n\n{prompt}"
227
+ events: list[RunEvent] = []
228
+ try:
229
+ async for event in backend_orchestrator.run_turn(
230
+ agent_id="codex",
231
+ state=state,
232
+ prompt=prompt,
233
+ model=config.codex_model,
234
+ reasoning=config.codex_reasoning,
235
+ session_key=f"template_scan:{template.blob_sha}:{attempt}",
236
+ ):
237
+ events.append(event)
238
+ except Exception as exc:
239
+ raise TemplateScanBackendError(str(exc)) from exc
240
+
241
+ for event in events:
242
+ if isinstance(event, Failed):
243
+ raise TemplateScanBackendError(event.error_message)
244
+
245
+ output = _extract_output_text(events)
246
+ if not output:
247
+ last_error = "Empty scan output"
248
+ continue
249
+ try:
250
+ decision = parse_template_scan_output(
251
+ output, expected_blob_sha=template.blob_sha
252
+ )
253
+ except TemplateScanFormatError as exc:
254
+ last_error = str(exc)
255
+ continue
256
+
257
+ record = _decision_to_record(template, decision, scanner=scanner)
258
+ write_scan_record(record, hub_root)
259
+ if record.decision == "reject":
260
+ raise TemplateScanRejectedError(
261
+ record, format_template_scan_rejection(record)
262
+ )
263
+ return record
264
+
265
+ raise TemplateScanFormatError(last_error or "Scan output failed validation")
266
+
267
+
268
+ async def run_template_scan(
269
+ *,
270
+ ctx: Any,
271
+ template: FetchedTemplate,
272
+ max_attempts: int = 2,
273
+ ) -> TemplateScanRecord:
274
+ backend_orchestrator = getattr(ctx, "_backend_orchestrator", None)
275
+ if backend_orchestrator is None:
276
+ raise TemplateScanBackendError(
277
+ "Template scanning requires a backend orchestrator; configure Codex or OpenCode."
278
+ )
279
+
280
+ config = ctx.config
281
+ try:
282
+ hub_root = load_hub_config(config.root).root
283
+ except Exception:
284
+ hub_root = getattr(config, "root", None)
285
+ if hub_root is None:
286
+ raise TemplateScanBackendError("Missing hub root for scan cache writes")
287
+
288
+ return await run_template_scan_with_orchestrator(
289
+ config=config,
290
+ backend_orchestrator=backend_orchestrator,
291
+ template=template,
292
+ hub_root=hub_root,
293
+ max_attempts=max_attempts,
294
+ )
295
+
296
+
297
+ def _build_scan_state() -> Any:
298
+ from ...core.state import RunnerState
299
+
300
+ return RunnerState(
301
+ last_run_id=None,
302
+ status="running",
303
+ last_exit_code=None,
304
+ last_run_started_at=None,
305
+ last_run_finished_at=None,
306
+ autorunner_agent_override="codex",
307
+ autorunner_model_override=None,
308
+ autorunner_effort_override=None,
309
+ autorunner_approval_policy="never",
310
+ autorunner_sandbox_mode="readOnly",
311
+ autorunner_workspace_write_network=False,
312
+ )
@@ -1,76 +1,37 @@
1
- """
2
- Modular API routes for the codex-autorunner server.
3
-
4
- This package splits monolithic api_routes.py into focused modules:
5
- - base: Index, WebSocket terminal, and general endpoints
6
- - agents: Agent harness models and event streaming
7
- - app_server: App-server thread registry endpoints
8
- - workspace: Optional workspace docs (active_context/decisions/spec)
9
- - flows: Flow runtime management (start/stop/resume/status/events/artifacts)
10
- - messages: Inbox/message wrappers over ticket_flow dispatch + reply histories
11
- - repos: Run control (start/stop/resume/reset)
12
- - sessions: Terminal session registry endpoints
13
- - settings: Session settings for autorunner overrides
14
- - file_chat: Unified file chat (tickets + workspace docs)
15
- - voice: Voice transcription and config
16
- - terminal_images: Terminal image uploads
17
- """
18
-
19
- from pathlib import Path
20
-
21
- from fastapi import APIRouter
22
-
23
- from .agents import build_agents_routes
24
- from .analytics import build_analytics_routes
25
- from .app_server import build_app_server_routes
26
- from .base import build_base_routes, build_frontend_routes
27
- from .file_chat import build_file_chat_routes
28
- from .flows import build_flow_routes
29
- from .messages import build_messages_routes
30
- from .repos import build_repos_routes
31
- from .review import build_review_routes
32
- from .sessions import build_sessions_routes
33
- from .settings import build_settings_routes
34
- from .system import build_system_routes
35
- from .terminal_images import build_terminal_image_routes
36
- from .usage import build_usage_routes
37
- from .voice import build_voice_routes
38
- from .workspace import build_workspace_routes
39
-
40
-
41
- def build_repo_router(static_dir: Path) -> APIRouter:
42
- """
43
- Build complete API router by combining all route modules.
44
-
45
- Args:
46
- static_dir: Path to static assets directory
47
-
48
- Returns:
49
- Combined APIRouter with all endpoints
50
- """
51
- router = APIRouter()
52
-
53
- # Include all route modules
54
- router.include_router(build_base_routes(static_dir))
55
- router.include_router(build_analytics_routes())
56
- router.include_router(build_agents_routes())
57
- router.include_router(build_app_server_routes())
58
- router.include_router(build_workspace_routes())
59
- router.include_router(build_flow_routes())
60
- router.include_router(build_file_chat_routes())
61
- router.include_router(build_messages_routes())
62
- router.include_router(build_repos_routes())
63
- router.include_router(build_review_routes())
64
- router.include_router(build_sessions_routes())
65
- router.include_router(build_settings_routes())
66
- router.include_router(build_system_routes())
67
- router.include_router(build_terminal_image_routes())
68
- router.include_router(build_usage_routes())
69
- router.include_router(build_voice_routes())
70
- # Include frontend routes last to avoid shadowing API routes
71
- router.include_router(build_frontend_routes(static_dir))
72
-
73
- return router
74
-
75
-
76
- __all__ = ["build_repo_router"]
1
+ """Backward-compatible route exports."""
2
+
3
+ from . import ( # noqa: F401
4
+ analytics,
5
+ app_server,
6
+ base,
7
+ file_chat,
8
+ flows,
9
+ messages,
10
+ repos,
11
+ review,
12
+ sessions,
13
+ settings,
14
+ shared,
15
+ system,
16
+ usage,
17
+ voice,
18
+ workspace,
19
+ )
20
+
21
+ __all__ = [
22
+ "analytics",
23
+ "app_server",
24
+ "base",
25
+ "file_chat",
26
+ "flows",
27
+ "messages",
28
+ "repos",
29
+ "review",
30
+ "sessions",
31
+ "settings",
32
+ "shared",
33
+ "system",
34
+ "usage",
35
+ "voice",
36
+ "workspace",
37
+ ]
@@ -1,138 +1,3 @@
1
- """
2
- Agent harness support routes (models + event streaming).
3
- """
1
+ """Backward-compatible agent routes."""
4
2
 
5
- from __future__ import annotations
6
-
7
- from typing import Any, Optional
8
-
9
- from fastapi import APIRouter, HTTPException, Request
10
- from fastapi.responses import StreamingResponse
11
-
12
- from ..agents.codex.harness import CodexHarness
13
- from ..agents.opencode.harness import OpenCodeHarness
14
- from ..agents.opencode.supervisor import OpenCodeSupervisorError
15
- from ..agents.types import ModelCatalog
16
- from .shared import SSE_HEADERS
17
-
18
-
19
- def _available_agents(request: Request) -> tuple[list[dict[str, str]], str]:
20
- agents: list[dict[str, str]] = []
21
- default_agent: Optional[str] = None
22
-
23
- if getattr(request.app.state, "app_server_supervisor", None) is not None:
24
- agents.append({"id": "codex", "name": "Codex", "protocol_version": "2.0"})
25
- default_agent = "codex"
26
-
27
- if getattr(request.app.state, "opencode_supervisor", None) is not None:
28
- supervisor = getattr(request.app.state, "opencode_supervisor", None)
29
- version = None
30
- if supervisor and hasattr(supervisor, "_handles"):
31
- handles = supervisor._handles
32
- if handles:
33
- first_handle = next(iter(handles.values()), None)
34
- if first_handle:
35
- version = getattr(first_handle, "version", None)
36
- agent_data = {"id": "opencode", "name": "OpenCode"}
37
- if version:
38
- agent_data["version"] = str(version)
39
- agents.append(agent_data)
40
- if default_agent is None:
41
- default_agent = "opencode"
42
-
43
- if not agents:
44
- agents = [{"id": "codex", "name": "Codex", "protocol_version": "2.0"}]
45
- default_agent = "codex"
46
-
47
- return agents, default_agent or "codex"
48
-
49
-
50
- def _serialize_model_catalog(catalog: ModelCatalog) -> dict[str, Any]:
51
- return {
52
- "default_model": catalog.default_model,
53
- "models": [
54
- {
55
- "id": model.id,
56
- "display_name": model.display_name,
57
- "supports_reasoning": model.supports_reasoning,
58
- "reasoning_options": list(model.reasoning_options),
59
- }
60
- for model in catalog.models
61
- ],
62
- }
63
-
64
-
65
- def build_agents_routes() -> APIRouter:
66
- router = APIRouter()
67
-
68
- @router.get("/api/agents")
69
- def list_agents(request: Request) -> dict[str, Any]:
70
- agents, default_agent = _available_agents(request)
71
- return {"agents": agents, "default": default_agent}
72
-
73
- @router.get("/api/agents/{agent}/models")
74
- async def list_agent_models(agent: str, request: Request):
75
- agent_id = (agent or "").strip().lower()
76
- engine = request.app.state.engine
77
- if agent_id == "codex":
78
- supervisor = request.app.state.app_server_supervisor
79
- events = request.app.state.app_server_events
80
- if supervisor is None:
81
- raise HTTPException(status_code=404, detail="Codex harness unavailable")
82
- codex_harness = CodexHarness(supervisor, events)
83
- catalog = await codex_harness.model_catalog(engine.repo_root)
84
- return _serialize_model_catalog(catalog)
85
- if agent_id == "opencode":
86
- supervisor = getattr(request.app.state, "opencode_supervisor", None)
87
- if supervisor is None:
88
- raise HTTPException(
89
- status_code=404, detail="OpenCode harness unavailable"
90
- )
91
- try:
92
- opencode_harness = OpenCodeHarness(supervisor)
93
- catalog = await opencode_harness.model_catalog(engine.repo_root)
94
- return _serialize_model_catalog(catalog)
95
- except OpenCodeSupervisorError as exc:
96
- raise HTTPException(status_code=502, detail=str(exc)) from exc
97
- except Exception as exc:
98
- raise HTTPException(status_code=502, detail=str(exc)) from exc
99
- raise HTTPException(status_code=404, detail="Unknown agent")
100
-
101
- @router.get("/api/agents/{agent}/turns/{turn_id}/events")
102
- async def stream_agent_turn_events(
103
- agent: str, turn_id: str, request: Request, thread_id: Optional[str] = None
104
- ):
105
- agent_id = (agent or "").strip().lower()
106
- if agent_id == "codex":
107
- events = getattr(request.app.state, "app_server_events", None)
108
- if events is None:
109
- raise HTTPException(status_code=404, detail="Codex events unavailable")
110
- if not thread_id:
111
- raise HTTPException(status_code=400, detail="thread_id is required")
112
- return StreamingResponse(
113
- events.stream(thread_id, turn_id),
114
- media_type="text/event-stream",
115
- headers=SSE_HEADERS,
116
- )
117
- if agent_id == "opencode":
118
- if not thread_id:
119
- raise HTTPException(status_code=400, detail="thread_id is required")
120
- supervisor = getattr(request.app.state, "opencode_supervisor", None)
121
- if supervisor is None:
122
- raise HTTPException(
123
- status_code=404, detail="OpenCode events unavailable"
124
- )
125
- harness = OpenCodeHarness(supervisor)
126
- return StreamingResponse(
127
- harness.stream_events(
128
- request.app.state.engine.repo_root, thread_id, turn_id
129
- ),
130
- media_type="text/event-stream",
131
- headers=SSE_HEADERS,
132
- )
133
- raise HTTPException(status_code=404, detail="Unknown agent")
134
-
135
- return router
136
-
137
-
138
- __all__ = ["build_agents_routes"]
3
+ from ..surfaces.web.routes.agents import * # noqa: F401,F403