codex-autorunner 0.1.2__py3-none-any.whl → 1.1.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 (276) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/__main__.py +4 -0
  3. codex_autorunner/agents/codex/harness.py +1 -1
  4. codex_autorunner/agents/opencode/client.py +68 -35
  5. codex_autorunner/agents/opencode/constants.py +3 -0
  6. codex_autorunner/agents/opencode/harness.py +6 -1
  7. codex_autorunner/agents/opencode/logging.py +21 -5
  8. codex_autorunner/agents/opencode/run_prompt.py +1 -0
  9. codex_autorunner/agents/opencode/runtime.py +176 -47
  10. codex_autorunner/agents/opencode/supervisor.py +36 -48
  11. codex_autorunner/agents/registry.py +155 -8
  12. codex_autorunner/api.py +25 -0
  13. codex_autorunner/bootstrap.py +22 -37
  14. codex_autorunner/cli.py +5 -1156
  15. codex_autorunner/codex_cli.py +20 -84
  16. codex_autorunner/core/__init__.py +4 -0
  17. codex_autorunner/core/about_car.py +49 -32
  18. codex_autorunner/core/adapter_utils.py +21 -0
  19. codex_autorunner/core/app_server_ids.py +59 -0
  20. codex_autorunner/core/app_server_logging.py +7 -3
  21. codex_autorunner/core/app_server_prompts.py +27 -260
  22. codex_autorunner/core/app_server_threads.py +26 -28
  23. codex_autorunner/core/app_server_utils.py +165 -0
  24. codex_autorunner/core/archive.py +349 -0
  25. codex_autorunner/core/codex_runner.py +12 -2
  26. codex_autorunner/core/config.py +587 -103
  27. codex_autorunner/core/docs.py +10 -2
  28. codex_autorunner/core/drafts.py +136 -0
  29. codex_autorunner/core/engine.py +1531 -866
  30. codex_autorunner/core/exceptions.py +4 -0
  31. codex_autorunner/core/flows/__init__.py +25 -0
  32. codex_autorunner/core/flows/controller.py +202 -0
  33. codex_autorunner/core/flows/definition.py +82 -0
  34. codex_autorunner/core/flows/models.py +88 -0
  35. codex_autorunner/core/flows/reasons.py +52 -0
  36. codex_autorunner/core/flows/reconciler.py +131 -0
  37. codex_autorunner/core/flows/runtime.py +382 -0
  38. codex_autorunner/core/flows/store.py +568 -0
  39. codex_autorunner/core/flows/transition.py +138 -0
  40. codex_autorunner/core/flows/ux_helpers.py +257 -0
  41. codex_autorunner/core/flows/worker_process.py +242 -0
  42. codex_autorunner/core/git_utils.py +62 -0
  43. codex_autorunner/core/hub.py +136 -16
  44. codex_autorunner/core/locks.py +4 -0
  45. codex_autorunner/core/notifications.py +14 -2
  46. codex_autorunner/core/ports/__init__.py +28 -0
  47. codex_autorunner/core/ports/agent_backend.py +150 -0
  48. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  49. codex_autorunner/core/ports/run_event.py +91 -0
  50. codex_autorunner/core/prompt.py +15 -7
  51. codex_autorunner/core/redaction.py +29 -0
  52. codex_autorunner/core/review_context.py +5 -8
  53. codex_autorunner/core/run_index.py +6 -0
  54. codex_autorunner/core/runner_process.py +5 -2
  55. codex_autorunner/core/state.py +0 -88
  56. codex_autorunner/core/state_roots.py +57 -0
  57. codex_autorunner/core/supervisor_protocol.py +15 -0
  58. codex_autorunner/core/supervisor_utils.py +67 -0
  59. codex_autorunner/core/text_delta_coalescer.py +54 -0
  60. codex_autorunner/core/ticket_linter_cli.py +201 -0
  61. codex_autorunner/core/ticket_manager_cli.py +432 -0
  62. codex_autorunner/core/update.py +24 -16
  63. codex_autorunner/core/update_paths.py +28 -0
  64. codex_autorunner/core/update_runner.py +2 -0
  65. codex_autorunner/core/usage.py +164 -12
  66. codex_autorunner/core/utils.py +120 -11
  67. codex_autorunner/discovery.py +2 -4
  68. codex_autorunner/flows/review/__init__.py +17 -0
  69. codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
  70. codex_autorunner/flows/ticket_flow/__init__.py +3 -0
  71. codex_autorunner/flows/ticket_flow/definition.py +98 -0
  72. codex_autorunner/integrations/agents/__init__.py +17 -0
  73. codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
  74. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  75. codex_autorunner/integrations/agents/codex_backend.py +448 -0
  76. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  77. codex_autorunner/integrations/agents/opencode_backend.py +598 -0
  78. codex_autorunner/integrations/agents/runner.py +91 -0
  79. codex_autorunner/integrations/agents/wiring.py +271 -0
  80. codex_autorunner/integrations/app_server/client.py +583 -152
  81. codex_autorunner/integrations/app_server/env.py +2 -107
  82. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  83. codex_autorunner/integrations/app_server/supervisor.py +59 -33
  84. codex_autorunner/integrations/telegram/adapter.py +204 -165
  85. codex_autorunner/integrations/telegram/api_schemas.py +120 -0
  86. codex_autorunner/integrations/telegram/config.py +221 -0
  87. codex_autorunner/integrations/telegram/constants.py +17 -2
  88. codex_autorunner/integrations/telegram/dispatch.py +17 -0
  89. codex_autorunner/integrations/telegram/doctor.py +47 -0
  90. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -4
  91. codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
  92. codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
  93. codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
  94. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1364 -0
  95. codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
  96. codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
  97. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
  98. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +137 -478
  99. codex_autorunner/integrations/telegram/handlers/commands_spec.py +17 -4
  100. codex_autorunner/integrations/telegram/handlers/messages.py +121 -9
  101. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  102. codex_autorunner/integrations/telegram/helpers.py +111 -16
  103. codex_autorunner/integrations/telegram/outbox.py +208 -37
  104. codex_autorunner/integrations/telegram/progress_stream.py +3 -10
  105. codex_autorunner/integrations/telegram/service.py +221 -42
  106. codex_autorunner/integrations/telegram/state.py +100 -2
  107. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +611 -0
  108. codex_autorunner/integrations/telegram/transport.py +39 -4
  109. codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
  110. codex_autorunner/manifest.py +2 -0
  111. codex_autorunner/plugin_api.py +22 -0
  112. codex_autorunner/routes/__init__.py +37 -67
  113. codex_autorunner/routes/agents.py +2 -137
  114. codex_autorunner/routes/analytics.py +3 -0
  115. codex_autorunner/routes/app_server.py +2 -131
  116. codex_autorunner/routes/base.py +2 -624
  117. codex_autorunner/routes/file_chat.py +7 -0
  118. codex_autorunner/routes/flows.py +7 -0
  119. codex_autorunner/routes/messages.py +7 -0
  120. codex_autorunner/routes/repos.py +2 -196
  121. codex_autorunner/routes/review.py +2 -147
  122. codex_autorunner/routes/sessions.py +2 -175
  123. codex_autorunner/routes/settings.py +2 -168
  124. codex_autorunner/routes/shared.py +2 -275
  125. codex_autorunner/routes/system.py +4 -188
  126. codex_autorunner/routes/usage.py +3 -0
  127. codex_autorunner/routes/voice.py +2 -119
  128. codex_autorunner/routes/workspace.py +3 -0
  129. codex_autorunner/server.py +3 -2
  130. codex_autorunner/static/agentControls.js +41 -11
  131. codex_autorunner/static/agentEvents.js +248 -0
  132. codex_autorunner/static/app.js +35 -24
  133. codex_autorunner/static/archive.js +826 -0
  134. codex_autorunner/static/archiveApi.js +37 -0
  135. codex_autorunner/static/autoRefresh.js +36 -8
  136. codex_autorunner/static/bootstrap.js +1 -0
  137. codex_autorunner/static/bus.js +1 -0
  138. codex_autorunner/static/cache.js +1 -0
  139. codex_autorunner/static/constants.js +20 -4
  140. codex_autorunner/static/dashboard.js +344 -325
  141. codex_autorunner/static/diffRenderer.js +37 -0
  142. codex_autorunner/static/docChatCore.js +324 -0
  143. codex_autorunner/static/docChatStorage.js +65 -0
  144. codex_autorunner/static/docChatVoice.js +65 -0
  145. codex_autorunner/static/docEditor.js +133 -0
  146. codex_autorunner/static/env.js +1 -0
  147. codex_autorunner/static/eventSummarizer.js +166 -0
  148. codex_autorunner/static/fileChat.js +182 -0
  149. codex_autorunner/static/health.js +155 -0
  150. codex_autorunner/static/hub.js +126 -185
  151. codex_autorunner/static/index.html +839 -863
  152. codex_autorunner/static/liveUpdates.js +1 -0
  153. codex_autorunner/static/loader.js +1 -0
  154. codex_autorunner/static/messages.js +873 -0
  155. codex_autorunner/static/mobileCompact.js +2 -1
  156. codex_autorunner/static/preserve.js +17 -0
  157. codex_autorunner/static/settings.js +149 -217
  158. codex_autorunner/static/smartRefresh.js +52 -0
  159. codex_autorunner/static/styles.css +8850 -3876
  160. codex_autorunner/static/tabs.js +175 -11
  161. codex_autorunner/static/terminal.js +32 -0
  162. codex_autorunner/static/terminalManager.js +34 -59
  163. codex_autorunner/static/ticketChatActions.js +333 -0
  164. codex_autorunner/static/ticketChatEvents.js +16 -0
  165. codex_autorunner/static/ticketChatStorage.js +16 -0
  166. codex_autorunner/static/ticketChatStream.js +264 -0
  167. codex_autorunner/static/ticketEditor.js +844 -0
  168. codex_autorunner/static/ticketVoice.js +9 -0
  169. codex_autorunner/static/tickets.js +1988 -0
  170. codex_autorunner/static/utils.js +43 -3
  171. codex_autorunner/static/voice.js +1 -0
  172. codex_autorunner/static/workspace.js +765 -0
  173. codex_autorunner/static/workspaceApi.js +53 -0
  174. codex_autorunner/static/workspaceFileBrowser.js +504 -0
  175. codex_autorunner/surfaces/__init__.py +5 -0
  176. codex_autorunner/surfaces/cli/__init__.py +6 -0
  177. codex_autorunner/surfaces/cli/cli.py +1224 -0
  178. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  179. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  180. codex_autorunner/surfaces/web/__init__.py +1 -0
  181. codex_autorunner/surfaces/web/app.py +2019 -0
  182. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  183. codex_autorunner/surfaces/web/middleware.py +587 -0
  184. codex_autorunner/surfaces/web/pty_session.py +370 -0
  185. codex_autorunner/surfaces/web/review.py +6 -0
  186. codex_autorunner/surfaces/web/routes/__init__.py +78 -0
  187. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  188. codex_autorunner/surfaces/web/routes/analytics.py +277 -0
  189. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  190. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  191. codex_autorunner/surfaces/web/routes/base.py +615 -0
  192. codex_autorunner/surfaces/web/routes/file_chat.py +836 -0
  193. codex_autorunner/surfaces/web/routes/flows.py +1164 -0
  194. codex_autorunner/surfaces/web/routes/messages.py +459 -0
  195. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  196. codex_autorunner/surfaces/web/routes/review.py +148 -0
  197. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  198. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  199. codex_autorunner/surfaces/web/routes/shared.py +280 -0
  200. codex_autorunner/surfaces/web/routes/system.py +196 -0
  201. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  202. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  203. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  204. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  205. codex_autorunner/surfaces/web/schemas.py +417 -0
  206. codex_autorunner/surfaces/web/static_assets.py +490 -0
  207. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  208. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  209. codex_autorunner/tickets/__init__.py +27 -0
  210. codex_autorunner/tickets/agent_pool.py +399 -0
  211. codex_autorunner/tickets/files.py +89 -0
  212. codex_autorunner/tickets/frontmatter.py +55 -0
  213. codex_autorunner/tickets/lint.py +102 -0
  214. codex_autorunner/tickets/models.py +97 -0
  215. codex_autorunner/tickets/outbox.py +244 -0
  216. codex_autorunner/tickets/replies.py +179 -0
  217. codex_autorunner/tickets/runner.py +881 -0
  218. codex_autorunner/tickets/spec_ingest.py +77 -0
  219. codex_autorunner/web/__init__.py +5 -1
  220. codex_autorunner/web/app.py +2 -1771
  221. codex_autorunner/web/hub_jobs.py +2 -191
  222. codex_autorunner/web/middleware.py +2 -587
  223. codex_autorunner/web/pty_session.py +2 -369
  224. codex_autorunner/web/runner_manager.py +2 -24
  225. codex_autorunner/web/schemas.py +2 -396
  226. codex_autorunner/web/static_assets.py +4 -484
  227. codex_autorunner/web/static_refresh.py +2 -85
  228. codex_autorunner/web/terminal_sessions.py +2 -77
  229. codex_autorunner/workspace/__init__.py +40 -0
  230. codex_autorunner/workspace/paths.py +335 -0
  231. codex_autorunner-1.1.0.dist-info/METADATA +154 -0
  232. codex_autorunner-1.1.0.dist-info/RECORD +308 -0
  233. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/WHEEL +1 -1
  234. codex_autorunner/agents/execution/policy.py +0 -292
  235. codex_autorunner/agents/factory.py +0 -52
  236. codex_autorunner/agents/orchestrator.py +0 -358
  237. codex_autorunner/core/doc_chat.py +0 -1446
  238. codex_autorunner/core/snapshot.py +0 -580
  239. codex_autorunner/integrations/github/chatops.py +0 -268
  240. codex_autorunner/integrations/github/pr_flow.py +0 -1314
  241. codex_autorunner/routes/docs.py +0 -381
  242. codex_autorunner/routes/github.py +0 -327
  243. codex_autorunner/routes/runs.py +0 -250
  244. codex_autorunner/spec_ingest.py +0 -812
  245. codex_autorunner/static/docChatActions.js +0 -287
  246. codex_autorunner/static/docChatEvents.js +0 -300
  247. codex_autorunner/static/docChatRender.js +0 -205
  248. codex_autorunner/static/docChatStream.js +0 -361
  249. codex_autorunner/static/docs.js +0 -20
  250. codex_autorunner/static/docsClipboard.js +0 -69
  251. codex_autorunner/static/docsCrud.js +0 -257
  252. codex_autorunner/static/docsDocUpdates.js +0 -62
  253. codex_autorunner/static/docsDrafts.js +0 -16
  254. codex_autorunner/static/docsElements.js +0 -69
  255. codex_autorunner/static/docsInit.js +0 -285
  256. codex_autorunner/static/docsParse.js +0 -160
  257. codex_autorunner/static/docsSnapshot.js +0 -87
  258. codex_autorunner/static/docsSpecIngest.js +0 -263
  259. codex_autorunner/static/docsState.js +0 -127
  260. codex_autorunner/static/docsThreadRegistry.js +0 -44
  261. codex_autorunner/static/docsUi.js +0 -153
  262. codex_autorunner/static/docsVoice.js +0 -56
  263. codex_autorunner/static/github.js +0 -504
  264. codex_autorunner/static/logs.js +0 -678
  265. codex_autorunner/static/review.js +0 -157
  266. codex_autorunner/static/runs.js +0 -418
  267. codex_autorunner/static/snapshot.js +0 -124
  268. codex_autorunner/static/state.js +0 -94
  269. codex_autorunner/static/todoPreview.js +0 -27
  270. codex_autorunner/workspace.py +0 -16
  271. codex_autorunner-0.1.2.dist-info/METADATA +0 -249
  272. codex_autorunner-0.1.2.dist-info/RECORD +0 -222
  273. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  274. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
  275. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
  276. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,271 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import zipfile
5
+ from dataclasses import asdict
6
+
7
+ from fastapi import APIRouter, File, Form, HTTPException, Request, UploadFile
8
+ from fastapi.responses import FileResponse, PlainTextResponse, StreamingResponse
9
+
10
+ from ....core import drafts as draft_utils
11
+ from ....tickets.spec_ingest import (
12
+ SpecIngestTicketsError,
13
+ ingest_workspace_spec_to_tickets,
14
+ )
15
+ from ....workspace.paths import (
16
+ PINNED_DOC_FILENAMES,
17
+ WORKSPACE_DOC_KINDS,
18
+ list_workspace_files,
19
+ list_workspace_tree,
20
+ normalize_workspace_rel_path,
21
+ read_workspace_doc,
22
+ read_workspace_file,
23
+ sanitize_workspace_filename,
24
+ workspace_dir,
25
+ workspace_doc_path,
26
+ write_workspace_doc,
27
+ write_workspace_file,
28
+ )
29
+ from ..schemas import (
30
+ SpecIngestTicketsResponse,
31
+ WorkspaceFileListResponse,
32
+ WorkspaceResponse,
33
+ WorkspaceTreeResponse,
34
+ WorkspaceUploadResponse,
35
+ WorkspaceWriteRequest,
36
+ )
37
+
38
+
39
+ def build_workspace_routes() -> APIRouter:
40
+ router = APIRouter(prefix="/api", tags=["workspace"])
41
+
42
+ @router.get("/workspace", response_model=WorkspaceResponse)
43
+ def get_workspace(request: Request):
44
+ repo_root = request.app.state.engine.repo_root
45
+ return {
46
+ "active_context": read_workspace_doc(repo_root, "active_context"),
47
+ "decisions": read_workspace_doc(repo_root, "decisions"),
48
+ "spec": read_workspace_doc(repo_root, "spec"),
49
+ }
50
+
51
+ @router.get("/workspace/file", response_class=PlainTextResponse)
52
+ def read_workspace(request: Request, path: str):
53
+ repo_root = request.app.state.engine.repo_root
54
+ try:
55
+ content = read_workspace_file(repo_root, path)
56
+ except ValueError as exc: # invalid path
57
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
58
+ return PlainTextResponse(content)
59
+
60
+ @router.put("/workspace/file", response_class=PlainTextResponse)
61
+ def write_workspace(request: Request, payload: WorkspaceWriteRequest, path: str):
62
+ repo_root = request.app.state.engine.repo_root
63
+ try:
64
+ # Normalize path the same way workspace helpers do to avoid traversal
65
+ safe_path, rel_posix = normalize_workspace_rel_path(repo_root, path)
66
+ content = write_workspace_file(repo_root, path, payload.content)
67
+ try:
68
+ rel_repo_path = safe_path.relative_to(repo_root).as_posix()
69
+ draft_utils.invalidate_drafts_for_path(repo_root, rel_repo_path)
70
+ state_key = f"workspace_{rel_posix.replace('/', '_')}"
71
+ draft_utils.remove_draft(repo_root, state_key)
72
+ except Exception:
73
+ pass
74
+ except ValueError as exc:
75
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
76
+ return PlainTextResponse(content)
77
+
78
+ @router.put("/workspace/{kind}", response_model=WorkspaceResponse)
79
+ def put_workspace(kind: str, payload: WorkspaceWriteRequest, request: Request):
80
+ key = (kind or "").strip().lower()
81
+ if key not in WORKSPACE_DOC_KINDS:
82
+ raise HTTPException(status_code=400, detail="invalid workspace doc kind")
83
+ repo_root = request.app.state.engine.repo_root
84
+ write_workspace_doc(repo_root, key, payload.content)
85
+ try:
86
+ rel_path = workspace_doc_path(repo_root, key).relative_to(repo_root)
87
+ draft_utils.invalidate_drafts_for_path(repo_root, rel_path.as_posix())
88
+ state_key = f"workspace_{rel_path.name}"
89
+ draft_utils.remove_draft(repo_root, state_key)
90
+ except Exception:
91
+ # best-effort invalidation; avoid blocking writes
92
+ pass
93
+ return {
94
+ "active_context": read_workspace_doc(repo_root, "active_context"),
95
+ "decisions": read_workspace_doc(repo_root, "decisions"),
96
+ "spec": read_workspace_doc(repo_root, "spec"),
97
+ }
98
+
99
+ @router.get("/workspace/files", response_model=WorkspaceFileListResponse)
100
+ def list_files(request: Request):
101
+ repo_root = request.app.state.engine.repo_root
102
+ files = [asdict(item) for item in list_workspace_files(repo_root)]
103
+ return {"files": files}
104
+
105
+ @router.get("/workspace/tree", response_model=WorkspaceTreeResponse)
106
+ def get_workspace_tree(request: Request):
107
+ repo_root = request.app.state.engine.repo_root
108
+ tree = [asdict(item) for item in list_workspace_tree(repo_root)]
109
+ return {"tree": tree}
110
+
111
+ @router.post("/workspace/upload", response_model=WorkspaceUploadResponse)
112
+ async def upload_workspace_files(
113
+ request: Request,
114
+ files: list[UploadFile] = File(...), # noqa: B008
115
+ subdir: str = Form(""),
116
+ ):
117
+ if not files:
118
+ raise HTTPException(status_code=400, detail="no files provided")
119
+
120
+ repo_root = request.app.state.engine.repo_root
121
+ base = workspace_dir(repo_root)
122
+ target_dir = base
123
+ if subdir:
124
+ try:
125
+ target_dir, _ = normalize_workspace_rel_path(repo_root, subdir)
126
+ except ValueError as exc:
127
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
128
+
129
+ target_dir.mkdir(parents=True, exist_ok=True)
130
+
131
+ uploaded: list[dict[str, str | int]] = []
132
+ for upload in files:
133
+ filename = sanitize_workspace_filename(upload.filename or "")
134
+ try:
135
+ data = await upload.read()
136
+ except (
137
+ Exception
138
+ ) as exc: # pragma: no cover - handled by FastAPI for most cases
139
+ raise HTTPException(
140
+ status_code=400, detail="failed to read upload"
141
+ ) from exc
142
+
143
+ dest = target_dir / filename
144
+ dest.write_bytes(
145
+ data
146
+ ) # codeql[py/path-injection] dest sits under normalized workspace dir
147
+ rel_path = dest.relative_to(base).as_posix()
148
+ uploaded.append({"filename": filename, "path": rel_path, "size": len(data)})
149
+
150
+ return {"status": "ok", "uploaded": uploaded}
151
+
152
+ @router.get("/workspace/download")
153
+ async def download_workspace_file(request: Request, path: str):
154
+ repo_root = request.app.state.engine.repo_root
155
+ try:
156
+ safe_path, _ = normalize_workspace_rel_path(repo_root, path)
157
+ except ValueError as exc:
158
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
159
+
160
+ if not safe_path.exists() or safe_path.is_dir():
161
+ raise HTTPException(status_code=404, detail="file not found")
162
+
163
+ return FileResponse(
164
+ path=safe_path, filename=safe_path.name
165
+ ) # codeql[py/path-injection] safe_path validated by normalize_workspace_rel_path
166
+
167
+ @router.get("/workspace/download-zip")
168
+ async def download_workspace_zip(request: Request, path: str = ""):
169
+ repo_root = request.app.state.engine.repo_root
170
+ base = workspace_dir(repo_root)
171
+ base.mkdir(parents=True, exist_ok=True)
172
+
173
+ target_dir = base
174
+ zip_name = "workspace.zip"
175
+ if path:
176
+ try:
177
+ target_dir, _ = normalize_workspace_rel_path(repo_root, path)
178
+ except ValueError as exc:
179
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
180
+ if not target_dir.exists() or not target_dir.is_dir():
181
+ raise HTTPException(status_code=404, detail="folder not found")
182
+ zip_name = f"{target_dir.name}.zip"
183
+
184
+ buffer = io.BytesIO()
185
+ base_real = base.resolve()
186
+ with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zf:
187
+ for file_path in target_dir.rglob("*"):
188
+ if file_path.is_dir():
189
+ continue
190
+ if file_path.is_symlink():
191
+ try:
192
+ file_path.resolve().relative_to(base_real)
193
+ except Exception:
194
+ continue
195
+ arc_name = file_path.relative_to(target_dir).as_posix()
196
+ zf.write(
197
+ file_path, arc_name
198
+ ) # codeql[py/path-injection] file_path constrained to workspace dir
199
+
200
+ buffer.seek(0)
201
+ return StreamingResponse(
202
+ buffer,
203
+ media_type="application/zip",
204
+ headers={"Content-Disposition": f'attachment; filename="{zip_name}"'},
205
+ )
206
+
207
+ @router.post("/workspace/folder")
208
+ async def create_workspace_folder(request: Request, path: str):
209
+ repo_root = request.app.state.engine.repo_root
210
+ try:
211
+ safe_path, rel_posix = normalize_workspace_rel_path(repo_root, path)
212
+ except ValueError as exc:
213
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
214
+
215
+ if safe_path.exists():
216
+ raise HTTPException(status_code=400, detail="path already exists")
217
+
218
+ safe_path.mkdir(parents=True, exist_ok=True)
219
+ return {"status": "created", "path": rel_posix}
220
+
221
+ @router.delete("/workspace/folder")
222
+ async def delete_workspace_folder(request: Request, path: str):
223
+ repo_root = request.app.state.engine.repo_root
224
+ try:
225
+ safe_path, rel_posix = normalize_workspace_rel_path(repo_root, path)
226
+ except ValueError as exc:
227
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
228
+
229
+ if not safe_path.exists():
230
+ raise HTTPException(status_code=404, detail="folder not found")
231
+ if not safe_path.is_dir():
232
+ raise HTTPException(status_code=400, detail="not a folder")
233
+ if any(safe_path.iterdir()):
234
+ raise HTTPException(status_code=400, detail="folder not empty")
235
+
236
+ safe_path.rmdir()
237
+ return {"status": "deleted", "path": rel_posix}
238
+
239
+ @router.delete("/workspace/file")
240
+ async def delete_workspace_file(request: Request, path: str):
241
+ repo_root = request.app.state.engine.repo_root
242
+ base = workspace_dir(repo_root)
243
+ try:
244
+ safe_path, rel_posix = normalize_workspace_rel_path(repo_root, path)
245
+ except ValueError as exc:
246
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
247
+
248
+ if safe_path.parent == base and safe_path.name in PINNED_DOC_FILENAMES:
249
+ raise HTTPException(status_code=400, detail="cannot delete pinned docs")
250
+ if not safe_path.exists():
251
+ raise HTTPException(status_code=404, detail="file not found")
252
+ if safe_path.is_dir():
253
+ raise HTTPException(status_code=400, detail="use folder delete endpoint")
254
+
255
+ safe_path.unlink() # codeql[py/path-injection] safe_path validated by normalize_workspace_rel_path
256
+ return {"status": "deleted", "path": rel_posix}
257
+
258
+ @router.post("/workspace/spec/ingest", response_model=SpecIngestTicketsResponse)
259
+ def ingest_workspace_spec(request: Request):
260
+ repo_root = request.app.state.engine.repo_root
261
+ try:
262
+ result = ingest_workspace_spec_to_tickets(repo_root)
263
+ except SpecIngestTicketsError as exc:
264
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
265
+ return {
266
+ "status": "ok",
267
+ "created": result.created,
268
+ "first_ticket_path": result.first_ticket_path,
269
+ }
270
+
271
+ return router
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from ...core.engine import Engine
4
+ from ...core.runner_controller import ProcessRunnerController
5
+
6
+
7
+ class RunnerManager:
8
+ def __init__(self, engine: Engine):
9
+ self._controller = ProcessRunnerController(engine)
10
+
11
+ @property
12
+ def running(self) -> bool:
13
+ return self._controller.running
14
+
15
+ def start(self, once: bool = False) -> None:
16
+ self._controller.start(once=once)
17
+
18
+ def resume(self, once: bool = False) -> None:
19
+ self._controller.resume(once=once)
20
+
21
+ def stop(self) -> None:
22
+ self._controller.stop()
23
+
24
+ def kill(self) -> None:
25
+ self._controller.kill()
@@ -0,0 +1,417 @@
1
+ """
2
+ Pydantic request/response schemas for web and API routes.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any, Dict, List, Literal, Optional
8
+
9
+ from pydantic import AliasChoices, BaseModel, ConfigDict, Field
10
+
11
+
12
+ class Payload(BaseModel):
13
+ model_config = ConfigDict(extra="ignore", populate_by_name=True)
14
+
15
+
16
+ class ResponseModel(BaseModel):
17
+ model_config = ConfigDict(extra="ignore")
18
+
19
+
20
+ class WorkspaceWriteRequest(Payload):
21
+ content: str = ""
22
+
23
+
24
+ class WorkspaceResponse(ResponseModel):
25
+ active_context: str
26
+ decisions: str
27
+ spec: str
28
+
29
+
30
+ class WorkspaceFileItem(ResponseModel):
31
+ name: str
32
+ path: str
33
+ is_pinned: bool = False
34
+ modified_at: Optional[str] = None
35
+
36
+
37
+ class WorkspaceFileListResponse(ResponseModel):
38
+ files: List[WorkspaceFileItem]
39
+
40
+
41
+ class WorkspaceNode(ResponseModel):
42
+ name: str
43
+ path: str
44
+ type: Literal["file", "folder"]
45
+ is_pinned: bool = False
46
+ modified_at: Optional[str] = None
47
+ size: Optional[int] = None
48
+ children: Optional[List["WorkspaceNode"]] = None
49
+
50
+
51
+ WorkspaceNode.model_rebuild()
52
+
53
+
54
+ class WorkspaceTreeResponse(ResponseModel):
55
+ tree: List[WorkspaceNode]
56
+
57
+
58
+ class WorkspaceUploadedItem(ResponseModel):
59
+ filename: str
60
+ path: str
61
+ size: int
62
+
63
+
64
+ class WorkspaceUploadResponse(ResponseModel):
65
+ status: str
66
+ uploaded: List[WorkspaceUploadedItem]
67
+
68
+
69
+ class ArchiveSnapshotSummary(ResponseModel):
70
+ snapshot_id: str
71
+ worktree_repo_id: str
72
+ created_at: Optional[str] = None
73
+ status: Optional[str] = None
74
+ branch: Optional[str] = None
75
+ head_sha: Optional[str] = None
76
+ note: Optional[str] = None
77
+ summary: Optional[Dict[str, Any]] = None
78
+
79
+
80
+ class ArchiveSnapshotsResponse(ResponseModel):
81
+ snapshots: List[ArchiveSnapshotSummary]
82
+
83
+
84
+ class ArchiveSnapshotDetailResponse(ResponseModel):
85
+ snapshot: ArchiveSnapshotSummary
86
+ meta: Optional[Dict[str, Any]] = None
87
+
88
+
89
+ class ArchiveTreeNode(ResponseModel):
90
+ path: str
91
+ name: str
92
+ type: Literal["file", "folder"]
93
+ size_bytes: Optional[int] = None
94
+ mtime: Optional[float] = None
95
+
96
+
97
+ class ArchiveTreeResponse(ResponseModel):
98
+ path: str
99
+ nodes: List[ArchiveTreeNode]
100
+
101
+
102
+ class SpecIngestTicketsResponse(ResponseModel):
103
+ status: str
104
+ created: int
105
+ first_ticket_path: Optional[str] = None
106
+
107
+
108
+ class RunControlRequest(Payload):
109
+ once: bool = False
110
+ agent: Optional[str] = None
111
+ model: Optional[str] = None
112
+ reasoning: Optional[str] = None
113
+
114
+
115
+ class HubCreateRepoRequest(Payload):
116
+ git_url: Optional[str] = Field(
117
+ default=None, validation_alias=AliasChoices("git_url", "gitUrl")
118
+ )
119
+ repo_id: Optional[str] = Field(
120
+ default=None, validation_alias=AliasChoices("repo_id", "id")
121
+ )
122
+ path: Optional[str] = None
123
+ git_init: bool = True
124
+ force: bool = False
125
+
126
+
127
+ class HubRemoveRepoRequest(Payload):
128
+ force: bool = False
129
+ delete_dir: bool = True
130
+ delete_worktrees: bool = False
131
+
132
+
133
+ class HubCreateWorktreeRequest(Payload):
134
+ base_repo_id: str = Field(
135
+ validation_alias=AliasChoices("base_repo_id", "baseRepoId")
136
+ )
137
+ branch: str
138
+ force: bool = False
139
+ start_point: Optional[str] = Field(
140
+ default=None,
141
+ validation_alias=AliasChoices(
142
+ "start_point", "startPoint", "base_ref", "baseRef"
143
+ ),
144
+ )
145
+
146
+
147
+ class HubCleanupWorktreeRequest(Payload):
148
+ worktree_repo_id: str = Field(
149
+ validation_alias=AliasChoices("worktree_repo_id", "worktreeRepoId")
150
+ )
151
+ delete_branch: bool = False
152
+ delete_remote: bool = False
153
+ archive: bool = True
154
+ force_archive: bool = Field(
155
+ default=False, validation_alias=AliasChoices("force_archive", "forceArchive")
156
+ )
157
+ archive_note: Optional[str] = Field(
158
+ default=None, validation_alias=AliasChoices("archive_note", "archiveNote")
159
+ )
160
+
161
+
162
+ class AppServerThreadResetRequest(Payload):
163
+ key: str = Field(
164
+ validation_alias=AliasChoices("key", "feature", "feature_key", "featureKey")
165
+ )
166
+
167
+
168
+ class AppServerThreadArchiveRequest(Payload):
169
+ thread_id: str = Field(validation_alias=AliasChoices("thread_id", "threadId", "id"))
170
+
171
+
172
+ class SessionSettingsRequest(Payload):
173
+ autorunner_model_override: Optional[str] = None
174
+ autorunner_effort_override: Optional[str] = None
175
+ autorunner_approval_policy: Optional[str] = None
176
+ autorunner_sandbox_mode: Optional[str] = None
177
+ autorunner_workspace_write_network: Optional[bool] = None
178
+ runner_stop_after_runs: Optional[int] = None
179
+
180
+
181
+ class GithubIssueRequest(Payload):
182
+ issue: str
183
+
184
+
185
+ class GithubContextRequest(Payload):
186
+ url: str
187
+
188
+
189
+ class GithubPrSyncRequest(Payload):
190
+ draft: bool = True
191
+ title: Optional[str] = None
192
+ body: Optional[str] = None
193
+ mode: Optional[str] = None
194
+
195
+
196
+ class SessionStopRequest(Payload):
197
+ session_id: Optional[str] = None
198
+ repo_path: Optional[str] = None
199
+
200
+
201
+ class SystemUpdateRequest(Payload):
202
+ target: Optional[str] = None
203
+
204
+
205
+ class HubJobResponse(ResponseModel):
206
+ job_id: str
207
+ kind: str
208
+ status: str
209
+ created_at: str
210
+ started_at: Optional[str]
211
+ finished_at: Optional[str]
212
+ result: Optional[Dict[str, Any]]
213
+ error: Optional[str]
214
+
215
+
216
+ class StateResponse(ResponseModel):
217
+ last_run_id: Optional[int]
218
+ status: str
219
+ last_exit_code: Optional[int]
220
+ last_run_started_at: Optional[str]
221
+ last_run_finished_at: Optional[str]
222
+ outstanding_count: int
223
+ done_count: int
224
+ running: bool
225
+ runner_pid: Optional[int]
226
+ lock_present: bool
227
+ lock_pid: Optional[int]
228
+ lock_freeable: bool
229
+ lock_freeable_reason: Optional[str]
230
+ terminal_idle_timeout_seconds: Optional[int]
231
+ codex_model: str
232
+
233
+
234
+ class SessionSettingsResponse(ResponseModel):
235
+ autorunner_model_override: Optional[str]
236
+ autorunner_effort_override: Optional[str]
237
+ autorunner_approval_policy: Optional[str]
238
+ autorunner_sandbox_mode: Optional[str]
239
+ autorunner_workspace_write_network: Optional[bool]
240
+ runner_stop_after_runs: Optional[int]
241
+
242
+
243
+ class VersionResponse(ResponseModel):
244
+ asset_version: Optional[str]
245
+
246
+
247
+ class RunControlResponse(ResponseModel):
248
+ running: bool
249
+ once: bool
250
+
251
+
252
+ class RunStatusResponse(ResponseModel):
253
+ running: bool
254
+
255
+
256
+ class RunResetResponse(ResponseModel):
257
+ status: str
258
+ message: str
259
+
260
+
261
+ class SessionItemResponse(ResponseModel):
262
+ session_id: str
263
+ repo_path: Optional[str]
264
+ abs_repo_path: Optional[str] = None
265
+ created_at: Optional[str]
266
+ last_seen_at: Optional[str]
267
+ status: Optional[str]
268
+ alive: bool
269
+
270
+
271
+ class SessionsResponse(ResponseModel):
272
+ sessions: List[SessionItemResponse]
273
+ repo_to_session: Dict[str, str]
274
+ abs_repo_to_session: Optional[Dict[str, str]] = None
275
+
276
+
277
+ class SessionStopResponse(ResponseModel):
278
+ status: str
279
+ session_id: str
280
+
281
+
282
+ class AppServerThreadsResponse(ResponseModel):
283
+ file_chat: Optional[str] = None
284
+ file_chat_opencode: Optional[str] = None
285
+ autorunner: Optional[str] = None
286
+ autorunner_opencode: Optional[str] = None
287
+ corruption: Optional[Dict[str, Any]] = None
288
+
289
+
290
+ class AppServerThreadResetResponse(ResponseModel):
291
+ status: str
292
+ key: str
293
+ cleared: bool
294
+
295
+
296
+ class AppServerThreadArchiveResponse(ResponseModel):
297
+ status: str
298
+ thread_id: str
299
+ archived: bool
300
+
301
+
302
+ class AppServerThreadResetAllResponse(ResponseModel):
303
+ status: str
304
+ cleared: bool
305
+
306
+
307
+ class TokenTotalsResponse(ResponseModel):
308
+ input_tokens: int
309
+ cached_input_tokens: int
310
+ output_tokens: int
311
+ reasoning_output_tokens: int
312
+ total_tokens: int
313
+
314
+
315
+ class RepoUsageResponse(ResponseModel):
316
+ mode: str
317
+ repo: str
318
+ codex_home: str
319
+ since: Optional[str]
320
+ until: Optional[str]
321
+ status: str
322
+ events: int
323
+ totals: TokenTotalsResponse
324
+ latest_rate_limits: Optional[Dict[str, Any]]
325
+
326
+
327
+ class UsageSeriesEntryResponse(ResponseModel):
328
+ key: str
329
+ model: Optional[str]
330
+ token_type: Optional[str]
331
+ total: int
332
+ values: List[int]
333
+
334
+
335
+ class UsageSeriesResponse(ResponseModel):
336
+ mode: str
337
+ repo: str
338
+ codex_home: str
339
+ since: Optional[str]
340
+ until: Optional[str]
341
+ status: str
342
+ bucket: str
343
+ segment: str
344
+ buckets: List[str]
345
+ series: List[UsageSeriesEntryResponse]
346
+
347
+
348
+ class SystemHealthResponse(ResponseModel):
349
+ status: str
350
+ mode: str
351
+ base_path: str
352
+ asset_version: Optional[str] = None
353
+
354
+
355
+ class SystemUpdateResponse(ResponseModel):
356
+ status: str
357
+ message: str
358
+ target: str
359
+
360
+
361
+ class SystemUpdateStatusResponse(ResponseModel):
362
+ status: str
363
+ message: str
364
+
365
+
366
+ class SystemUpdateCheckResponse(ResponseModel):
367
+ status: str
368
+ update_available: bool
369
+ message: str
370
+ local_commit: Optional[str] = None
371
+ remote_commit: Optional[str] = None
372
+
373
+
374
+ class ReviewStartRequest(Payload):
375
+ agent: Optional[str] = None
376
+ model: Optional[str] = None
377
+ reasoning: Optional[str] = None
378
+ max_wallclock_seconds: Optional[int] = Field(
379
+ default=None,
380
+ validation_alias=AliasChoices("max_wallclock_seconds", "maxWallclockSeconds"),
381
+ )
382
+
383
+
384
+ class ReviewStatusResponse(ResponseModel):
385
+ review: Dict[str, Any]
386
+
387
+
388
+ class ReviewControlResponse(ResponseModel):
389
+ status: str
390
+ detail: Optional[str] = None
391
+
392
+
393
+ # Ticket CRUD schemas
394
+
395
+
396
+ class TicketCreateRequest(Payload):
397
+ agent: str = "codex"
398
+ title: Optional[str] = None
399
+ goal: Optional[str] = None
400
+ body: str = ""
401
+
402
+
403
+ class TicketUpdateRequest(Payload):
404
+ content: str # Full markdown with frontmatter
405
+
406
+
407
+ class TicketResponse(ResponseModel):
408
+ path: str
409
+ index: int
410
+ frontmatter: Dict[str, Any]
411
+ body: str
412
+
413
+
414
+ class TicketDeleteResponse(ResponseModel):
415
+ status: str
416
+ index: int
417
+ path: str