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.
- codex_autorunner/__init__.py +12 -1
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/runtime.py +59 -18
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +36 -7
- codex_autorunner/bootstrap.py +226 -4
- codex_autorunner/cli.py +5 -1174
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +20 -0
- codex_autorunner/core/about_car.py +119 -1
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_threads.py +17 -2
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +6 -2
- codex_autorunner/core/config.py +433 -4
- codex_autorunner/core/context_awareness.py +38 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/drafts.py +58 -4
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +96 -2
- codex_autorunner/core/flows/models.py +13 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +134 -0
- codex_autorunner/core/flows/runtime.py +57 -4
- codex_autorunner/core/flows/store.py +142 -7
- codex_autorunner/core/flows/transition.py +27 -15
- codex_autorunner/core/flows/ux_helpers.py +272 -0
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +291 -20
- codex_autorunner/core/lifecycle_events.py +253 -0
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/path_utils.py +2 -1
- codex_autorunner/core/pma_audit.py +224 -0
- codex_autorunner/core/pma_context.py +496 -0
- codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
- codex_autorunner/core/pma_lifecycle.py +527 -0
- codex_autorunner/core/pma_queue.py +367 -0
- codex_autorunner/core/pma_safety.py +221 -0
- codex_autorunner/core/pma_state.py +115 -0
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
- codex_autorunner/core/prompt.py +0 -80
- codex_autorunner/core/prompts.py +56 -172
- codex_autorunner/core/redaction.py +0 -4
- codex_autorunner/core/review_context.py +11 -9
- codex_autorunner/core/runner_controller.py +35 -33
- codex_autorunner/core/runner_state.py +147 -0
- codex_autorunner/core/runtime.py +829 -0
- codex_autorunner/core/sqlite_utils.py +13 -4
- codex_autorunner/core/state.py +7 -10
- codex_autorunner/core/state_roots.py +62 -0
- codex_autorunner/core/supervisor_protocol.py +15 -0
- codex_autorunner/core/templates/__init__.py +39 -0
- codex_autorunner/core/templates/git_mirror.py +234 -0
- codex_autorunner/core/templates/provenance.py +56 -0
- codex_autorunner/core/templates/scan_cache.py +120 -0
- codex_autorunner/core/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +218 -0
- codex_autorunner/core/ticket_manager_cli.py +494 -0
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/update.py +4 -5
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +125 -15
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
- codex_autorunner/flows/ticket_flow/definition.py +52 -3
- codex_autorunner/integrations/agents/__init__.py +11 -19
- codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +177 -25
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +305 -32
- codex_autorunner/integrations/agents/runner.py +86 -0
- codex_autorunner/integrations/agents/wiring.py +279 -0
- codex_autorunner/integrations/app_server/client.py +7 -60
- codex_autorunner/integrations/app_server/env.py +2 -107
- codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
- codex_autorunner/integrations/telegram/adapter.py +65 -0
- codex_autorunner/integrations/telegram/config.py +46 -0
- codex_autorunner/integrations/telegram/constants.py +1 -1
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
- codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +1496 -71
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
- codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +22 -1
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +45 -10
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
- codex_autorunner/integrations/telegram/transport.py +13 -4
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/routes/__init__.py +37 -76
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +2 -238
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -596
- codex_autorunner/routes/file_chat.py +4 -833
- codex_autorunner/routes/flows.py +4 -977
- codex_autorunner/routes/messages.py +4 -456
- codex_autorunner/routes/repos.py +2 -196
- codex_autorunner/routes/review.py +2 -147
- codex_autorunner/routes/sessions.py +2 -175
- codex_autorunner/routes/settings.py +2 -168
- codex_autorunner/routes/shared.py +2 -275
- codex_autorunner/routes/system.py +4 -193
- codex_autorunner/routes/usage.py +2 -86
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +2 -270
- codex_autorunner/server.py +4 -4
- codex_autorunner/static/agentControls.js +61 -16
- codex_autorunner/static/app.js +126 -14
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +7 -7
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/dashboard.js +224 -171
- codex_autorunner/static/docChatCore.js +185 -13
- codex_autorunner/static/fileChat.js +68 -40
- codex_autorunner/static/fileboxUi.js +159 -0
- codex_autorunner/static/hub.js +114 -131
- codex_autorunner/static/index.html +375 -49
- codex_autorunner/static/messages.js +568 -87
- codex_autorunner/static/notifications.js +255 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +128 -6
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9798 -6143
- codex_autorunner/static/tabs.js +152 -11
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/terminal.js +18 -0
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +137 -15
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +821 -98
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +39 -0
- codex_autorunner/static/workspace.js +389 -82
- codex_autorunner/static/workspaceFileBrowser.js +15 -13
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +2534 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2223 -0
- codex_autorunner/surfaces/web/hub_jobs.py +192 -0
- codex_autorunner/surfaces/web/middleware.py +587 -0
- codex_autorunner/surfaces/web/pty_session.py +370 -0
- codex_autorunner/surfaces/web/review.py +6 -0
- codex_autorunner/surfaces/web/routes/__init__.py +82 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +284 -0
- codex_autorunner/surfaces/web/routes/app_server.py +132 -0
- codex_autorunner/surfaces/web/routes/archive.py +357 -0
- codex_autorunner/surfaces/web/routes/base.py +615 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +1354 -0
- codex_autorunner/surfaces/web/routes/messages.py +490 -0
- codex_autorunner/surfaces/web/routes/pma.py +1652 -0
- codex_autorunner/surfaces/web/routes/repos.py +197 -0
- codex_autorunner/surfaces/web/routes/review.py +148 -0
- codex_autorunner/surfaces/web/routes/sessions.py +176 -0
- codex_autorunner/surfaces/web/routes/settings.py +169 -0
- codex_autorunner/surfaces/web/routes/shared.py +277 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -0
- codex_autorunner/surfaces/web/routes/templates.py +634 -0
- codex_autorunner/surfaces/web/routes/usage.py +89 -0
- codex_autorunner/surfaces/web/routes/voice.py +120 -0
- codex_autorunner/surfaces/web/routes/workspace.py +271 -0
- codex_autorunner/surfaces/web/runner_manager.py +25 -0
- codex_autorunner/surfaces/web/schemas.py +469 -0
- codex_autorunner/surfaces/web/static_assets.py +490 -0
- codex_autorunner/surfaces/web/static_refresh.py +86 -0
- codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
- codex_autorunner/tickets/__init__.py +8 -1
- codex_autorunner/tickets/agent_pool.py +53 -4
- codex_autorunner/tickets/files.py +37 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +6 -1
- codex_autorunner/tickets/outbox.py +50 -2
- codex_autorunner/tickets/runner.py +396 -57
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1949
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -586
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -376
- codex_autorunner/web/static_assets.py +4 -441
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/paths.py +49 -33
- codex_autorunner-1.2.0.dist-info/METADATA +150 -0
- codex_autorunner-1.2.0.dist-info/RECORD +339 -0
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -2653
- codex_autorunner/core/static_assets.py +0 -55
- codex_autorunner-1.0.0.dist-info/METADATA +0 -246
- codex_autorunner-1.0.0.dist-info/RECORD +0 -251
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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
|