codex-autorunner 0.1.2__py3-none-any.whl → 1.0.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/__main__.py +4 -0
- codex_autorunner/agents/opencode/client.py +68 -35
- codex_autorunner/agents/opencode/logging.py +21 -5
- codex_autorunner/agents/opencode/run_prompt.py +1 -0
- codex_autorunner/agents/opencode/runtime.py +118 -30
- codex_autorunner/agents/opencode/supervisor.py +36 -48
- codex_autorunner/agents/registry.py +136 -8
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +16 -35
- codex_autorunner/cli.py +157 -139
- codex_autorunner/core/about_car.py +44 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_logging.py +7 -3
- codex_autorunner/core/app_server_prompts.py +27 -260
- codex_autorunner/core/app_server_threads.py +15 -26
- codex_autorunner/core/codex_runner.py +6 -0
- codex_autorunner/core/config.py +390 -100
- codex_autorunner/core/docs.py +10 -2
- codex_autorunner/core/drafts.py +82 -0
- codex_autorunner/core/engine.py +278 -262
- codex_autorunner/core/flows/__init__.py +25 -0
- codex_autorunner/core/flows/controller.py +178 -0
- codex_autorunner/core/flows/definition.py +82 -0
- codex_autorunner/core/flows/models.py +75 -0
- codex_autorunner/core/flows/runtime.py +351 -0
- codex_autorunner/core/flows/store.py +485 -0
- codex_autorunner/core/flows/transition.py +133 -0
- codex_autorunner/core/flows/worker_process.py +242 -0
- codex_autorunner/core/hub.py +15 -9
- codex_autorunner/core/locks.py +4 -0
- codex_autorunner/core/prompt.py +15 -7
- codex_autorunner/core/redaction.py +29 -0
- codex_autorunner/core/review_context.py +5 -8
- codex_autorunner/core/run_index.py +6 -0
- codex_autorunner/core/runner_process.py +5 -2
- codex_autorunner/core/state.py +0 -88
- codex_autorunner/core/static_assets.py +55 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/update.py +20 -11
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/utils.py +29 -2
- codex_autorunner/discovery.py +2 -4
- codex_autorunner/flows/ticket_flow/__init__.py +3 -0
- codex_autorunner/flows/ticket_flow/definition.py +91 -0
- codex_autorunner/integrations/agents/__init__.py +27 -0
- codex_autorunner/integrations/agents/agent_backend.py +142 -0
- codex_autorunner/integrations/agents/codex_backend.py +307 -0
- codex_autorunner/integrations/agents/opencode_backend.py +325 -0
- codex_autorunner/integrations/agents/run_event.py +71 -0
- codex_autorunner/integrations/app_server/client.py +576 -92
- codex_autorunner/integrations/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +141 -167
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +175 -0
- codex_autorunner/integrations/telegram/constants.py +16 -1
- codex_autorunner/integrations/telegram/dispatch.py +17 -0
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/callbacks.py +0 -4
- codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
- codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
- codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
- codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +133 -475
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +11 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +120 -9
- codex_autorunner/integrations/telegram/helpers.py +88 -16
- codex_autorunner/integrations/telegram/outbox.py +208 -37
- codex_autorunner/integrations/telegram/progress_stream.py +3 -10
- codex_autorunner/integrations/telegram/service.py +214 -40
- codex_autorunner/integrations/telegram/state.py +100 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
- codex_autorunner/integrations/telegram/transport.py +36 -3
- codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
- codex_autorunner/manifest.py +2 -0
- codex_autorunner/plugin_api.py +22 -0
- codex_autorunner/routes/__init__.py +23 -14
- codex_autorunner/routes/analytics.py +239 -0
- codex_autorunner/routes/base.py +81 -109
- codex_autorunner/routes/file_chat.py +836 -0
- codex_autorunner/routes/flows.py +980 -0
- codex_autorunner/routes/messages.py +459 -0
- codex_autorunner/routes/system.py +6 -1
- codex_autorunner/routes/usage.py +87 -0
- codex_autorunner/routes/workspace.py +271 -0
- codex_autorunner/server.py +2 -1
- codex_autorunner/static/agentControls.js +1 -0
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +25 -22
- codex_autorunner/static/autoRefresh.js +29 -1
- codex_autorunner/static/bootstrap.js +1 -0
- codex_autorunner/static/bus.js +1 -0
- codex_autorunner/static/cache.js +1 -0
- codex_autorunner/static/constants.js +20 -4
- codex_autorunner/static/dashboard.js +162 -196
- codex_autorunner/static/diffRenderer.js +37 -0
- codex_autorunner/static/docChatCore.js +324 -0
- codex_autorunner/static/docChatStorage.js +65 -0
- codex_autorunner/static/docChatVoice.js +65 -0
- codex_autorunner/static/docEditor.js +133 -0
- codex_autorunner/static/env.js +1 -0
- codex_autorunner/static/eventSummarizer.js +166 -0
- codex_autorunner/static/fileChat.js +182 -0
- codex_autorunner/static/health.js +155 -0
- codex_autorunner/static/hub.js +41 -118
- codex_autorunner/static/index.html +787 -858
- codex_autorunner/static/liveUpdates.js +1 -0
- codex_autorunner/static/loader.js +1 -0
- codex_autorunner/static/messages.js +470 -0
- codex_autorunner/static/mobileCompact.js +2 -1
- codex_autorunner/static/settings.js +24 -211
- codex_autorunner/static/styles.css +7567 -3865
- codex_autorunner/static/tabs.js +28 -5
- codex_autorunner/static/terminal.js +14 -0
- codex_autorunner/static/terminalManager.js +34 -59
- codex_autorunner/static/ticketChatActions.js +333 -0
- codex_autorunner/static/ticketChatEvents.js +16 -0
- codex_autorunner/static/ticketChatStorage.js +16 -0
- codex_autorunner/static/ticketChatStream.js +264 -0
- codex_autorunner/static/ticketEditor.js +750 -0
- codex_autorunner/static/ticketVoice.js +9 -0
- codex_autorunner/static/tickets.js +1315 -0
- codex_autorunner/static/utils.js +32 -3
- codex_autorunner/static/voice.js +1 -0
- codex_autorunner/static/workspace.js +672 -0
- codex_autorunner/static/workspaceApi.js +53 -0
- codex_autorunner/static/workspaceFileBrowser.js +504 -0
- codex_autorunner/tickets/__init__.py +20 -0
- codex_autorunner/tickets/agent_pool.py +377 -0
- codex_autorunner/tickets/files.py +85 -0
- codex_autorunner/tickets/frontmatter.py +55 -0
- codex_autorunner/tickets/lint.py +102 -0
- codex_autorunner/tickets/models.py +95 -0
- codex_autorunner/tickets/outbox.py +232 -0
- codex_autorunner/tickets/replies.py +179 -0
- codex_autorunner/tickets/runner.py +823 -0
- codex_autorunner/tickets/spec_ingest.py +77 -0
- codex_autorunner/web/app.py +269 -91
- codex_autorunner/web/middleware.py +3 -4
- codex_autorunner/web/schemas.py +89 -109
- codex_autorunner/web/static_assets.py +1 -44
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +319 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +18 -21
- codex_autorunner-1.0.0.dist-info/RECORD +251 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
- codex_autorunner/agents/execution/policy.py +0 -292
- codex_autorunner/agents/factory.py +0 -52
- codex_autorunner/agents/orchestrator.py +0 -358
- codex_autorunner/core/doc_chat.py +0 -1446
- codex_autorunner/core/snapshot.py +0 -580
- codex_autorunner/integrations/github/chatops.py +0 -268
- codex_autorunner/integrations/github/pr_flow.py +0 -1314
- codex_autorunner/routes/docs.py +0 -381
- codex_autorunner/routes/github.py +0 -327
- codex_autorunner/routes/runs.py +0 -250
- codex_autorunner/spec_ingest.py +0 -812
- codex_autorunner/static/docChatActions.js +0 -287
- codex_autorunner/static/docChatEvents.js +0 -300
- codex_autorunner/static/docChatRender.js +0 -205
- codex_autorunner/static/docChatStream.js +0 -361
- codex_autorunner/static/docs.js +0 -20
- codex_autorunner/static/docsClipboard.js +0 -69
- codex_autorunner/static/docsCrud.js +0 -257
- codex_autorunner/static/docsDocUpdates.js +0 -62
- codex_autorunner/static/docsDrafts.js +0 -16
- codex_autorunner/static/docsElements.js +0 -69
- codex_autorunner/static/docsInit.js +0 -285
- codex_autorunner/static/docsParse.js +0 -160
- codex_autorunner/static/docsSnapshot.js +0 -87
- codex_autorunner/static/docsSpecIngest.js +0 -263
- codex_autorunner/static/docsState.js +0 -127
- codex_autorunner/static/docsThreadRegistry.js +0 -44
- codex_autorunner/static/docsUi.js +0 -153
- codex_autorunner/static/docsVoice.js +0 -56
- codex_autorunner/static/github.js +0 -504
- codex_autorunner/static/logs.js +0 -678
- codex_autorunner/static/review.js +0 -157
- codex_autorunner/static/runs.js +0 -418
- codex_autorunner/static/snapshot.js +0 -124
- codex_autorunner/static/state.js +0 -94
- codex_autorunner/static/todoPreview.js +0 -27
- codex_autorunner/workspace.py +0 -16
- codex_autorunner-0.1.2.dist-info/RECORD +0 -222
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from ..workspace.paths import read_workspace_doc, workspace_doc_path
|
|
8
|
+
from .files import list_ticket_paths, safe_relpath
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SpecIngestTicketsError(Exception):
|
|
12
|
+
"""Raised when workspace spec → tickets ingest fails."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class SpecIngestTicketsResult:
|
|
17
|
+
created: int
|
|
18
|
+
first_ticket_path: Optional[str] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _ticket_dir(repo_root: Path) -> Path:
|
|
22
|
+
return repo_root / ".codex-autorunner" / "tickets"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _ticket_path(repo_root: Path, index: int) -> Path:
|
|
26
|
+
return _ticket_dir(repo_root) / f"TICKET-{index:03d}.md"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def ingest_workspace_spec_to_tickets(repo_root: Path) -> SpecIngestTicketsResult:
|
|
30
|
+
"""Generate initial tickets from `.codex-autorunner/workspace/spec.md`.
|
|
31
|
+
|
|
32
|
+
Behavior is intentionally conservative:
|
|
33
|
+
- Refuses to run if any tickets already exist.
|
|
34
|
+
- Writes exactly one bootstrap ticket that asks the agent to break down the spec.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
spec_path = workspace_doc_path(repo_root, "spec")
|
|
38
|
+
spec_text = read_workspace_doc(repo_root, "spec")
|
|
39
|
+
if not spec_text.strip():
|
|
40
|
+
raise SpecIngestTicketsError(
|
|
41
|
+
f"Workspace spec is missing or empty at {safe_relpath(spec_path, repo_root)}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
ticket_dir = _ticket_dir(repo_root)
|
|
45
|
+
existing = list_ticket_paths(ticket_dir)
|
|
46
|
+
if existing:
|
|
47
|
+
raise SpecIngestTicketsError(
|
|
48
|
+
"Tickets already exist; refusing to generate tickets from spec."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
ticket_dir.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
ticket_path = _ticket_path(repo_root, 1)
|
|
53
|
+
|
|
54
|
+
rel_spec = safe_relpath(spec_path, repo_root)
|
|
55
|
+
template = f"""---
|
|
56
|
+
agent: codex
|
|
57
|
+
done: false
|
|
58
|
+
title: Bootstrap tickets from workspace spec
|
|
59
|
+
goal: Read workspace spec and create follow-up tickets
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
You are the first ticket in a workspace-driven workflow.
|
|
63
|
+
|
|
64
|
+
- Read `{rel_spec}`.
|
|
65
|
+
- Break the work into additional `TICKET-00X.md` files under `.codex-autorunner/tickets/`.
|
|
66
|
+
- Keep this ticket open until the follow-up tickets exist and are coherent.
|
|
67
|
+
- Keep tickets small and single-purpose; prefer many small tickets over one big one.
|
|
68
|
+
|
|
69
|
+
When you need ongoing context, you may also consult (optional):
|
|
70
|
+
- `.codex-autorunner/workspace/active_context.md`
|
|
71
|
+
- `.codex-autorunner/workspace/decisions.md`
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
ticket_path.write_text(template, encoding="utf-8")
|
|
75
|
+
return SpecIngestTicketsResult(
|
|
76
|
+
created=1, first_ticket_path=safe_relpath(ticket_path, repo_root)
|
|
77
|
+
)
|
codex_autorunner/web/app.py
CHANGED
|
@@ -11,6 +11,7 @@ from typing import Mapping, Optional
|
|
|
11
11
|
from fastapi import FastAPI, HTTPException
|
|
12
12
|
from fastapi.responses import HTMLResponse
|
|
13
13
|
from fastapi.staticfiles import StaticFiles
|
|
14
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
14
15
|
from starlette.middleware.gzip import GZipMiddleware
|
|
15
16
|
from starlette.routing import Mount
|
|
16
17
|
from starlette.types import ASGIApp
|
|
@@ -32,13 +33,13 @@ from ..core.config import (
|
|
|
32
33
|
load_repo_config,
|
|
33
34
|
resolve_env_for_root,
|
|
34
35
|
)
|
|
35
|
-
from ..core.doc_chat import DocChatService
|
|
36
36
|
from ..core.engine import Engine, LockError
|
|
37
|
+
from ..core.flows.models import FlowRunStatus
|
|
38
|
+
from ..core.flows.store import FlowStore
|
|
37
39
|
from ..core.hub import HubSupervisor
|
|
38
40
|
from ..core.logging_utils import safe_log, setup_rotating_logger
|
|
39
41
|
from ..core.optional_dependencies import require_optional_dependencies
|
|
40
42
|
from ..core.request_context import get_request_id
|
|
41
|
-
from ..core.snapshot import SnapshotService
|
|
42
43
|
from ..core.state import load_state, persist_session_registry
|
|
43
44
|
from ..core.usage import (
|
|
44
45
|
UsageError,
|
|
@@ -49,17 +50,18 @@ from ..core.usage import (
|
|
|
49
50
|
)
|
|
50
51
|
from ..core.utils import (
|
|
51
52
|
build_opencode_supervisor,
|
|
53
|
+
reset_repo_root_context,
|
|
54
|
+
set_repo_root_context,
|
|
52
55
|
)
|
|
53
56
|
from ..housekeeping import run_housekeeping_once
|
|
54
57
|
from ..integrations.app_server.client import ApprovalHandler, NotificationHandler
|
|
55
58
|
from ..integrations.app_server.env import build_app_server_env
|
|
56
59
|
from ..integrations.app_server.supervisor import WorkspaceAppServerSupervisor
|
|
57
|
-
from ..integrations.github.chatops import GitHubChatOpsPoller
|
|
58
|
-
from ..integrations.github.pr_flow import PrFlowManager
|
|
59
60
|
from ..manifest import load_manifest
|
|
60
61
|
from ..routes import build_repo_router
|
|
61
62
|
from ..routes.system import build_system_routes
|
|
62
|
-
from ..
|
|
63
|
+
from ..tickets.files import safe_relpath
|
|
64
|
+
from ..tickets.outbox import parse_dispatch, resolve_outbox_paths
|
|
63
65
|
from ..voice import VoiceConfig, VoiceService
|
|
64
66
|
from .hub_jobs import HubJobManager
|
|
65
67
|
from .middleware import (
|
|
@@ -94,9 +96,6 @@ class AppContext:
|
|
|
94
96
|
env: Mapping[str, str]
|
|
95
97
|
engine: Engine
|
|
96
98
|
manager: RunnerManager
|
|
97
|
-
doc_chat: DocChatService
|
|
98
|
-
spec_ingest: SpecIngestService
|
|
99
|
-
snapshot_service: SnapshotService
|
|
100
99
|
app_server_supervisor: Optional[WorkspaceAppServerSupervisor]
|
|
101
100
|
app_server_prune_interval: Optional[float]
|
|
102
101
|
app_server_threads: AppServerThreadRegistry
|
|
@@ -211,6 +210,25 @@ def _extract_turn_context(params: dict) -> tuple[Optional[str], Optional[str]]:
|
|
|
211
210
|
return thread_id, turn_id
|
|
212
211
|
|
|
213
212
|
|
|
213
|
+
def _path_is_allowed_for_file_write(path: str) -> bool:
|
|
214
|
+
normalized = (path or "").strip()
|
|
215
|
+
if not normalized:
|
|
216
|
+
return False
|
|
217
|
+
# Canonical allowlist for all AI-assisted file edits via app-server approval:
|
|
218
|
+
# - tickets: .codex-autorunner/tickets/**
|
|
219
|
+
# - workspace docs: .codex-autorunner/workspace/**
|
|
220
|
+
allowed_prefixes = (
|
|
221
|
+
".codex-autorunner/tickets/",
|
|
222
|
+
".codex-autorunner/workspace/",
|
|
223
|
+
)
|
|
224
|
+
if normalized in (".codex-autorunner/tickets", ".codex-autorunner/workspace"):
|
|
225
|
+
return True
|
|
226
|
+
return any(
|
|
227
|
+
normalized == prefix.rstrip("/") or normalized.startswith(prefix)
|
|
228
|
+
for prefix in allowed_prefixes
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
214
232
|
def _build_app_server_supervisor(
|
|
215
233
|
config: AppServerConfig,
|
|
216
234
|
*,
|
|
@@ -246,9 +264,19 @@ def _build_app_server_supervisor(
|
|
|
246
264
|
state_root=config.state_root,
|
|
247
265
|
env_builder=_env_builder,
|
|
248
266
|
logger=logger,
|
|
267
|
+
auto_restart=config.auto_restart,
|
|
249
268
|
max_handles=config.max_handles,
|
|
250
269
|
idle_ttl_seconds=config.idle_ttl_seconds,
|
|
251
270
|
request_timeout=config.request_timeout,
|
|
271
|
+
turn_stall_timeout_seconds=config.turn_stall_timeout_seconds,
|
|
272
|
+
turn_stall_poll_interval_seconds=config.turn_stall_poll_interval_seconds,
|
|
273
|
+
turn_stall_recovery_min_interval_seconds=config.turn_stall_recovery_min_interval_seconds,
|
|
274
|
+
max_message_bytes=config.client.max_message_bytes,
|
|
275
|
+
oversize_preview_bytes=config.client.oversize_preview_bytes,
|
|
276
|
+
max_oversize_drain_bytes=config.client.max_oversize_drain_bytes,
|
|
277
|
+
restart_backoff_initial_seconds=config.client.restart_backoff_initial_seconds,
|
|
278
|
+
restart_backoff_max_seconds=config.client.restart_backoff_max_seconds,
|
|
279
|
+
restart_backoff_jitter_ratio=config.client.restart_backoff_jitter_ratio,
|
|
252
280
|
notification_handler=notification_handler,
|
|
253
281
|
approval_handler=approval_handler,
|
|
254
282
|
)
|
|
@@ -273,6 +301,7 @@ def _build_opencode_supervisor(
|
|
|
273
301
|
logger: logging.Logger,
|
|
274
302
|
env: Mapping[str, str],
|
|
275
303
|
subagent_models: Optional[Mapping[str, str]] = None,
|
|
304
|
+
session_stall_timeout_seconds: Optional[float] = None,
|
|
276
305
|
) -> tuple[Optional[OpenCodeSupervisor], Optional[float]]:
|
|
277
306
|
supervisor = build_opencode_supervisor(
|
|
278
307
|
opencode_command=opencode_command,
|
|
@@ -282,6 +311,7 @@ def _build_opencode_supervisor(
|
|
|
282
311
|
request_timeout=config.request_timeout,
|
|
283
312
|
max_handles=config.max_handles,
|
|
284
313
|
idle_ttl_seconds=config.idle_ttl_seconds,
|
|
314
|
+
session_stall_timeout_seconds=session_stall_timeout_seconds,
|
|
285
315
|
base_env=env,
|
|
286
316
|
subagent_models=subagent_models,
|
|
287
317
|
)
|
|
@@ -352,19 +382,8 @@ def _build_app_context(
|
|
|
352
382
|
f"Repo server ready at {engine.repo_root}",
|
|
353
383
|
)
|
|
354
384
|
app_server_events = AppServerEventBuffer()
|
|
355
|
-
allowed_doc_paths = {
|
|
356
|
-
path
|
|
357
|
-
for kind in ("todo", "progress", "opinions", "spec", "summary")
|
|
358
|
-
for path in [
|
|
359
|
-
_normalize_approval_path(
|
|
360
|
-
str(engine.config.doc_path(kind).relative_to(engine.config.root)),
|
|
361
|
-
engine.config.root,
|
|
362
|
-
)
|
|
363
|
-
]
|
|
364
|
-
if path
|
|
365
|
-
}
|
|
366
385
|
|
|
367
|
-
async def
|
|
386
|
+
async def _file_write_approval_handler(message: dict) -> str:
|
|
368
387
|
method = message.get("method")
|
|
369
388
|
params = message.get("params")
|
|
370
389
|
params = params if isinstance(params, dict) else {}
|
|
@@ -385,9 +404,11 @@ def _build_app_context(
|
|
|
385
404
|
}
|
|
386
405
|
)
|
|
387
406
|
return "decline"
|
|
388
|
-
rejected = [
|
|
407
|
+
rejected = [
|
|
408
|
+
path for path in normalized if not _path_is_allowed_for_file_write(path)
|
|
409
|
+
]
|
|
389
410
|
if rejected:
|
|
390
|
-
notice = "Rejected write
|
|
411
|
+
notice = "Rejected write outside allowlist: " + ", ".join(rejected)
|
|
391
412
|
await app_server_events.handle_notification(
|
|
392
413
|
{
|
|
393
414
|
"method": "error",
|
|
@@ -401,7 +422,7 @@ def _build_app_context(
|
|
|
401
422
|
return "decline"
|
|
402
423
|
return "accept"
|
|
403
424
|
if method == "item/commandExecution/requestApproval":
|
|
404
|
-
notice = "Rejected command execution in
|
|
425
|
+
notice = "Rejected command execution in file write session."
|
|
405
426
|
await app_server_events.handle_notification(
|
|
406
427
|
{
|
|
407
428
|
"method": "error",
|
|
@@ -421,7 +442,7 @@ def _build_app_context(
|
|
|
421
442
|
event_prefix="web.app_server",
|
|
422
443
|
base_env=env,
|
|
423
444
|
notification_handler=app_server_events.handle_notification,
|
|
424
|
-
approval_handler=
|
|
445
|
+
approval_handler=_file_write_approval_handler,
|
|
425
446
|
)
|
|
426
447
|
app_server_threads = AppServerThreadRegistry(
|
|
427
448
|
default_app_server_threads_path(engine.repo_root)
|
|
@@ -441,27 +462,7 @@ def _build_app_context(
|
|
|
441
462
|
logger=logger,
|
|
442
463
|
env=env,
|
|
443
464
|
subagent_models=subagent_models,
|
|
444
|
-
|
|
445
|
-
doc_chat = DocChatService(
|
|
446
|
-
engine,
|
|
447
|
-
app_server_supervisor=app_server_supervisor,
|
|
448
|
-
app_server_threads=app_server_threads,
|
|
449
|
-
app_server_events=app_server_events,
|
|
450
|
-
opencode_supervisor=opencode_supervisor,
|
|
451
|
-
env=env,
|
|
452
|
-
)
|
|
453
|
-
spec_ingest = SpecIngestService(
|
|
454
|
-
engine,
|
|
455
|
-
app_server_supervisor=app_server_supervisor,
|
|
456
|
-
app_server_threads=app_server_threads,
|
|
457
|
-
app_server_events=app_server_events,
|
|
458
|
-
opencode_supervisor=opencode_supervisor,
|
|
459
|
-
env=env,
|
|
460
|
-
)
|
|
461
|
-
snapshot_service = SnapshotService(
|
|
462
|
-
engine,
|
|
463
|
-
app_server_supervisor=app_server_supervisor,
|
|
464
|
-
app_server_threads=app_server_threads,
|
|
465
|
+
session_stall_timeout_seconds=config.opencode.session_stall_timeout_seconds,
|
|
465
466
|
)
|
|
466
467
|
voice_service: Optional[VoiceService]
|
|
467
468
|
if voice_missing_reason:
|
|
@@ -565,9 +566,6 @@ def _build_app_context(
|
|
|
565
566
|
env=env,
|
|
566
567
|
engine=engine,
|
|
567
568
|
manager=manager,
|
|
568
|
-
doc_chat=doc_chat,
|
|
569
|
-
spec_ingest=spec_ingest,
|
|
570
|
-
snapshot_service=snapshot_service,
|
|
571
569
|
app_server_supervisor=app_server_supervisor,
|
|
572
570
|
app_server_prune_interval=app_server_prune_interval,
|
|
573
571
|
app_server_threads=app_server_threads,
|
|
@@ -600,9 +598,6 @@ def _apply_app_context(app: FastAPI, context: AppContext) -> None:
|
|
|
600
598
|
app.state.engine = context.engine
|
|
601
599
|
app.state.config = context.engine.config # Expose config consistently
|
|
602
600
|
app.state.manager = context.manager
|
|
603
|
-
app.state.doc_chat = context.doc_chat
|
|
604
|
-
app.state.spec_ingest = context.spec_ingest
|
|
605
|
-
app.state.snapshot_service = context.snapshot_service
|
|
606
601
|
app.state.app_server_supervisor = context.app_server_supervisor
|
|
607
602
|
app.state.app_server_prune_interval = context.app_server_prune_interval
|
|
608
603
|
app.state.app_server_threads = context.app_server_threads
|
|
@@ -789,24 +784,6 @@ def _app_lifespan(context: AppContext):
|
|
|
789
784
|
|
|
790
785
|
tasks.append(asyncio.create_task(_opencode_prune_loop()))
|
|
791
786
|
|
|
792
|
-
pr_flow_manager = getattr(app.state, "pr_flow_manager", None)
|
|
793
|
-
if pr_flow_manager is None:
|
|
794
|
-
pr_flow_manager = PrFlowManager(
|
|
795
|
-
app.state.engine.repo_root,
|
|
796
|
-
app_server_supervisor=getattr(app.state, "app_server_supervisor", None),
|
|
797
|
-
opencode_supervisor=getattr(app.state, "opencode_supervisor", None),
|
|
798
|
-
logger=getattr(app.state, "logger", None),
|
|
799
|
-
)
|
|
800
|
-
app.state.pr_flow_manager = pr_flow_manager
|
|
801
|
-
chatops = GitHubChatOpsPoller(
|
|
802
|
-
app.state.engine.repo_root,
|
|
803
|
-
pr_flow_manager,
|
|
804
|
-
logger=getattr(app.state, "logger", None),
|
|
805
|
-
)
|
|
806
|
-
app.state.pr_flow_chatops = chatops
|
|
807
|
-
if pr_flow_manager.chatops_config().get("enabled", False):
|
|
808
|
-
tasks.append(asyncio.create_task(chatops.run()))
|
|
809
|
-
|
|
810
787
|
if (
|
|
811
788
|
context.tui_idle_seconds is not None
|
|
812
789
|
and context.tui_idle_check_seconds is not None
|
|
@@ -879,17 +856,6 @@ def _app_lifespan(context: AppContext):
|
|
|
879
856
|
task.cancel()
|
|
880
857
|
if tasks:
|
|
881
858
|
await asyncio.gather(*tasks, return_exceptions=True)
|
|
882
|
-
chatops = getattr(app.state, "pr_flow_chatops", None)
|
|
883
|
-
if chatops is not None:
|
|
884
|
-
try:
|
|
885
|
-
await chatops.stop()
|
|
886
|
-
except Exception as exc:
|
|
887
|
-
safe_log(
|
|
888
|
-
app.state.logger,
|
|
889
|
-
logging.DEBUG,
|
|
890
|
-
"Failed to stop chatops during shutdown",
|
|
891
|
-
exc=exc,
|
|
892
|
-
)
|
|
893
859
|
async with app.state.terminal_lock:
|
|
894
860
|
for session in app.state.terminal_sessions.values():
|
|
895
861
|
session.close()
|
|
@@ -930,14 +896,100 @@ def _app_lifespan(context: AppContext):
|
|
|
930
896
|
return lifespan
|
|
931
897
|
|
|
932
898
|
|
|
899
|
+
def create_repo_app(
|
|
900
|
+
repo_root: Path,
|
|
901
|
+
server_overrides: Optional[ServerOverrides] = None,
|
|
902
|
+
hub_config: Optional[HubConfig] = None,
|
|
903
|
+
) -> ASGIApp:
|
|
904
|
+
# Hub-only: repo apps are always mounted under `/repos/<id>` and must not
|
|
905
|
+
# apply their own base-path rewriting (the hub handles that globally).
|
|
906
|
+
context = _build_app_context(repo_root, base_path="", hub_config=hub_config)
|
|
907
|
+
app = FastAPI(redirect_slashes=False, lifespan=_app_lifespan(context))
|
|
908
|
+
|
|
909
|
+
class _RepoRootContextMiddleware(BaseHTTPMiddleware):
|
|
910
|
+
"""Ensure find_repo_root() resolves to the mounted repo even when cwd differs."""
|
|
911
|
+
|
|
912
|
+
def __init__(self, app, repo_root: Path):
|
|
913
|
+
super().__init__(app)
|
|
914
|
+
self.repo_root = repo_root
|
|
915
|
+
|
|
916
|
+
async def dispatch(self, request, call_next):
|
|
917
|
+
token = set_repo_root_context(self.repo_root)
|
|
918
|
+
try:
|
|
919
|
+
return await call_next(request)
|
|
920
|
+
finally:
|
|
921
|
+
reset_repo_root_context(token)
|
|
922
|
+
|
|
923
|
+
app.add_middleware(_RepoRootContextMiddleware, repo_root=context.engine.repo_root)
|
|
924
|
+
_apply_app_context(app, context)
|
|
925
|
+
app.add_middleware(GZipMiddleware, minimum_size=500)
|
|
926
|
+
static_files = CacheStaticFiles(directory=context.static_dir)
|
|
927
|
+
app.state.static_files = static_files
|
|
928
|
+
app.state.static_assets_lock = threading.Lock()
|
|
929
|
+
app.state.hub_static_assets = (
|
|
930
|
+
hub_config.static_assets if hub_config is not None else None
|
|
931
|
+
)
|
|
932
|
+
app.mount("/static", static_files, name="static")
|
|
933
|
+
# Route handlers
|
|
934
|
+
app.include_router(build_repo_router(context.static_dir))
|
|
935
|
+
|
|
936
|
+
allowed_hosts = _resolve_allowed_hosts(
|
|
937
|
+
context.engine.config.server_host, context.engine.config.server_allowed_hosts
|
|
938
|
+
)
|
|
939
|
+
allowed_origins = context.engine.config.server_allowed_origins
|
|
940
|
+
auth_token_env = context.engine.config.server_auth_token_env
|
|
941
|
+
if server_overrides is not None:
|
|
942
|
+
if server_overrides.allowed_hosts is not None:
|
|
943
|
+
allowed_hosts = list(server_overrides.allowed_hosts)
|
|
944
|
+
if server_overrides.allowed_origins is not None:
|
|
945
|
+
allowed_origins = list(server_overrides.allowed_origins)
|
|
946
|
+
if server_overrides.auth_token_env is not None:
|
|
947
|
+
auth_token_env = server_overrides.auth_token_env
|
|
948
|
+
auth_token = _resolve_auth_token(auth_token_env, env=context.env)
|
|
949
|
+
app.state.auth_token = auth_token
|
|
950
|
+
if auth_token:
|
|
951
|
+
app.add_middleware(
|
|
952
|
+
AuthTokenMiddleware, auth_token=auth_token, base_path=context.base_path
|
|
953
|
+
)
|
|
954
|
+
app.add_middleware(
|
|
955
|
+
HostOriginMiddleware,
|
|
956
|
+
allowed_hosts=allowed_hosts,
|
|
957
|
+
allowed_origins=allowed_origins,
|
|
958
|
+
)
|
|
959
|
+
app.add_middleware(RequestIdMiddleware)
|
|
960
|
+
app.add_middleware(SecurityHeadersMiddleware)
|
|
961
|
+
|
|
962
|
+
return app
|
|
963
|
+
|
|
964
|
+
|
|
933
965
|
def create_app(
|
|
934
966
|
repo_root: Optional[Path] = None,
|
|
935
967
|
base_path: Optional[str] = None,
|
|
936
968
|
server_overrides: Optional[ServerOverrides] = None,
|
|
937
969
|
hub_config: Optional[HubConfig] = None,
|
|
938
970
|
) -> ASGIApp:
|
|
971
|
+
"""
|
|
972
|
+
Public-facing factory for standalone repo apps (non-hub) retained for backward compatibility.
|
|
973
|
+
"""
|
|
974
|
+
# Respect provided base_path when running directly; hub passes base_path="".
|
|
939
975
|
context = _build_app_context(repo_root, base_path, hub_config=hub_config)
|
|
940
976
|
app = FastAPI(redirect_slashes=False, lifespan=_app_lifespan(context))
|
|
977
|
+
|
|
978
|
+
class _RepoRootContextMiddleware(BaseHTTPMiddleware):
|
|
979
|
+
"""Ensure find_repo_root() resolves to the mounted repo even when cwd differs."""
|
|
980
|
+
|
|
981
|
+
def __init__(self, app, repo_root: Path):
|
|
982
|
+
super().__init__(app)
|
|
983
|
+
self.repo_root = repo_root
|
|
984
|
+
|
|
985
|
+
async def dispatch(self, request, call_next):
|
|
986
|
+
token = set_repo_root_context(self.repo_root)
|
|
987
|
+
try:
|
|
988
|
+
return await call_next(request)
|
|
989
|
+
finally:
|
|
990
|
+
reset_repo_root_context(token)
|
|
991
|
+
|
|
992
|
+
app.add_middleware(_RepoRootContextMiddleware, repo_root=context.engine.repo_root)
|
|
941
993
|
_apply_app_context(app, context)
|
|
942
994
|
app.add_middleware(GZipMiddleware, minimum_size=500)
|
|
943
995
|
static_files = CacheStaticFiles(directory=context.static_dir)
|
|
@@ -964,16 +1016,21 @@ def create_app(
|
|
|
964
1016
|
auth_token_env = server_overrides.auth_token_env
|
|
965
1017
|
auth_token = _resolve_auth_token(auth_token_env, env=context.env)
|
|
966
1018
|
app.state.auth_token = auth_token
|
|
967
|
-
asgi_app: ASGIApp = app
|
|
968
1019
|
if auth_token:
|
|
969
|
-
|
|
1020
|
+
app.add_middleware(
|
|
1021
|
+
AuthTokenMiddleware, auth_token=auth_token, base_path=context.base_path
|
|
1022
|
+
)
|
|
970
1023
|
if context.base_path:
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1024
|
+
app.add_middleware(BasePathRouterMiddleware, base_path=context.base_path)
|
|
1025
|
+
app.add_middleware(
|
|
1026
|
+
HostOriginMiddleware,
|
|
1027
|
+
allowed_hosts=allowed_hosts,
|
|
1028
|
+
allowed_origins=allowed_origins,
|
|
1029
|
+
)
|
|
1030
|
+
app.add_middleware(RequestIdMiddleware)
|
|
1031
|
+
app.add_middleware(SecurityHeadersMiddleware)
|
|
975
1032
|
|
|
976
|
-
return
|
|
1033
|
+
return app
|
|
977
1034
|
|
|
978
1035
|
|
|
979
1036
|
def create_hub_app(
|
|
@@ -1096,9 +1153,8 @@ def create_hub_app(
|
|
|
1096
1153
|
return False
|
|
1097
1154
|
try:
|
|
1098
1155
|
# Hub already handles the base path; avoid reapplying it in child apps.
|
|
1099
|
-
sub_app =
|
|
1156
|
+
sub_app = create_repo_app(
|
|
1100
1157
|
repo_path,
|
|
1101
|
-
base_path="",
|
|
1102
1158
|
server_overrides=repo_server_overrides,
|
|
1103
1159
|
hub_config=context.config,
|
|
1104
1160
|
)
|
|
@@ -1126,6 +1182,9 @@ def create_hub_app(
|
|
|
1126
1182
|
exc=exc2,
|
|
1127
1183
|
)
|
|
1128
1184
|
return False
|
|
1185
|
+
fastapi_app = _unwrap_fastapi(sub_app)
|
|
1186
|
+
if fastapi_app is not None:
|
|
1187
|
+
fastapi_app.state.repo_id = prefix
|
|
1129
1188
|
app.mount(f"/repos/{prefix}", sub_app)
|
|
1130
1189
|
mounted_repos.add(prefix)
|
|
1131
1190
|
repo_apps[prefix] = sub_app
|
|
@@ -1154,9 +1213,8 @@ def create_hub_app(
|
|
|
1154
1213
|
continue
|
|
1155
1214
|
# Hub already handles the base path; avoid reapplying it in child apps.
|
|
1156
1215
|
try:
|
|
1157
|
-
sub_app =
|
|
1216
|
+
sub_app = create_repo_app(
|
|
1158
1217
|
snap.path,
|
|
1159
|
-
base_path="",
|
|
1160
1218
|
server_overrides=repo_server_overrides,
|
|
1161
1219
|
hub_config=context.config,
|
|
1162
1220
|
)
|
|
@@ -1188,6 +1246,9 @@ def create_hub_app(
|
|
|
1188
1246
|
exc=exc2,
|
|
1189
1247
|
)
|
|
1190
1248
|
continue
|
|
1249
|
+
fastapi_app = _unwrap_fastapi(sub_app)
|
|
1250
|
+
if fastapi_app is not None:
|
|
1251
|
+
fastapi_app.state.repo_id = snap.id
|
|
1191
1252
|
app.mount(f"/repos/{snap.id}", sub_app)
|
|
1192
1253
|
mounted_repos.add(snap.id)
|
|
1193
1254
|
repo_apps[snap.id] = sub_app
|
|
@@ -1366,6 +1427,123 @@ def create_hub_app(
|
|
|
1366
1427
|
**series,
|
|
1367
1428
|
}
|
|
1368
1429
|
|
|
1430
|
+
@app.get("/hub/messages")
|
|
1431
|
+
async def hub_messages(limit: int = 100):
|
|
1432
|
+
"""Return paused ticket_flow dispatches across all repos.
|
|
1433
|
+
|
|
1434
|
+
The hub inbox is intentionally simple: it surfaces the latest archived
|
|
1435
|
+
dispatch for each paused ticket_flow run.
|
|
1436
|
+
"""
|
|
1437
|
+
|
|
1438
|
+
def _latest_dispatch(
|
|
1439
|
+
repo_root: Path, run_id: str, input_data: dict
|
|
1440
|
+
) -> Optional[dict]:
|
|
1441
|
+
try:
|
|
1442
|
+
workspace_root = Path(input_data.get("workspace_root") or repo_root)
|
|
1443
|
+
runs_dir = Path(input_data.get("runs_dir") or ".codex-autorunner/runs")
|
|
1444
|
+
outbox_paths = resolve_outbox_paths(
|
|
1445
|
+
workspace_root=workspace_root, runs_dir=runs_dir, run_id=run_id
|
|
1446
|
+
)
|
|
1447
|
+
history_dir = outbox_paths.dispatch_history_dir
|
|
1448
|
+
if not history_dir.exists() or not history_dir.is_dir():
|
|
1449
|
+
return None
|
|
1450
|
+
seq_dirs: list[Path] = []
|
|
1451
|
+
for child in history_dir.iterdir():
|
|
1452
|
+
if not child.is_dir():
|
|
1453
|
+
continue
|
|
1454
|
+
name = child.name
|
|
1455
|
+
if len(name) == 4 and name.isdigit():
|
|
1456
|
+
seq_dirs.append(child)
|
|
1457
|
+
if not seq_dirs:
|
|
1458
|
+
return None
|
|
1459
|
+
latest_dir = sorted(seq_dirs, key=lambda p: p.name)[-1]
|
|
1460
|
+
seq = int(latest_dir.name)
|
|
1461
|
+
dispatch_path = latest_dir / "DISPATCH.md"
|
|
1462
|
+
dispatch, errors = parse_dispatch(dispatch_path)
|
|
1463
|
+
if errors or dispatch is None:
|
|
1464
|
+
return {
|
|
1465
|
+
"seq": seq,
|
|
1466
|
+
"dir": safe_relpath(latest_dir, repo_root),
|
|
1467
|
+
"dispatch": None,
|
|
1468
|
+
"errors": errors,
|
|
1469
|
+
"files": [],
|
|
1470
|
+
}
|
|
1471
|
+
files: list[str] = []
|
|
1472
|
+
for child in sorted(latest_dir.iterdir(), key=lambda p: p.name):
|
|
1473
|
+
if child.name.startswith("."):
|
|
1474
|
+
continue
|
|
1475
|
+
if child.name == "DISPATCH.md":
|
|
1476
|
+
continue
|
|
1477
|
+
if child.is_file():
|
|
1478
|
+
files.append(child.name)
|
|
1479
|
+
dispatch_dict = {
|
|
1480
|
+
"mode": dispatch.mode,
|
|
1481
|
+
"title": dispatch.title,
|
|
1482
|
+
"body": dispatch.body,
|
|
1483
|
+
"extra": dispatch.extra,
|
|
1484
|
+
"is_handoff": dispatch.is_handoff,
|
|
1485
|
+
}
|
|
1486
|
+
return {
|
|
1487
|
+
"seq": seq,
|
|
1488
|
+
"dir": safe_relpath(latest_dir, repo_root),
|
|
1489
|
+
"dispatch": dispatch_dict,
|
|
1490
|
+
"errors": [],
|
|
1491
|
+
"files": files,
|
|
1492
|
+
}
|
|
1493
|
+
except Exception:
|
|
1494
|
+
return None
|
|
1495
|
+
|
|
1496
|
+
def _gather() -> list[dict]:
|
|
1497
|
+
messages: list[dict] = []
|
|
1498
|
+
try:
|
|
1499
|
+
snapshots = context.supervisor.list_repos()
|
|
1500
|
+
except Exception:
|
|
1501
|
+
return []
|
|
1502
|
+
for snap in snapshots:
|
|
1503
|
+
if not (snap.initialized and snap.exists_on_disk):
|
|
1504
|
+
continue
|
|
1505
|
+
repo_root = snap.path
|
|
1506
|
+
db_path = repo_root / ".codex-autorunner" / "flows.db"
|
|
1507
|
+
if not db_path.exists():
|
|
1508
|
+
continue
|
|
1509
|
+
try:
|
|
1510
|
+
store = FlowStore(db_path)
|
|
1511
|
+
store.initialize()
|
|
1512
|
+
paused = store.list_flow_runs(
|
|
1513
|
+
flow_type="ticket_flow", status=FlowRunStatus.PAUSED
|
|
1514
|
+
)
|
|
1515
|
+
except Exception:
|
|
1516
|
+
continue
|
|
1517
|
+
if not paused:
|
|
1518
|
+
continue
|
|
1519
|
+
for record in paused:
|
|
1520
|
+
latest = _latest_dispatch(
|
|
1521
|
+
repo_root, str(record.id), dict(record.input_data or {})
|
|
1522
|
+
)
|
|
1523
|
+
if not latest or not latest.get("dispatch"):
|
|
1524
|
+
continue
|
|
1525
|
+
messages.append(
|
|
1526
|
+
{
|
|
1527
|
+
"repo_id": snap.id,
|
|
1528
|
+
"repo_display_name": snap.display_name,
|
|
1529
|
+
"repo_path": str(snap.path),
|
|
1530
|
+
"run_id": record.id,
|
|
1531
|
+
"run_created_at": record.created_at,
|
|
1532
|
+
"status": record.status.value,
|
|
1533
|
+
"seq": latest["seq"],
|
|
1534
|
+
"dispatch": latest["dispatch"],
|
|
1535
|
+
"files": latest.get("files") or [],
|
|
1536
|
+
"open_url": f"/repos/{snap.id}/?tab=inbox&run_id={record.id}",
|
|
1537
|
+
}
|
|
1538
|
+
)
|
|
1539
|
+
messages.sort(key=lambda m: (m.get("run_created_at") or ""), reverse=True)
|
|
1540
|
+
if limit and limit > 0:
|
|
1541
|
+
return messages[: int(limit)]
|
|
1542
|
+
return messages
|
|
1543
|
+
|
|
1544
|
+
items = await asyncio.to_thread(_gather)
|
|
1545
|
+
return {"items": items}
|
|
1546
|
+
|
|
1369
1547
|
@app.get("/hub/repos")
|
|
1370
1548
|
async def list_repos():
|
|
1371
1549
|
safe_log(app.state.logger, logging.INFO, "Hub list_repos")
|
|
@@ -495,11 +495,10 @@ class RequestIdMiddleware:
|
|
|
495
495
|
"""Check if endpoint should log response size (docs, runs, hub repos)."""
|
|
496
496
|
path_lower = path.lower()
|
|
497
497
|
heavy_prefixes = (
|
|
498
|
-
"/api/
|
|
499
|
-
"/api/
|
|
500
|
-
"/api/
|
|
498
|
+
"/api/workspace",
|
|
499
|
+
"/api/workspace/spec/ingest",
|
|
500
|
+
"/api/file-chat",
|
|
501
501
|
"/api/usage",
|
|
502
|
-
"/api/ingest-spec",
|
|
503
502
|
"/hub/usage",
|
|
504
503
|
"/hub/repos",
|
|
505
504
|
)
|