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,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Repository run control routes: start, stop, resume, reset, kill.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
8
|
+
|
|
9
|
+
from ....core.runtime import LockError, clear_stale_lock
|
|
10
|
+
from ....core.state import RunnerState, load_state, now_iso, save_state, state_lock
|
|
11
|
+
from ..schemas import (
|
|
12
|
+
RunControlRequest,
|
|
13
|
+
RunControlResponse,
|
|
14
|
+
RunResetResponse,
|
|
15
|
+
RunStatusResponse,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _normalize_override(value: Optional[str]) -> Optional[str]:
|
|
20
|
+
if not isinstance(value, str):
|
|
21
|
+
return None
|
|
22
|
+
trimmed = value.strip()
|
|
23
|
+
return trimmed or None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _apply_run_overrides(request: Request, payload: RunControlRequest) -> None:
|
|
27
|
+
engine = request.app.state.engine
|
|
28
|
+
agent = _normalize_override(payload.agent)
|
|
29
|
+
model = _normalize_override(payload.model)
|
|
30
|
+
reasoning = _normalize_override(payload.reasoning)
|
|
31
|
+
fields_set = getattr(payload, "model_fields_set", set())
|
|
32
|
+
agent_set = "agent" in fields_set
|
|
33
|
+
model_set = "model" in fields_set
|
|
34
|
+
reasoning_set = "reasoning" in fields_set
|
|
35
|
+
if not (agent_set or model_set or reasoning_set):
|
|
36
|
+
return
|
|
37
|
+
with state_lock(engine.state_path):
|
|
38
|
+
state = load_state(engine.state_path)
|
|
39
|
+
new_state = RunnerState(
|
|
40
|
+
last_run_id=state.last_run_id,
|
|
41
|
+
status=state.status,
|
|
42
|
+
last_exit_code=state.last_exit_code,
|
|
43
|
+
last_run_started_at=state.last_run_started_at,
|
|
44
|
+
last_run_finished_at=state.last_run_finished_at,
|
|
45
|
+
autorunner_agent_override=(
|
|
46
|
+
agent if agent_set else state.autorunner_agent_override
|
|
47
|
+
),
|
|
48
|
+
autorunner_model_override=(
|
|
49
|
+
model if model_set else state.autorunner_model_override
|
|
50
|
+
),
|
|
51
|
+
autorunner_effort_override=(
|
|
52
|
+
reasoning if reasoning_set else state.autorunner_effort_override
|
|
53
|
+
),
|
|
54
|
+
autorunner_approval_policy=state.autorunner_approval_policy,
|
|
55
|
+
autorunner_sandbox_mode=state.autorunner_sandbox_mode,
|
|
56
|
+
autorunner_workspace_write_network=state.autorunner_workspace_write_network,
|
|
57
|
+
runner_pid=state.runner_pid,
|
|
58
|
+
sessions=state.sessions,
|
|
59
|
+
repo_to_session=state.repo_to_session,
|
|
60
|
+
)
|
|
61
|
+
save_state(engine.state_path, new_state)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def build_repos_routes() -> APIRouter:
|
|
65
|
+
"""Build routes for run control."""
|
|
66
|
+
router = APIRouter()
|
|
67
|
+
|
|
68
|
+
@router.post("/api/run/start", response_model=RunControlResponse)
|
|
69
|
+
def start_run(request: Request, payload: Optional[RunControlRequest] = None):
|
|
70
|
+
manager = request.app.state.manager
|
|
71
|
+
logger = request.app.state.logger
|
|
72
|
+
once = payload.once if payload else False
|
|
73
|
+
try:
|
|
74
|
+
logger.info("run/start once=%s", once)
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
if payload:
|
|
78
|
+
_apply_run_overrides(request, payload)
|
|
79
|
+
try:
|
|
80
|
+
manager.start(once=once)
|
|
81
|
+
except LockError as exc:
|
|
82
|
+
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
83
|
+
return {"running": manager.running, "once": once}
|
|
84
|
+
|
|
85
|
+
@router.post("/api/run/stop", response_model=RunStatusResponse)
|
|
86
|
+
def stop_run(request: Request):
|
|
87
|
+
manager = request.app.state.manager
|
|
88
|
+
logger = request.app.state.logger
|
|
89
|
+
try:
|
|
90
|
+
logger.info("run/stop requested")
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
manager.stop()
|
|
94
|
+
return {"running": manager.running}
|
|
95
|
+
|
|
96
|
+
@router.post("/api/run/kill", response_model=RunStatusResponse)
|
|
97
|
+
def kill_run(request: Request):
|
|
98
|
+
engine = request.app.state.engine
|
|
99
|
+
manager = request.app.state.manager
|
|
100
|
+
logger = request.app.state.logger
|
|
101
|
+
try:
|
|
102
|
+
logger.info("run/kill requested")
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
manager.kill()
|
|
106
|
+
with state_lock(engine.state_path):
|
|
107
|
+
state = load_state(engine.state_path)
|
|
108
|
+
new_state = RunnerState(
|
|
109
|
+
last_run_id=state.last_run_id,
|
|
110
|
+
status="error",
|
|
111
|
+
last_exit_code=137,
|
|
112
|
+
last_run_started_at=state.last_run_started_at,
|
|
113
|
+
last_run_finished_at=now_iso(),
|
|
114
|
+
autorunner_agent_override=state.autorunner_agent_override,
|
|
115
|
+
autorunner_model_override=state.autorunner_model_override,
|
|
116
|
+
autorunner_effort_override=state.autorunner_effort_override,
|
|
117
|
+
autorunner_approval_policy=state.autorunner_approval_policy,
|
|
118
|
+
autorunner_sandbox_mode=state.autorunner_sandbox_mode,
|
|
119
|
+
autorunner_workspace_write_network=state.autorunner_workspace_write_network,
|
|
120
|
+
runner_pid=None,
|
|
121
|
+
sessions=state.sessions,
|
|
122
|
+
repo_to_session=state.repo_to_session,
|
|
123
|
+
)
|
|
124
|
+
save_state(engine.state_path, new_state)
|
|
125
|
+
clear_stale_lock(engine.lock_path)
|
|
126
|
+
engine.reconcile_run_index()
|
|
127
|
+
return {"running": manager.running}
|
|
128
|
+
|
|
129
|
+
@router.post("/api/run/clear-lock", response_model=RunStatusResponse)
|
|
130
|
+
def clear_lock(request: Request):
|
|
131
|
+
manager = request.app.state.manager
|
|
132
|
+
logger = request.app.state.logger
|
|
133
|
+
try:
|
|
134
|
+
logger.info("run/clear-lock requested")
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
assessment = manager.clear_freeable_lock()
|
|
138
|
+
if not assessment.freeable:
|
|
139
|
+
detail = "Lock is still active; cannot clear."
|
|
140
|
+
if assessment.pid:
|
|
141
|
+
detail = f"Lock pid {assessment.pid} is still active; cannot clear."
|
|
142
|
+
raise HTTPException(status_code=409, detail=detail)
|
|
143
|
+
return {"running": manager.running}
|
|
144
|
+
|
|
145
|
+
@router.post("/api/run/resume", response_model=RunControlResponse)
|
|
146
|
+
def resume_run(request: Request, payload: Optional[RunControlRequest] = None):
|
|
147
|
+
manager = request.app.state.manager
|
|
148
|
+
logger = request.app.state.logger
|
|
149
|
+
once = payload.once if payload else False
|
|
150
|
+
try:
|
|
151
|
+
logger.info("run/resume once=%s", once)
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
try:
|
|
155
|
+
manager.resume(once=once)
|
|
156
|
+
except LockError as exc:
|
|
157
|
+
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
158
|
+
return {"running": manager.running, "once": once}
|
|
159
|
+
|
|
160
|
+
@router.post("/api/run/reset", response_model=RunResetResponse)
|
|
161
|
+
def reset_runner(request: Request):
|
|
162
|
+
engine = request.app.state.engine
|
|
163
|
+
manager = request.app.state.manager
|
|
164
|
+
logger = request.app.state.logger
|
|
165
|
+
if manager.running:
|
|
166
|
+
raise HTTPException(
|
|
167
|
+
status_code=409, detail="Cannot reset while runner is active"
|
|
168
|
+
)
|
|
169
|
+
try:
|
|
170
|
+
logger.info("run/reset requested")
|
|
171
|
+
except Exception:
|
|
172
|
+
pass
|
|
173
|
+
with state_lock(engine.state_path):
|
|
174
|
+
current_state = load_state(engine.state_path)
|
|
175
|
+
engine.lock_path.unlink(missing_ok=True)
|
|
176
|
+
initial_state = RunnerState(
|
|
177
|
+
last_run_id=None,
|
|
178
|
+
status="idle",
|
|
179
|
+
last_exit_code=None,
|
|
180
|
+
last_run_started_at=None,
|
|
181
|
+
last_run_finished_at=None,
|
|
182
|
+
autorunner_agent_override=current_state.autorunner_agent_override,
|
|
183
|
+
autorunner_model_override=current_state.autorunner_model_override,
|
|
184
|
+
autorunner_effort_override=current_state.autorunner_effort_override,
|
|
185
|
+
autorunner_approval_policy=current_state.autorunner_approval_policy,
|
|
186
|
+
autorunner_sandbox_mode=current_state.autorunner_sandbox_mode,
|
|
187
|
+
autorunner_workspace_write_network=current_state.autorunner_workspace_write_network,
|
|
188
|
+
runner_pid=None,
|
|
189
|
+
sessions=current_state.sessions,
|
|
190
|
+
repo_to_session=current_state.repo_to_session,
|
|
191
|
+
)
|
|
192
|
+
save_state(engine.state_path, initial_state)
|
|
193
|
+
if engine.log_path.exists():
|
|
194
|
+
engine.log_path.unlink()
|
|
195
|
+
return {"status": "ok", "message": "Runner reset complete"}
|
|
196
|
+
|
|
197
|
+
return router
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Review workflow routes.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException, Query, Request
|
|
8
|
+
from fastapi.responses import FileResponse
|
|
9
|
+
|
|
10
|
+
from ..review import ReviewBusyError, ReviewError, ReviewService
|
|
11
|
+
from ..schemas import (
|
|
12
|
+
ReviewControlResponse,
|
|
13
|
+
ReviewStartRequest,
|
|
14
|
+
ReviewStatusResponse,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _review(request: Request) -> ReviewService:
|
|
19
|
+
"""Get a ReviewService instance from request."""
|
|
20
|
+
manager = getattr(request.app.state, "review_manager", None)
|
|
21
|
+
if manager is None:
|
|
22
|
+
engine = request.app.state.engine
|
|
23
|
+
manager = ReviewService(
|
|
24
|
+
engine,
|
|
25
|
+
app_server_supervisor=getattr(
|
|
26
|
+
request.app.state, "app_server_supervisor", None
|
|
27
|
+
),
|
|
28
|
+
opencode_supervisor=getattr(request.app.state, "opencode_supervisor", None),
|
|
29
|
+
logger=getattr(request.app.state, "logger", None),
|
|
30
|
+
)
|
|
31
|
+
request.app.state.review_manager = manager
|
|
32
|
+
return manager
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build_review_routes() -> APIRouter:
|
|
36
|
+
"""Build routes for review workflow."""
|
|
37
|
+
router = APIRouter()
|
|
38
|
+
|
|
39
|
+
@router.get("/api/review/status")
|
|
40
|
+
async def review_status(request: Request):
|
|
41
|
+
try:
|
|
42
|
+
service = _review(request)
|
|
43
|
+
status = service.status()
|
|
44
|
+
return ReviewStatusResponse(review=status)
|
|
45
|
+
except ReviewError as exc:
|
|
46
|
+
raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
|
|
47
|
+
except Exception as exc:
|
|
48
|
+
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
49
|
+
|
|
50
|
+
@router.post("/api/review/start")
|
|
51
|
+
async def review_start(request: Request, payload: ReviewStartRequest):
|
|
52
|
+
try:
|
|
53
|
+
service = _review(request)
|
|
54
|
+
state = service.start(payload=payload.model_dump(exclude_none=True))
|
|
55
|
+
return ReviewControlResponse(
|
|
56
|
+
status=state.get("status", "unknown"),
|
|
57
|
+
detail="Review started",
|
|
58
|
+
)
|
|
59
|
+
except ReviewBusyError as exc:
|
|
60
|
+
raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
|
|
61
|
+
except ReviewError as exc:
|
|
62
|
+
raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
|
|
63
|
+
except Exception as exc:
|
|
64
|
+
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
65
|
+
|
|
66
|
+
@router.post("/api/review/stop")
|
|
67
|
+
async def review_stop(request: Request):
|
|
68
|
+
try:
|
|
69
|
+
service = _review(request)
|
|
70
|
+
state = service.stop()
|
|
71
|
+
return ReviewControlResponse(
|
|
72
|
+
status=state.get("status", "unknown"),
|
|
73
|
+
detail="Review stopped",
|
|
74
|
+
)
|
|
75
|
+
except ReviewError as exc:
|
|
76
|
+
raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
|
|
77
|
+
except Exception as exc:
|
|
78
|
+
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
79
|
+
|
|
80
|
+
@router.post("/api/review/reset")
|
|
81
|
+
async def review_reset(request: Request):
|
|
82
|
+
try:
|
|
83
|
+
service = _review(request)
|
|
84
|
+
state = service.reset()
|
|
85
|
+
return ReviewControlResponse(
|
|
86
|
+
status=state.get("status", "idle"),
|
|
87
|
+
detail="Review state reset",
|
|
88
|
+
)
|
|
89
|
+
except ReviewBusyError as exc:
|
|
90
|
+
raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
|
|
91
|
+
except ReviewError as exc:
|
|
92
|
+
raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
95
|
+
|
|
96
|
+
@router.get("/api/review/artifact")
|
|
97
|
+
async def review_artifact(
|
|
98
|
+
request: Request,
|
|
99
|
+
kind: str = Query(
|
|
100
|
+
..., description="final_report|workflow_log|scratchpad_bundle"
|
|
101
|
+
),
|
|
102
|
+
):
|
|
103
|
+
try:
|
|
104
|
+
service = _review(request)
|
|
105
|
+
status = service.status()
|
|
106
|
+
|
|
107
|
+
mapping = {
|
|
108
|
+
"final_report": status.get("final_output_path"),
|
|
109
|
+
"workflow_log": status.get("run_dir"),
|
|
110
|
+
"scratchpad_bundle": status.get("scratchpad_bundle_path"),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
raw_path = mapping.get(kind)
|
|
114
|
+
if not raw_path:
|
|
115
|
+
raise HTTPException(status_code=404, detail="Artifact not found")
|
|
116
|
+
|
|
117
|
+
target = Path(raw_path).expanduser().resolve()
|
|
118
|
+
allowed_root = request.app.state.engine.repo_root.resolve()
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
target.relative_to(allowed_root)
|
|
122
|
+
if ".codex-autorunner" not in target.parts:
|
|
123
|
+
raise HTTPException(status_code=403, detail="Access denied")
|
|
124
|
+
except ValueError:
|
|
125
|
+
raise HTTPException(status_code=403, detail="Access denied") from None
|
|
126
|
+
|
|
127
|
+
if not target.exists():
|
|
128
|
+
raise HTTPException(status_code=404, detail="Artifact not found")
|
|
129
|
+
|
|
130
|
+
if kind == "workflow_log" and target.is_dir():
|
|
131
|
+
target = target / "review.log"
|
|
132
|
+
|
|
133
|
+
media_type = "text/plain"
|
|
134
|
+
if target.suffix == ".md":
|
|
135
|
+
media_type = "text/markdown"
|
|
136
|
+
elif target.suffix == ".zip":
|
|
137
|
+
media_type = "application/zip"
|
|
138
|
+
|
|
139
|
+
return FileResponse(target, media_type=media_type, filename=target.name)
|
|
140
|
+
|
|
141
|
+
except ReviewError as exc:
|
|
142
|
+
raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
|
|
143
|
+
except HTTPException:
|
|
144
|
+
raise
|
|
145
|
+
except Exception as exc:
|
|
146
|
+
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
147
|
+
|
|
148
|
+
return router
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Terminal session registry routes.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
10
|
+
|
|
11
|
+
from ....core.state import persist_session_registry
|
|
12
|
+
from ..schemas import (
|
|
13
|
+
SessionsResponse,
|
|
14
|
+
SessionStopRequest,
|
|
15
|
+
SessionStopResponse,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("codex_autorunner.routes.sessions")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _relative_repo_path(repo_path: str, repo_root: Path) -> str:
|
|
22
|
+
path = Path(repo_path)
|
|
23
|
+
if not path.is_absolute():
|
|
24
|
+
return repo_path
|
|
25
|
+
try:
|
|
26
|
+
rel = path.resolve().relative_to(repo_root)
|
|
27
|
+
return rel.as_posix() or "."
|
|
28
|
+
except ValueError as exc:
|
|
29
|
+
logger.debug("Failed to resolve relative path: %s", exc)
|
|
30
|
+
return path.name
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _relative_repo_key(repo_key: str, repo_root: Path) -> str:
|
|
34
|
+
"""
|
|
35
|
+
Format repo_to_session keys for display in API responses.
|
|
36
|
+
|
|
37
|
+
Keys are either:
|
|
38
|
+
- `<repo_path>` for the default codex agent (backwards compatible)
|
|
39
|
+
- `<repo_path>:<agent>` for non-default agents (e.g. opencode)
|
|
40
|
+
"""
|
|
41
|
+
if ":" not in repo_key:
|
|
42
|
+
return _relative_repo_path(repo_key, repo_root)
|
|
43
|
+
repo_path, agent = repo_key.split(":", 1)
|
|
44
|
+
rel = _relative_repo_path(repo_path, repo_root)
|
|
45
|
+
agent = agent.strip().lower()
|
|
46
|
+
if not agent or agent == "codex":
|
|
47
|
+
return rel
|
|
48
|
+
return f"{rel}:{agent}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _allow_abs_paths(request: Request, include_abs_paths: bool) -> bool:
|
|
52
|
+
if not include_abs_paths:
|
|
53
|
+
return False
|
|
54
|
+
return bool(getattr(request.app.state, "auth_token", None))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _session_payload(
|
|
58
|
+
session_id: str,
|
|
59
|
+
record,
|
|
60
|
+
terminal_sessions: dict,
|
|
61
|
+
repo_root: Path,
|
|
62
|
+
include_abs_paths: bool,
|
|
63
|
+
) -> dict:
|
|
64
|
+
active = terminal_sessions.get(session_id)
|
|
65
|
+
alive = bool(active and active.pty.isalive())
|
|
66
|
+
payload = {
|
|
67
|
+
"session_id": session_id,
|
|
68
|
+
"repo_path": _relative_repo_path(record.repo_path, repo_root),
|
|
69
|
+
"created_at": record.created_at,
|
|
70
|
+
"last_seen_at": record.last_seen_at,
|
|
71
|
+
"status": record.status,
|
|
72
|
+
"alive": alive,
|
|
73
|
+
}
|
|
74
|
+
if include_abs_paths:
|
|
75
|
+
payload["abs_repo_path"] = record.repo_path
|
|
76
|
+
return payload
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def build_sessions_routes() -> APIRouter:
|
|
80
|
+
router = APIRouter()
|
|
81
|
+
|
|
82
|
+
@router.get("/api/sessions", response_model=SessionsResponse)
|
|
83
|
+
def list_sessions(request: Request, include_abs_paths: bool = False):
|
|
84
|
+
terminal_sessions = request.app.state.terminal_sessions
|
|
85
|
+
session_registry = request.app.state.session_registry
|
|
86
|
+
repo_to_session = request.app.state.repo_to_session
|
|
87
|
+
repo_root = Path(request.app.state.engine.repo_root)
|
|
88
|
+
allow_abs = _allow_abs_paths(request, include_abs_paths)
|
|
89
|
+
sessions = [
|
|
90
|
+
_session_payload(
|
|
91
|
+
session_id, record, terminal_sessions, repo_root, allow_abs
|
|
92
|
+
)
|
|
93
|
+
for session_id, record in session_registry.items()
|
|
94
|
+
]
|
|
95
|
+
repo_to_session_payload = {
|
|
96
|
+
_relative_repo_key(repo_key, repo_root): session_id
|
|
97
|
+
for repo_key, session_id in repo_to_session.items()
|
|
98
|
+
}
|
|
99
|
+
payload = {
|
|
100
|
+
"sessions": sessions,
|
|
101
|
+
"repo_to_session": repo_to_session_payload,
|
|
102
|
+
}
|
|
103
|
+
if allow_abs:
|
|
104
|
+
payload["abs_repo_to_session"] = dict(repo_to_session)
|
|
105
|
+
return {
|
|
106
|
+
**payload,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@router.post("/api/sessions/stop", response_model=SessionStopResponse)
|
|
110
|
+
async def stop_session(request: Request, payload: SessionStopRequest):
|
|
111
|
+
session_id = payload.session_id
|
|
112
|
+
repo_path = payload.repo_path
|
|
113
|
+
if not session_id and not repo_path:
|
|
114
|
+
raise HTTPException(
|
|
115
|
+
status_code=400, detail="Provide session_id or repo_path"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
terminal_sessions = request.app.state.terminal_sessions
|
|
119
|
+
session_registry = request.app.state.session_registry
|
|
120
|
+
repo_to_session = request.app.state.repo_to_session
|
|
121
|
+
terminal_lock = request.app.state.terminal_lock
|
|
122
|
+
engine = request.app.state.engine
|
|
123
|
+
|
|
124
|
+
if repo_path and isinstance(repo_path, str):
|
|
125
|
+
repo_root = Path(request.app.state.engine.repo_root)
|
|
126
|
+
normalized_repo_path = repo_path.strip()
|
|
127
|
+
if normalized_repo_path:
|
|
128
|
+
raw_path = Path(normalized_repo_path)
|
|
129
|
+
try:
|
|
130
|
+
# Reject absolute paths outright to prevent symlink traversal attacks
|
|
131
|
+
if raw_path.is_absolute():
|
|
132
|
+
raise ValueError("Absolute paths are not allowed")
|
|
133
|
+
# Only process relative paths, join with repo_root and resolve
|
|
134
|
+
resolved = (repo_root / raw_path).resolve()
|
|
135
|
+
# Verify the resolved path is still under repo_root
|
|
136
|
+
resolved.relative_to(repo_root)
|
|
137
|
+
except (OSError, RuntimeError, ValueError):
|
|
138
|
+
# On any resolution or containment failure, treat as invalid
|
|
139
|
+
normalized_repo_path = ""
|
|
140
|
+
else:
|
|
141
|
+
normalized_repo_path = str(resolved)
|
|
142
|
+
candidates: list[str] = []
|
|
143
|
+
if normalized_repo_path:
|
|
144
|
+
candidates.extend(
|
|
145
|
+
[normalized_repo_path, f"{normalized_repo_path}:opencode"]
|
|
146
|
+
)
|
|
147
|
+
for key in candidates:
|
|
148
|
+
mapped = repo_to_session.get(key)
|
|
149
|
+
if mapped:
|
|
150
|
+
session_id = mapped
|
|
151
|
+
break
|
|
152
|
+
if not isinstance(session_id, str) or not session_id:
|
|
153
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
154
|
+
if session_id not in session_registry and session_id not in terminal_sessions:
|
|
155
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
156
|
+
|
|
157
|
+
async with terminal_lock:
|
|
158
|
+
session = terminal_sessions.get(session_id)
|
|
159
|
+
if session:
|
|
160
|
+
session.close()
|
|
161
|
+
await session.wait_closed()
|
|
162
|
+
terminal_sessions.pop(session_id, None)
|
|
163
|
+
session_registry.pop(session_id, None)
|
|
164
|
+
repo_to_session = {
|
|
165
|
+
repo: sid for repo, sid in repo_to_session.items() if sid != session_id
|
|
166
|
+
}
|
|
167
|
+
request.app.state.repo_to_session = repo_to_session
|
|
168
|
+
persist_session_registry(
|
|
169
|
+
engine.state_path, session_registry, repo_to_session
|
|
170
|
+
)
|
|
171
|
+
request.app.state.session_state_last_write = time.time()
|
|
172
|
+
request.app.state.session_state_dirty = False
|
|
173
|
+
|
|
174
|
+
return {"status": "stopped", "session_id": session_id}
|
|
175
|
+
|
|
176
|
+
return router
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session settings routes for autorunner overrides.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
8
|
+
|
|
9
|
+
from ....core.state import RunnerState, load_state, save_state, state_lock
|
|
10
|
+
from ..schemas import SessionSettingsRequest, SessionSettingsResponse
|
|
11
|
+
|
|
12
|
+
ALLOWED_APPROVAL_POLICIES = {"never", "unlessTrusted"}
|
|
13
|
+
ALLOWED_SANDBOX_MODES = {"dangerFullAccess", "workspaceWrite"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _normalize_optional_string(value: object, field: str) -> Optional[str]:
|
|
17
|
+
if value is None:
|
|
18
|
+
return None
|
|
19
|
+
if not isinstance(value, str):
|
|
20
|
+
raise HTTPException(status_code=400, detail=f"{field} must be a string")
|
|
21
|
+
cleaned = value.strip()
|
|
22
|
+
return cleaned or None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_settings_routes() -> APIRouter:
|
|
26
|
+
router = APIRouter()
|
|
27
|
+
|
|
28
|
+
@router.get("/api/session/settings", response_model=SessionSettingsResponse)
|
|
29
|
+
def get_session_settings(request: Request):
|
|
30
|
+
state = load_state(request.app.state.engine.state_path)
|
|
31
|
+
return {
|
|
32
|
+
"autorunner_model_override": state.autorunner_model_override,
|
|
33
|
+
"autorunner_effort_override": state.autorunner_effort_override,
|
|
34
|
+
"autorunner_approval_policy": state.autorunner_approval_policy,
|
|
35
|
+
"autorunner_sandbox_mode": state.autorunner_sandbox_mode,
|
|
36
|
+
"autorunner_workspace_write_network": state.autorunner_workspace_write_network,
|
|
37
|
+
"runner_stop_after_runs": state.runner_stop_after_runs,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@router.post("/api/session/settings", response_model=SessionSettingsResponse)
|
|
41
|
+
def update_session_settings(request: Request, payload: SessionSettingsRequest):
|
|
42
|
+
updates = payload.model_dump(exclude_unset=True)
|
|
43
|
+
engine = request.app.state.engine
|
|
44
|
+
manager = request.app.state.manager
|
|
45
|
+
registry = request.app.state.app_server_threads
|
|
46
|
+
with state_lock(engine.state_path):
|
|
47
|
+
state = load_state(engine.state_path)
|
|
48
|
+
model_override = (
|
|
49
|
+
_normalize_optional_string(
|
|
50
|
+
updates.get("autorunner_model_override"),
|
|
51
|
+
"autorunner_model_override",
|
|
52
|
+
)
|
|
53
|
+
if "autorunner_model_override" in updates
|
|
54
|
+
else state.autorunner_model_override
|
|
55
|
+
)
|
|
56
|
+
effort_override = (
|
|
57
|
+
_normalize_optional_string(
|
|
58
|
+
updates.get("autorunner_effort_override"),
|
|
59
|
+
"autorunner_effort_override",
|
|
60
|
+
)
|
|
61
|
+
if "autorunner_effort_override" in updates
|
|
62
|
+
else state.autorunner_effort_override
|
|
63
|
+
)
|
|
64
|
+
approval_policy = (
|
|
65
|
+
_normalize_optional_string(
|
|
66
|
+
updates.get("autorunner_approval_policy"),
|
|
67
|
+
"autorunner_approval_policy",
|
|
68
|
+
)
|
|
69
|
+
if "autorunner_approval_policy" in updates
|
|
70
|
+
else state.autorunner_approval_policy
|
|
71
|
+
)
|
|
72
|
+
if approval_policy and approval_policy not in ALLOWED_APPROVAL_POLICIES:
|
|
73
|
+
raise HTTPException(
|
|
74
|
+
status_code=400,
|
|
75
|
+
detail="approval policy must be never or unlessTrusted",
|
|
76
|
+
)
|
|
77
|
+
sandbox_mode = (
|
|
78
|
+
_normalize_optional_string(
|
|
79
|
+
updates.get("autorunner_sandbox_mode"),
|
|
80
|
+
"autorunner_sandbox_mode",
|
|
81
|
+
)
|
|
82
|
+
if "autorunner_sandbox_mode" in updates
|
|
83
|
+
else state.autorunner_sandbox_mode
|
|
84
|
+
)
|
|
85
|
+
if sandbox_mode and sandbox_mode not in ALLOWED_SANDBOX_MODES:
|
|
86
|
+
raise HTTPException(
|
|
87
|
+
status_code=400,
|
|
88
|
+
detail="sandbox mode must be dangerFullAccess or workspaceWrite",
|
|
89
|
+
)
|
|
90
|
+
workspace_write_network = (
|
|
91
|
+
updates.get("autorunner_workspace_write_network")
|
|
92
|
+
if "autorunner_workspace_write_network" in updates
|
|
93
|
+
else state.autorunner_workspace_write_network
|
|
94
|
+
)
|
|
95
|
+
if (
|
|
96
|
+
"autorunner_workspace_write_network" in updates
|
|
97
|
+
and workspace_write_network is not None
|
|
98
|
+
and not isinstance(workspace_write_network, bool)
|
|
99
|
+
):
|
|
100
|
+
raise HTTPException(
|
|
101
|
+
status_code=400,
|
|
102
|
+
detail="autorunner_workspace_write_network must be a boolean",
|
|
103
|
+
)
|
|
104
|
+
runner_stop_after_runs = (
|
|
105
|
+
updates.get("runner_stop_after_runs")
|
|
106
|
+
if "runner_stop_after_runs" in updates
|
|
107
|
+
else state.runner_stop_after_runs
|
|
108
|
+
)
|
|
109
|
+
if (
|
|
110
|
+
"runner_stop_after_runs" in updates
|
|
111
|
+
and runner_stop_after_runs is not None
|
|
112
|
+
and (
|
|
113
|
+
not isinstance(runner_stop_after_runs, int)
|
|
114
|
+
or isinstance(runner_stop_after_runs, bool)
|
|
115
|
+
or runner_stop_after_runs <= 0
|
|
116
|
+
)
|
|
117
|
+
):
|
|
118
|
+
raise HTTPException(
|
|
119
|
+
status_code=400,
|
|
120
|
+
detail="runner_stop_after_runs must be a positive integer",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
thread_reset_required = any(
|
|
124
|
+
(
|
|
125
|
+
model_override != state.autorunner_model_override,
|
|
126
|
+
effort_override != state.autorunner_effort_override,
|
|
127
|
+
approval_policy != state.autorunner_approval_policy,
|
|
128
|
+
sandbox_mode != state.autorunner_sandbox_mode,
|
|
129
|
+
workspace_write_network != state.autorunner_workspace_write_network,
|
|
130
|
+
runner_stop_after_runs != state.runner_stop_after_runs,
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
if thread_reset_required and manager.running:
|
|
134
|
+
raise HTTPException(
|
|
135
|
+
status_code=409,
|
|
136
|
+
detail="Cannot change autorunner settings while a run is active",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
new_state = RunnerState(
|
|
140
|
+
last_run_id=state.last_run_id,
|
|
141
|
+
status=state.status,
|
|
142
|
+
last_exit_code=state.last_exit_code,
|
|
143
|
+
last_run_started_at=state.last_run_started_at,
|
|
144
|
+
last_run_finished_at=state.last_run_finished_at,
|
|
145
|
+
autorunner_agent_override=state.autorunner_agent_override,
|
|
146
|
+
autorunner_model_override=model_override,
|
|
147
|
+
autorunner_effort_override=effort_override,
|
|
148
|
+
autorunner_approval_policy=approval_policy,
|
|
149
|
+
autorunner_sandbox_mode=sandbox_mode,
|
|
150
|
+
autorunner_workspace_write_network=workspace_write_network,
|
|
151
|
+
runner_stop_after_runs=runner_stop_after_runs,
|
|
152
|
+
runner_pid=state.runner_pid,
|
|
153
|
+
sessions=state.sessions,
|
|
154
|
+
repo_to_session=state.repo_to_session,
|
|
155
|
+
)
|
|
156
|
+
save_state(engine.state_path, new_state)
|
|
157
|
+
if thread_reset_required:
|
|
158
|
+
registry.reset_thread("autorunner")
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
"autorunner_model_override": model_override,
|
|
162
|
+
"autorunner_effort_override": effort_override,
|
|
163
|
+
"autorunner_approval_policy": approval_policy,
|
|
164
|
+
"autorunner_sandbox_mode": sandbox_mode,
|
|
165
|
+
"autorunner_workspace_write_network": workspace_write_network,
|
|
166
|
+
"runner_stop_after_runs": runner_stop_after_runs,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return router
|