codex-autorunner 1.1.0__py3-none-any.whl → 1.2.1__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/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +17 -7
- codex_autorunner/bootstrap.py +219 -1
- codex_autorunner/core/__init__.py +17 -1
- codex_autorunner/core/about_car.py +124 -11
- codex_autorunner/core/app_server_threads.py +6 -0
- codex_autorunner/core/config.py +238 -3
- codex_autorunner/core/context_awareness.py +39 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +71 -1
- codex_autorunner/core/flows/reconciler.py +4 -1
- codex_autorunner/core/flows/runtime.py +22 -0
- codex_autorunner/core/flows/store.py +61 -9
- codex_autorunner/core/flows/transition.py +23 -16
- codex_autorunner/core/flows/ux_helpers.py +18 -3
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/hub.py +198 -41
- codex_autorunner/core/lifecycle_events.py +253 -0
- codex_autorunner/core/path_utils.py +2 -1
- codex_autorunner/core/pma_audit.py +224 -0
- codex_autorunner/core/pma_context.py +683 -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/agent_backend.py +2 -5
- codex_autorunner/core/ports/run_event.py +1 -4
- 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 +5 -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/ticket_linter_cli.py +17 -0
- codex_autorunner/core/ticket_manager_cli.py +154 -92
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/utils.py +34 -6
- codex_autorunner/flows/review/service.py +23 -25
- codex_autorunner/flows/ticket_flow/definition.py +43 -1
- codex_autorunner/integrations/agents/__init__.py +2 -0
- codex_autorunner/integrations/agents/backend_orchestrator.py +18 -0
- codex_autorunner/integrations/agents/codex_backend.py +19 -8
- codex_autorunner/integrations/agents/runner.py +3 -8
- codex_autorunner/integrations/agents/wiring.py +8 -0
- codex_autorunner/integrations/telegram/adapter.py +1 -1
- codex_autorunner/integrations/telegram/config.py +1 -1
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- 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 +346 -58
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +202 -45
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +18 -7
- codex_autorunner/integrations/telegram/handlers/messages.py +34 -3
- codex_autorunner/integrations/telegram/helpers.py +1 -3
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +30 -0
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +10 -4
- codex_autorunner/integrations/telegram/transport.py +10 -3
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/server.py +2 -2
- codex_autorunner/static/agentControls.js +21 -5
- codex_autorunner/static/app.js +115 -11
- codex_autorunner/static/archive.js +274 -81
- codex_autorunner/static/archiveApi.js +21 -0
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/constants.js +1 -1
- 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 +46 -81
- codex_autorunner/static/index.html +303 -24
- codex_autorunner/static/messages.js +82 -4
- codex_autorunner/static/notifications.js +288 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/settings.js +3 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9141 -6742
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/terminalManager.js +22 -3
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +41 -13
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +69 -19
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +28 -0
- codex_autorunner/static/workspace.js +258 -44
- codex_autorunner/static/workspaceFileBrowser.js +6 -4
- codex_autorunner/surfaces/cli/cli.py +1465 -155
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/web/app.py +253 -49
- codex_autorunner/surfaces/web/routes/__init__.py +4 -0
- codex_autorunner/surfaces/web/routes/analytics.py +29 -22
- codex_autorunner/surfaces/web/routes/archive.py +197 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +297 -36
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +219 -29
- codex_autorunner/surfaces/web/routes/messages.py +70 -39
- codex_autorunner/surfaces/web/routes/pma.py +1652 -0
- codex_autorunner/surfaces/web/routes/repos.py +1 -1
- codex_autorunner/surfaces/web/routes/shared.py +0 -3
- codex_autorunner/surfaces/web/routes/templates.py +634 -0
- codex_autorunner/surfaces/web/runner_manager.py +2 -2
- codex_autorunner/surfaces/web/schemas.py +81 -18
- codex_autorunner/tickets/agent_pool.py +27 -0
- codex_autorunner/tickets/files.py +33 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +3 -0
- codex_autorunner/tickets/outbox.py +41 -5
- codex_autorunner/tickets/runner.py +350 -69
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/METADATA +15 -19
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/RECORD +132 -101
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -3302
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/top_level.txt +0 -0
|
@@ -6,6 +6,7 @@ from datetime import datetime
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Awaitable, Callable, Optional
|
|
8
8
|
|
|
9
|
+
from ...core.config import load_repo_config
|
|
9
10
|
from ...core.flows import FlowStore
|
|
10
11
|
from ...core.flows.controller import FlowController
|
|
11
12
|
from ...core.flows.models import FlowRunRecord, FlowRunStatus
|
|
@@ -241,7 +242,8 @@ class TelegramTicketFlowBridge:
|
|
|
241
242
|
db_path = workspace_root / ".codex-autorunner" / "flows.db"
|
|
242
243
|
if not db_path.exists():
|
|
243
244
|
return None
|
|
244
|
-
|
|
245
|
+
config = load_repo_config(workspace_root)
|
|
246
|
+
store = FlowStore(db_path, durable=config.durable_writes)
|
|
245
247
|
try:
|
|
246
248
|
store.initialize()
|
|
247
249
|
runs = store.list_flow_runs(
|
|
@@ -305,7 +307,8 @@ class TelegramTicketFlowBridge:
|
|
|
305
307
|
db_path = workspace_root / ".codex-autorunner" / "flows.db"
|
|
306
308
|
if not db_path.exists():
|
|
307
309
|
return None
|
|
308
|
-
|
|
310
|
+
config = load_repo_config(workspace_root)
|
|
311
|
+
store = FlowStore(db_path, durable=config.durable_writes)
|
|
309
312
|
try:
|
|
310
313
|
store.initialize()
|
|
311
314
|
if preferred_run_id:
|
|
@@ -573,16 +576,19 @@ def _ticket_controller_for(repo_root: Path) -> FlowController:
|
|
|
573
576
|
artifacts_root = repo_root / ".codex-autorunner" / "flows"
|
|
574
577
|
from ...agents.registry import validate_agent_id
|
|
575
578
|
from ...core.config import load_repo_config
|
|
576
|
-
from ...core.
|
|
579
|
+
from ...core.runtime import RuntimeContext
|
|
580
|
+
from ...integrations.agents import build_backend_orchestrator
|
|
577
581
|
from ...integrations.agents.wiring import (
|
|
578
582
|
build_agent_backend_factory,
|
|
579
583
|
build_app_server_supervisor_factory,
|
|
580
584
|
)
|
|
581
585
|
|
|
582
586
|
config = load_repo_config(repo_root)
|
|
583
|
-
|
|
587
|
+
backend_orchestrator = build_backend_orchestrator(repo_root, config)
|
|
588
|
+
engine = RuntimeContext(
|
|
584
589
|
repo_root,
|
|
585
590
|
config=config,
|
|
591
|
+
backend_orchestrator=backend_orchestrator,
|
|
586
592
|
backend_factory=build_agent_backend_factory(repo_root, config),
|
|
587
593
|
app_server_supervisor_factory=build_app_server_supervisor_factory(config),
|
|
588
594
|
agent_id_validator=validate_agent_id,
|
|
@@ -265,6 +265,7 @@ class TelegramMessageTransport:
|
|
|
265
265
|
thread_id: Optional[int] = None,
|
|
266
266
|
reply_to: Optional[int] = None,
|
|
267
267
|
reply_markup: Optional[dict[str, Any]] = None,
|
|
268
|
+
parse_mode: Optional[str] = None,
|
|
268
269
|
) -> None:
|
|
269
270
|
if _should_trace_message(text):
|
|
270
271
|
text = _with_conversation_id(
|
|
@@ -279,9 +280,15 @@ class TelegramMessageTransport:
|
|
|
279
280
|
)
|
|
280
281
|
if prefix:
|
|
281
282
|
text = f"{prefix}{text}"
|
|
282
|
-
|
|
283
|
-
if
|
|
284
|
-
|
|
283
|
+
effective_parse_mode = parse_mode or self._config.parse_mode
|
|
284
|
+
if effective_parse_mode:
|
|
285
|
+
try:
|
|
286
|
+
rendered, used_mode = self._render_message(
|
|
287
|
+
text, parse_mode=effective_parse_mode
|
|
288
|
+
)
|
|
289
|
+
except TypeError:
|
|
290
|
+
# Back-compat for subclasses/tests that don't accept parse_mode kwarg
|
|
291
|
+
rendered, used_mode = self._render_message(text) # type: ignore[misc]
|
|
285
292
|
if used_mode and len(rendered) > TELEGRAM_MAX_MESSAGE_LENGTH:
|
|
286
293
|
overflow_mode = getattr(self._config, "message_overflow", "document")
|
|
287
294
|
if overflow_mode == "split" and used_mode in (
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Template integration helpers."""
|
|
2
|
+
|
|
3
|
+
from .scan_agent import (
|
|
4
|
+
TemplateScanBackendError,
|
|
5
|
+
TemplateScanDecision,
|
|
6
|
+
TemplateScanError,
|
|
7
|
+
TemplateScanFormatError,
|
|
8
|
+
TemplateScanRejectedError,
|
|
9
|
+
build_template_scan_prompt,
|
|
10
|
+
format_template_scan_rejection,
|
|
11
|
+
parse_template_scan_output,
|
|
12
|
+
run_template_scan,
|
|
13
|
+
run_template_scan_with_orchestrator,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"TemplateScanBackendError",
|
|
18
|
+
"TemplateScanDecision",
|
|
19
|
+
"TemplateScanError",
|
|
20
|
+
"TemplateScanFormatError",
|
|
21
|
+
"TemplateScanRejectedError",
|
|
22
|
+
"build_template_scan_prompt",
|
|
23
|
+
"format_template_scan_rejection",
|
|
24
|
+
"parse_template_scan_output",
|
|
25
|
+
"run_template_scan",
|
|
26
|
+
"run_template_scan_with_orchestrator",
|
|
27
|
+
]
|
|
@@ -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
|
+
)
|
codex_autorunner/server.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from importlib import resources
|
|
2
2
|
|
|
3
|
-
from .core.
|
|
3
|
+
from .core.runtime import LockError, RuntimeContext, clear_stale_lock, doctor
|
|
4
4
|
from .surfaces.web.app import create_app, create_hub_app, create_repo_app
|
|
5
5
|
from .surfaces.web.middleware import BasePathRouterMiddleware
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
|
-
"Engine",
|
|
9
8
|
"LockError",
|
|
9
|
+
"RuntimeContext",
|
|
10
10
|
"BasePathRouterMiddleware",
|
|
11
11
|
"clear_stale_lock",
|
|
12
12
|
"create_app",
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// GENERATED FILE - do not edit directly. Source: static_src/
|
|
2
2
|
import { api, flash } from "./utils.js";
|
|
3
3
|
import { createSmartRefresh } from "./smartRefresh.js";
|
|
4
|
+
import { REPO_ID } from "./env.js";
|
|
5
|
+
const API_PREFIX = REPO_ID ? "/api" : "/hub/pma";
|
|
6
|
+
const STORAGE_PREFIX = REPO_ID ? "car.agent" : "car.pma.agent";
|
|
4
7
|
const STORAGE_KEYS = {
|
|
5
|
-
selected:
|
|
6
|
-
model: (agent) =>
|
|
7
|
-
reasoning: (agent) =>
|
|
8
|
+
selected: `${STORAGE_PREFIX}.selected`,
|
|
9
|
+
model: (agent) => `${STORAGE_PREFIX}.${agent}.model`,
|
|
10
|
+
reasoning: (agent) => `${STORAGE_PREFIX}.${agent}.reasoning`,
|
|
8
11
|
};
|
|
9
12
|
const FALLBACK_AGENTS = [
|
|
10
13
|
{ id: "codex", name: "Codex" },
|
|
@@ -92,7 +95,7 @@ async function loadAgents() {
|
|
|
92
95
|
}
|
|
93
96
|
agentsLoadPromise = (async () => {
|
|
94
97
|
try {
|
|
95
|
-
const data = await api(
|
|
98
|
+
const data = await api(`${API_PREFIX}/agents`, { method: "GET" });
|
|
96
99
|
const agents = Array.isArray(data?.agents) ? data.agents : [];
|
|
97
100
|
// Only use API response if it contains valid agents
|
|
98
101
|
if (agents.length > 0 && agents.every((a) => a && typeof a.id === "string")) {
|
|
@@ -152,7 +155,7 @@ async function loadModelCatalog(agent) {
|
|
|
152
155
|
if (modelCatalogPromises.has(agent)) {
|
|
153
156
|
return await modelCatalogPromises.get(agent) || null;
|
|
154
157
|
}
|
|
155
|
-
const promise = api(
|
|
158
|
+
const promise = api(`${API_PREFIX}/agents/${encodeURIComponent(agent)}/models`, {
|
|
156
159
|
method: "GET",
|
|
157
160
|
})
|
|
158
161
|
.then((data) => {
|
|
@@ -379,3 +382,16 @@ export function initAgentControls(config = {}) {
|
|
|
379
382
|
export async function ensureAgentCatalog() {
|
|
380
383
|
await refreshAgentControls({ force: true, reason: "manual" });
|
|
381
384
|
}
|
|
385
|
+
export function clearAgentSelectionStorage() {
|
|
386
|
+
if (REPO_ID)
|
|
387
|
+
return;
|
|
388
|
+
safeSetStorage(STORAGE_KEYS.selected, "");
|
|
389
|
+
const candidates = new Set([
|
|
390
|
+
...agentList.map((agent) => agent.id),
|
|
391
|
+
...FALLBACK_AGENTS.map((agent) => agent.id),
|
|
392
|
+
]);
|
|
393
|
+
candidates.forEach((agentId) => {
|
|
394
|
+
safeSetStorage(STORAGE_KEYS.model(agentId), "");
|
|
395
|
+
safeSetStorage(STORAGE_KEYS.reasoning(agentId), "");
|
|
396
|
+
});
|
|
397
|
+
}
|
codex_autorunner/static/app.js
CHANGED
|
@@ -8,12 +8,115 @@ import { initMessages, initMessageBell } from "./messages.js";
|
|
|
8
8
|
import { initMobileCompact } from "./mobileCompact.js";
|
|
9
9
|
import { subscribe } from "./bus.js";
|
|
10
10
|
import { initRepoSettingsPanel, openRepoSettings } from "./settings.js";
|
|
11
|
-
import { flash } from "./utils.js";
|
|
11
|
+
import { flash, getAuthToken, repairModalBackgroundIfStuck, resolvePath, updateUrlParams, } from "./utils.js";
|
|
12
12
|
import { initLiveUpdates } from "./liveUpdates.js";
|
|
13
13
|
import { initHealthGate } from "./health.js";
|
|
14
14
|
import { initWorkspace } from "./workspace.js";
|
|
15
15
|
import { initDashboard } from "./dashboard.js";
|
|
16
16
|
import { initArchive } from "./archive.js";
|
|
17
|
+
import { initPMA } from "./pma.js";
|
|
18
|
+
import { initNotifications } from "./notifications.js";
|
|
19
|
+
let pmaInitialized = false;
|
|
20
|
+
async function initPMAView() {
|
|
21
|
+
if (!pmaInitialized) {
|
|
22
|
+
await initPMA();
|
|
23
|
+
pmaInitialized = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function showHubView() {
|
|
27
|
+
const hubShell = document.getElementById("hub-shell");
|
|
28
|
+
const pmaShell = document.getElementById("pma-shell");
|
|
29
|
+
if (hubShell)
|
|
30
|
+
hubShell.classList.remove("hidden");
|
|
31
|
+
if (pmaShell)
|
|
32
|
+
pmaShell.classList.add("hidden");
|
|
33
|
+
updateModeToggle("manual");
|
|
34
|
+
updateUrlParams({ view: null });
|
|
35
|
+
}
|
|
36
|
+
function showPMAView() {
|
|
37
|
+
const hubShell = document.getElementById("hub-shell");
|
|
38
|
+
const pmaShell = document.getElementById("pma-shell");
|
|
39
|
+
if (hubShell)
|
|
40
|
+
hubShell.classList.add("hidden");
|
|
41
|
+
if (pmaShell)
|
|
42
|
+
pmaShell.classList.remove("hidden");
|
|
43
|
+
updateModeToggle("pma");
|
|
44
|
+
void initPMAView();
|
|
45
|
+
updateUrlParams({ view: "pma" });
|
|
46
|
+
}
|
|
47
|
+
function updateModeToggle(mode) {
|
|
48
|
+
const manualBtns = document.querySelectorAll('[data-hub-mode="manual"]');
|
|
49
|
+
const pmaBtns = document.querySelectorAll('[data-hub-mode="pma"]');
|
|
50
|
+
manualBtns.forEach((btn) => {
|
|
51
|
+
const active = mode === "manual";
|
|
52
|
+
btn.classList.toggle("active", active);
|
|
53
|
+
btn.setAttribute("aria-selected", active ? "true" : "false");
|
|
54
|
+
});
|
|
55
|
+
pmaBtns.forEach((btn) => {
|
|
56
|
+
const active = mode === "pma";
|
|
57
|
+
btn.classList.toggle("active", active);
|
|
58
|
+
btn.setAttribute("aria-selected", active ? "true" : "false");
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async function probePMAEnabled() {
|
|
62
|
+
const headers = {};
|
|
63
|
+
const token = getAuthToken();
|
|
64
|
+
if (token) {
|
|
65
|
+
headers.Authorization = `Bearer ${token}`;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const res = await fetch(resolvePath("/hub/pma/agents"), {
|
|
69
|
+
method: "GET",
|
|
70
|
+
headers,
|
|
71
|
+
});
|
|
72
|
+
return res.ok;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function initHubShell() {
|
|
79
|
+
const hubShell = document.getElementById("hub-shell");
|
|
80
|
+
const repoShell = document.getElementById("repo-shell");
|
|
81
|
+
const manualBtns = Array.from(document.querySelectorAll('[data-hub-mode="manual"]'));
|
|
82
|
+
const pmaBtns = Array.from(document.querySelectorAll('[data-hub-mode="pma"]'));
|
|
83
|
+
if (hubShell)
|
|
84
|
+
hubShell.classList.remove("hidden");
|
|
85
|
+
if (repoShell)
|
|
86
|
+
repoShell.classList.add("hidden");
|
|
87
|
+
initHub();
|
|
88
|
+
initNotifications();
|
|
89
|
+
manualBtns.forEach((btn) => {
|
|
90
|
+
btn.addEventListener("click", () => {
|
|
91
|
+
showHubView();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
pmaBtns.forEach((btn) => {
|
|
95
|
+
btn.addEventListener("click", () => {
|
|
96
|
+
showPMAView();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
100
|
+
const requestedPMA = urlParams.get("view") === "pma";
|
|
101
|
+
const pmaEnabled = await probePMAEnabled();
|
|
102
|
+
if (!pmaEnabled) {
|
|
103
|
+
pmaBtns.forEach((btn) => {
|
|
104
|
+
btn.disabled = true;
|
|
105
|
+
btn.setAttribute("aria-disabled", "true");
|
|
106
|
+
btn.title = "Enable PMA in config to use Project Manager";
|
|
107
|
+
btn.classList.add("hidden");
|
|
108
|
+
btn.classList.remove("active");
|
|
109
|
+
btn.setAttribute("aria-selected", "false");
|
|
110
|
+
});
|
|
111
|
+
if (requestedPMA) {
|
|
112
|
+
showHubView();
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (requestedPMA) {
|
|
117
|
+
showPMAView();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
17
120
|
async function initRepoShell() {
|
|
18
121
|
await initHealthGate();
|
|
19
122
|
if (REPO_ID) {
|
|
@@ -88,22 +191,23 @@ async function initRepoShell() {
|
|
|
88
191
|
if (repoShell?.hasAttribute("inert")) {
|
|
89
192
|
const openModals = document.querySelectorAll(".modal-overlay:not([hidden])");
|
|
90
193
|
const count = openModals.length;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
194
|
+
if (!count && repairModalBackgroundIfStuck()) {
|
|
195
|
+
flash("Recovered from stuck modal state (UI was inert).", "info");
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
flash(count
|
|
199
|
+
? `UI inert: ${count} modal${count === 1 ? "" : "s"} open`
|
|
200
|
+
: "UI inert but no modal is visible", "error");
|
|
201
|
+
}
|
|
94
202
|
}
|
|
95
203
|
}
|
|
96
204
|
function bootstrap() {
|
|
97
|
-
const hubShell = document.getElementById("hub-shell");
|
|
98
|
-
const repoShell = document.getElementById("repo-shell");
|
|
99
205
|
if (!REPO_ID) {
|
|
100
|
-
|
|
101
|
-
hubShell.classList.remove("hidden");
|
|
102
|
-
if (repoShell)
|
|
103
|
-
repoShell.classList.add("hidden");
|
|
104
|
-
initHub();
|
|
206
|
+
void initHubShell();
|
|
105
207
|
return;
|
|
106
208
|
}
|
|
209
|
+
const hubShell = document.getElementById("hub-shell");
|
|
210
|
+
const repoShell = document.getElementById("repo-shell");
|
|
107
211
|
if (repoShell)
|
|
108
212
|
repoShell.classList.remove("hidden");
|
|
109
213
|
if (hubShell)
|