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,227 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Any, Optional
6
+
7
+ from fastapi import APIRouter, HTTPException, Request
8
+ from fastapi.responses import FileResponse
9
+
10
+ from ....core.filebox import (
11
+ BOXES,
12
+ FileBoxEntry,
13
+ delete_file,
14
+ ensure_structure,
15
+ list_filebox,
16
+ migrate_legacy,
17
+ resolve_file,
18
+ save_file,
19
+ )
20
+ from ....core.hub import HubSupervisor
21
+ from ....core.utils import find_repo_root
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ def _serialize_entry(entry: FileBoxEntry, *, request: Request) -> dict[str, Any]:
27
+ base = request.scope.get("root_path", "") or ""
28
+ # Provide a download URL that survives base_path rewrites.
29
+ download = f"{base}/api/filebox/{entry.box}/{entry.name}"
30
+ return {
31
+ "name": entry.name,
32
+ "box": entry.box,
33
+ "size": entry.size,
34
+ "modified_at": entry.modified_at,
35
+ "source": entry.source,
36
+ "url": download,
37
+ }
38
+
39
+
40
+ def _serialize_listing(
41
+ entries: dict[str, list[FileBoxEntry]], *, request: Request
42
+ ) -> dict[str, Any]:
43
+ return {
44
+ box: [_serialize_entry(e, request=request) for e in files]
45
+ for box, files in entries.items()
46
+ }
47
+
48
+
49
+ def _resolve_repo_root(request: Request) -> Path:
50
+ engine = getattr(request.app.state, "engine", None)
51
+ repo_root = getattr(engine, "repo_root", None)
52
+ if isinstance(repo_root, Path):
53
+ return repo_root
54
+ if isinstance(repo_root, str):
55
+ try:
56
+ return Path(repo_root)
57
+ except Exception:
58
+ pass
59
+ return find_repo_root()
60
+
61
+
62
+ def build_filebox_routes() -> APIRouter:
63
+ router = APIRouter(prefix="/api", tags=["filebox"])
64
+
65
+ @router.get("/filebox")
66
+ def list_box(request: Request) -> dict[str, Any]:
67
+ repo_root = _resolve_repo_root(request)
68
+ ensure_structure(repo_root)
69
+ try:
70
+ migrate_legacy(repo_root)
71
+ except Exception:
72
+ logger.debug("FileBox legacy migration skipped", exc_info=True)
73
+ entries = list_filebox(repo_root)
74
+ return _serialize_listing(entries, request=request)
75
+
76
+ @router.get("/filebox/{box}")
77
+ def list_single_box(box: str, request: Request) -> dict[str, Any]:
78
+ if box not in BOXES:
79
+ raise HTTPException(status_code=400, detail="Invalid box")
80
+ repo_root = _resolve_repo_root(request)
81
+ ensure_structure(repo_root)
82
+ entries = list_filebox(repo_root)
83
+ return {box: _serialize_listing(entries, request=request).get(box, [])}
84
+
85
+ @router.post("/filebox/{box}")
86
+ async def upload_file(box: str, request: Request) -> dict[str, Any]:
87
+ if box not in BOXES:
88
+ raise HTTPException(status_code=400, detail="Invalid box")
89
+ repo_root = _resolve_repo_root(request)
90
+ ensure_structure(repo_root)
91
+
92
+ form = await request.form()
93
+ saved = []
94
+ for filename, file in form.items():
95
+ try:
96
+ data = await file.read()
97
+ except Exception as exc: # pragma: no cover - defensive
98
+ logger.warning("Failed to read upload: %s", exc)
99
+ continue
100
+ try:
101
+ path = save_file(repo_root, box, filename, data)
102
+ saved.append(path.name)
103
+ except ValueError as exc:
104
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
105
+ return {"status": "ok", "saved": saved}
106
+
107
+ @router.get("/filebox/{box}/{filename}")
108
+ def download_file(box: str, filename: str, request: Request):
109
+ if box not in BOXES:
110
+ raise HTTPException(status_code=400, detail="Invalid box")
111
+ repo_root = _resolve_repo_root(request)
112
+ entry = resolve_file(repo_root, box, filename)
113
+ if entry is None:
114
+ raise HTTPException(status_code=404, detail="File not found")
115
+ return FileResponse(entry.path, filename=entry.name)
116
+
117
+ @router.delete("/filebox/{box}/{filename}")
118
+ def delete_file_entry(box: str, filename: str, request: Request) -> dict[str, Any]:
119
+ if box not in BOXES:
120
+ raise HTTPException(status_code=400, detail="Invalid box")
121
+ repo_root = _resolve_repo_root(request)
122
+ removed = delete_file(repo_root, box, filename)
123
+ if not removed:
124
+ raise HTTPException(status_code=404, detail="File not found")
125
+ return {"status": "ok"}
126
+
127
+ return router
128
+
129
+
130
+ def _resolve_hub_repo_root(request: Request, repo_id: Optional[str]) -> Path:
131
+ supervisor: HubSupervisor | None = getattr(
132
+ request.app.state, "hub_supervisor", None
133
+ )
134
+ if supervisor is None:
135
+ raise HTTPException(status_code=404, detail="Hub supervisor unavailable")
136
+ snapshots = supervisor.list_repos()
137
+ candidates = [
138
+ snap for snap in snapshots if snap.initialized and snap.exists_on_disk
139
+ ]
140
+ target = None
141
+ if repo_id:
142
+ target = next((snap for snap in candidates if snap.id == repo_id), None)
143
+ if target is None:
144
+ raise HTTPException(status_code=404, detail="Repo not found")
145
+ else:
146
+ if len(candidates) == 1:
147
+ target = candidates[0]
148
+ if target is None:
149
+ raise HTTPException(status_code=400, detail="repo_id is required")
150
+ return target.path
151
+
152
+
153
+ def _serialize_hub_entry(
154
+ entry: FileBoxEntry, *, request: Request, repo_id: str
155
+ ) -> dict[str, Any]:
156
+ base = request.scope.get("root_path", "") or ""
157
+ download = f"{base}/hub/filebox/{repo_id}/{entry.box}/{entry.name}"
158
+ return {
159
+ "name": entry.name,
160
+ "box": entry.box,
161
+ "size": entry.size,
162
+ "modified_at": entry.modified_at,
163
+ "source": entry.source,
164
+ "url": download,
165
+ "repo_id": repo_id,
166
+ }
167
+
168
+
169
+ def build_hub_filebox_routes() -> APIRouter:
170
+ router = APIRouter(prefix="/hub/filebox", tags=["filebox"])
171
+
172
+ @router.get("/{repo_id}")
173
+ def list_repo_filebox(repo_id: str, request: Request) -> dict[str, Any]:
174
+ repo_root = _resolve_hub_repo_root(request, repo_id)
175
+ try:
176
+ migrate_legacy(repo_root)
177
+ except Exception:
178
+ logger.debug("Hub FileBox legacy migration skipped", exc_info=True)
179
+ entries = list_filebox(repo_root)
180
+ serialized = {
181
+ box: [
182
+ _serialize_hub_entry(e, request=request, repo_id=repo_id) for e in files
183
+ ]
184
+ for box, files in entries.items()
185
+ }
186
+ return serialized
187
+
188
+ @router.post("/{repo_id}/{box}")
189
+ async def hub_upload(repo_id: str, box: str, request: Request) -> dict[str, Any]:
190
+ if box not in BOXES:
191
+ raise HTTPException(status_code=400, detail="Invalid box")
192
+ repo_root = _resolve_hub_repo_root(request, repo_id)
193
+ ensure_structure(repo_root)
194
+ form = await request.form()
195
+ saved = []
196
+ for filename, file in form.items():
197
+ data = await file.read()
198
+ path = save_file(repo_root, box, filename, data)
199
+ saved.append(path.name)
200
+ return {"status": "ok", "saved": saved}
201
+
202
+ @router.get("/{repo_id}/{box}/{filename}")
203
+ def hub_download(repo_id: str, box: str, filename: str, request: Request):
204
+ if box not in BOXES:
205
+ raise HTTPException(status_code=400, detail="Invalid box")
206
+ repo_root = _resolve_hub_repo_root(request, repo_id)
207
+ entry = resolve_file(repo_root, box, filename)
208
+ if entry is None:
209
+ raise HTTPException(status_code=404, detail="File not found")
210
+ return FileResponse(entry.path, filename=entry.name)
211
+
212
+ @router.delete("/{repo_id}/{box}/{filename}")
213
+ def hub_delete(
214
+ repo_id: str, box: str, filename: str, request: Request
215
+ ) -> dict[str, Any]:
216
+ if box not in BOXES:
217
+ raise HTTPException(status_code=400, detail="Invalid box")
218
+ repo_root = _resolve_hub_repo_root(request, repo_id)
219
+ removed = delete_file(repo_root, box, filename)
220
+ if not removed:
221
+ raise HTTPException(status_code=404, detail="File not found")
222
+ return {"status": "ok"}
223
+
224
+ return router
225
+
226
+
227
+ __all__ = ["build_filebox_routes", "build_hub_filebox_routes"]