codex-autorunner 0.1.1__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/__init__.py +20 -0
- codex_autorunner/agents/base.py +2 -2
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/__init__.py +4 -0
- codex_autorunner/agents/opencode/agent_config.py +104 -0
- codex_autorunner/agents/opencode/client.py +305 -28
- codex_autorunner/agents/opencode/harness.py +71 -20
- codex_autorunner/agents/opencode/logging.py +225 -0
- codex_autorunner/agents/opencode/run_prompt.py +261 -0
- codex_autorunner/agents/opencode/runtime.py +1202 -132
- codex_autorunner/agents/opencode/supervisor.py +194 -68
- codex_autorunner/agents/registry.py +258 -0
- codex_autorunner/agents/types.py +2 -2
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +19 -40
- codex_autorunner/cli.py +234 -151
- codex_autorunner/core/about_car.py +44 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_events.py +15 -6
- codex_autorunner/core/app_server_logging.py +55 -15
- codex_autorunner/core/app_server_prompts.py +28 -259
- codex_autorunner/core/app_server_threads.py +15 -26
- codex_autorunner/core/circuit_breaker.py +183 -0
- codex_autorunner/core/codex_runner.py +6 -0
- codex_autorunner/core/config.py +555 -133
- codex_autorunner/core/docs.py +54 -9
- codex_autorunner/core/drafts.py +82 -0
- codex_autorunner/core/engine.py +828 -274
- codex_autorunner/core/exceptions.py +60 -0
- 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 +21 -13
- codex_autorunner/core/locks.py +118 -1
- codex_autorunner/core/logging_utils.py +9 -6
- codex_autorunner/core/path_utils.py +123 -0
- codex_autorunner/core/prompt.py +15 -7
- codex_autorunner/core/redaction.py +29 -0
- codex_autorunner/core/retry.py +61 -0
- codex_autorunner/core/review.py +888 -0
- codex_autorunner/core/review_context.py +161 -0
- codex_autorunner/core/run_index.py +223 -0
- codex_autorunner/core/runner_controller.py +44 -1
- codex_autorunner/core/runner_process.py +30 -1
- codex_autorunner/core/sqlite_utils.py +32 -0
- codex_autorunner/core/state.py +273 -44
- codex_autorunner/core/static_assets.py +55 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/text_delta_coalescer.py +43 -0
- codex_autorunner/core/update.py +20 -11
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/usage.py +107 -75
- codex_autorunner/core/utils.py +167 -3
- codex_autorunner/discovery.py +3 -3
- 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 +708 -153
- codex_autorunner/integrations/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +474 -185
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +239 -1
- codex_autorunner/integrations/telegram/constants.py +19 -1
- codex_autorunner/integrations/telegram/dispatch.py +44 -8
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/approvals.py +12 -10
- codex_autorunner/integrations/telegram/handlers/callbacks.py +15 -1
- codex_autorunner/integrations/telegram/handlers/commands/__init__.py +29 -0
- codex_autorunner/integrations/telegram/handlers/commands/approvals.py +173 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +2595 -0
- codex_autorunner/integrations/telegram/handlers/commands/files.py +1408 -0
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
- codex_autorunner/integrations/telegram/handlers/commands/formatting.py +81 -0
- codex_autorunner/integrations/telegram/handlers/commands/github.py +1688 -0
- codex_autorunner/integrations/telegram/handlers/commands/shared.py +190 -0
- codex_autorunner/integrations/telegram/handlers/commands/voice.py +112 -0
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +2043 -0
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +954 -5689
- codex_autorunner/integrations/telegram/handlers/{commands.py → commands_spec.py} +11 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +374 -49
- codex_autorunner/integrations/telegram/handlers/questions.py +389 -0
- codex_autorunner/integrations/telegram/handlers/selections.py +6 -4
- codex_autorunner/integrations/telegram/handlers/utils.py +171 -0
- codex_autorunner/integrations/telegram/helpers.py +90 -18
- codex_autorunner/integrations/telegram/notifications.py +126 -35
- codex_autorunner/integrations/telegram/outbox.py +214 -43
- codex_autorunner/integrations/telegram/progress_stream.py +42 -19
- codex_autorunner/integrations/telegram/runtime.py +24 -13
- codex_autorunner/integrations/telegram/service.py +500 -129
- codex_autorunner/integrations/telegram/state.py +1278 -330
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
- codex_autorunner/integrations/telegram/transport.py +37 -4
- codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
- codex_autorunner/integrations/telegram/types.py +22 -2
- codex_autorunner/integrations/telegram/voice.py +14 -15
- codex_autorunner/manifest.py +2 -0
- codex_autorunner/plugin_api.py +22 -0
- codex_autorunner/routes/__init__.py +25 -14
- codex_autorunner/routes/agents.py +18 -78
- codex_autorunner/routes/analytics.py +239 -0
- codex_autorunner/routes/base.py +142 -113
- 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/repos.py +17 -0
- codex_autorunner/routes/review.py +148 -0
- codex_autorunner/routes/sessions.py +16 -8
- codex_autorunner/routes/settings.py +22 -0
- codex_autorunner/routes/shared.py +33 -3
- codex_autorunner/routes/system.py +22 -1
- codex_autorunner/routes/usage.py +87 -0
- codex_autorunner/routes/voice.py +5 -13
- codex_autorunner/routes/workspace.py +271 -0
- codex_autorunner/server.py +2 -1
- codex_autorunner/static/agentControls.js +9 -1
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +27 -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 -150
- 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 +67 -126
- codex_autorunner/static/index.html +788 -807
- codex_autorunner/static/liveUpdates.js +59 -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 -205
- codex_autorunner/static/styles.css +7577 -3758
- codex_autorunner/static/tabs.js +28 -5
- codex_autorunner/static/terminal.js +14 -0
- codex_autorunner/static/terminalManager.js +53 -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 +21 -7
- 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/voice/capture.py +7 -7
- codex_autorunner/voice/service.py +51 -9
- codex_autorunner/web/app.py +419 -199
- codex_autorunner/web/hub_jobs.py +13 -2
- codex_autorunner/web/middleware.py +47 -13
- codex_autorunner/web/pty_session.py +26 -13
- codex_autorunner/web/schemas.py +114 -109
- codex_autorunner/web/static_assets.py +55 -42
- codex_autorunner/web/static_refresh.py +86 -0
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +319 -0
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +20 -21
- codex_autorunner-1.0.0.dist-info/RECORD +251 -0
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
- codex_autorunner/core/doc_chat.py +0 -1415
- 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 -118
- codex_autorunner/spec_ingest.py +0 -788
- codex_autorunner/static/docChatActions.js +0 -279
- 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 -274
- 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 -442
- codex_autorunner/static/logs.js +0 -640
- codex_autorunner/static/runs.js +0 -409
- codex_autorunner/static/snapshot.js +0 -124
- codex_autorunner/static/state.js +0 -86
- codex_autorunner/static/todoPreview.js +0 -27
- codex_autorunner/workspace.py +0 -16
- codex_autorunner-0.1.1.dist-info/RECORD +0 -191
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
codex_autorunner/core/config.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import dataclasses
|
|
2
2
|
import ipaddress
|
|
3
3
|
import json
|
|
4
|
+
import logging
|
|
4
5
|
import os
|
|
5
6
|
import shlex
|
|
6
7
|
from os import PathLike
|
|
@@ -10,6 +11,9 @@ from typing import IO, Any, Dict, List, Mapping, Optional, Union, cast
|
|
|
10
11
|
import yaml
|
|
11
12
|
|
|
12
13
|
from ..housekeeping import HousekeepingConfig, parse_housekeeping_config
|
|
14
|
+
from .path_utils import ConfigPathError, resolve_config_path
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("codex_autorunner.core.config")
|
|
13
17
|
|
|
14
18
|
DOTENV_AVAILABLE = True
|
|
15
19
|
try:
|
|
@@ -48,13 +52,18 @@ DEFAULT_REPO_CONFIG: Dict[str, Any] = {
|
|
|
48
52
|
"version": CONFIG_VERSION,
|
|
49
53
|
"mode": "repo",
|
|
50
54
|
"docs": {
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
"
|
|
55
|
+
"active_context": ".codex-autorunner/workspace/active_context.md",
|
|
56
|
+
"decisions": ".codex-autorunner/workspace/decisions.md",
|
|
57
|
+
"spec": ".codex-autorunner/workspace/spec.md",
|
|
58
|
+
},
|
|
59
|
+
"review": {
|
|
60
|
+
"enabled": True,
|
|
61
|
+
"agent": "opencode",
|
|
62
|
+
"model": "zai-coding-plan/glm-4.7",
|
|
63
|
+
"subagent_agent": "subagent",
|
|
64
|
+
"subagent_model": "zai-coding-plan/glm-4.7-flashx",
|
|
65
|
+
"reasoning": None,
|
|
66
|
+
"max_wallclock_seconds": None,
|
|
58
67
|
},
|
|
59
68
|
"codex": {
|
|
60
69
|
"binary": "codex",
|
|
@@ -77,16 +86,52 @@ DEFAULT_REPO_CONFIG: Dict[str, Any] = {
|
|
|
77
86
|
},
|
|
78
87
|
"opencode": {
|
|
79
88
|
"binary": "opencode",
|
|
89
|
+
"subagent_models": {
|
|
90
|
+
"subagent": "zai-coding-plan/glm-4.7-flashx",
|
|
91
|
+
},
|
|
80
92
|
},
|
|
81
93
|
},
|
|
82
94
|
"prompt": {
|
|
83
95
|
"prev_run_max_chars": 6000,
|
|
84
96
|
"template": ".codex-autorunner/prompt.txt",
|
|
85
97
|
},
|
|
98
|
+
"security": {
|
|
99
|
+
"redact_run_logs": True,
|
|
100
|
+
},
|
|
86
101
|
"runner": {
|
|
87
102
|
"sleep_seconds": 5,
|
|
88
103
|
"stop_after_runs": None,
|
|
89
104
|
"max_wallclock_seconds": None,
|
|
105
|
+
"no_progress_threshold": 3,
|
|
106
|
+
"review": {
|
|
107
|
+
"enabled": False,
|
|
108
|
+
"trigger": {
|
|
109
|
+
"on_todos_complete": True,
|
|
110
|
+
"on_no_progress_stop": True,
|
|
111
|
+
"on_max_runs_stop": True,
|
|
112
|
+
"on_stop_requested": False,
|
|
113
|
+
"on_error_exit": False,
|
|
114
|
+
},
|
|
115
|
+
"agent": None,
|
|
116
|
+
"model": None,
|
|
117
|
+
"reasoning": None,
|
|
118
|
+
"max_wallclock_seconds": None,
|
|
119
|
+
"context": {
|
|
120
|
+
"primary_docs": ["spec", "decisions"],
|
|
121
|
+
"include_docs": ["active_context"],
|
|
122
|
+
"include_last_run_artifacts": True,
|
|
123
|
+
"max_doc_chars": 20000,
|
|
124
|
+
},
|
|
125
|
+
"artifacts": {
|
|
126
|
+
"attach_to_last_run_index": True,
|
|
127
|
+
"write_to_review_runs_dir": True,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
"ticket_flow": {
|
|
132
|
+
"approval_mode": "yolo",
|
|
133
|
+
# Keep ticket_flow deterministic by default; surfaces can tighten this.
|
|
134
|
+
"default_approval_decision": "accept",
|
|
90
135
|
},
|
|
91
136
|
"git": {
|
|
92
137
|
"auto_commit": False,
|
|
@@ -98,34 +143,33 @@ DEFAULT_REPO_CONFIG: Dict[str, Any] = {
|
|
|
98
143
|
"sync_commit_mode": "auto", # none|auto|always
|
|
99
144
|
# Bounds the agentic sync step in GitHubService.sync_pr (seconds).
|
|
100
145
|
"sync_agent_timeout_seconds": 1800,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
"stop_condition": "no_issues",
|
|
105
|
-
"max_implementation_runs": None,
|
|
106
|
-
"max_wallclock_seconds": None,
|
|
107
|
-
"review": {
|
|
108
|
-
"include_codex": True,
|
|
109
|
-
"include_github": True,
|
|
110
|
-
"include_checks": True,
|
|
111
|
-
},
|
|
112
|
-
"chatops": {
|
|
113
|
-
"enabled": False,
|
|
114
|
-
"poll_interval_seconds": 60,
|
|
115
|
-
"allow_users": [],
|
|
116
|
-
"allow_associations": [],
|
|
117
|
-
"ignore_bots": True,
|
|
118
|
-
},
|
|
119
|
-
},
|
|
146
|
+
},
|
|
147
|
+
"update": {
|
|
148
|
+
"skip_checks": False,
|
|
120
149
|
},
|
|
121
150
|
"app_server": {
|
|
122
151
|
"command": ["codex", "app-server"],
|
|
123
152
|
"state_root": "~/.codex-autorunner/workspaces",
|
|
153
|
+
"auto_restart": True,
|
|
124
154
|
"max_handles": 20,
|
|
125
155
|
"idle_ttl_seconds": 3600,
|
|
126
156
|
"turn_timeout_seconds": 28800,
|
|
157
|
+
"turn_stall_timeout_seconds": 60,
|
|
158
|
+
"turn_stall_poll_interval_seconds": 2,
|
|
159
|
+
"turn_stall_recovery_min_interval_seconds": 10,
|
|
127
160
|
"request_timeout": None,
|
|
161
|
+
"client": {
|
|
162
|
+
"max_message_bytes": 50 * 1024 * 1024,
|
|
163
|
+
"oversize_preview_bytes": 4096,
|
|
164
|
+
"max_oversize_drain_bytes": 100 * 1024 * 1024,
|
|
165
|
+
"restart_backoff_initial_seconds": 0.5,
|
|
166
|
+
"restart_backoff_max_seconds": 30.0,
|
|
167
|
+
"restart_backoff_jitter_ratio": 0.1,
|
|
168
|
+
},
|
|
128
169
|
"prompts": {
|
|
170
|
+
# NOTE: These keys are legacy names kept for config compatibility.
|
|
171
|
+
# The workspace cutover uses tickets + workspace docs + unified file chat; only
|
|
172
|
+
# the "autorunner" prompt is currently used by the app-server prompt builder.
|
|
129
173
|
"doc_chat": {
|
|
130
174
|
"max_chars": 12000,
|
|
131
175
|
"message_max_chars": 2000,
|
|
@@ -145,6 +189,9 @@ DEFAULT_REPO_CONFIG: Dict[str, Any] = {
|
|
|
145
189
|
},
|
|
146
190
|
},
|
|
147
191
|
},
|
|
192
|
+
"opencode": {
|
|
193
|
+
"session_stall_timeout_seconds": 60,
|
|
194
|
+
},
|
|
148
195
|
"server": {
|
|
149
196
|
"host": "127.0.0.1",
|
|
150
197
|
"port": 4173,
|
|
@@ -208,6 +255,20 @@ DEFAULT_REPO_CONFIG: Dict[str, Any] = {
|
|
|
208
255
|
"timeout_ms": 120000,
|
|
209
256
|
"max_output_chars": 3800,
|
|
210
257
|
},
|
|
258
|
+
"cache": {
|
|
259
|
+
"cleanup_interval_seconds": 300,
|
|
260
|
+
"coalesce_buffer_ttl_seconds": 60,
|
|
261
|
+
"media_batch_buffer_ttl_seconds": 60,
|
|
262
|
+
"model_pending_ttl_seconds": 1800,
|
|
263
|
+
"pending_approval_ttl_seconds": 600,
|
|
264
|
+
"pending_question_ttl_seconds": 600,
|
|
265
|
+
"reasoning_buffer_ttl_seconds": 900,
|
|
266
|
+
"selection_state_ttl_seconds": 1800,
|
|
267
|
+
"turn_preview_ttl_seconds": 900,
|
|
268
|
+
"progress_stream_ttl_seconds": 900,
|
|
269
|
+
"oversize_warning_ttl_seconds": 3600,
|
|
270
|
+
"update_id_persist_interval_seconds": 60,
|
|
271
|
+
},
|
|
211
272
|
"command_registration": {
|
|
212
273
|
"enabled": True,
|
|
213
274
|
"scopes": [
|
|
@@ -215,12 +276,18 @@ DEFAULT_REPO_CONFIG: Dict[str, Any] = {
|
|
|
215
276
|
{"type": "all_group_chats", "language_code": ""},
|
|
216
277
|
],
|
|
217
278
|
},
|
|
218
|
-
"
|
|
279
|
+
"opencode_command": None,
|
|
280
|
+
"state_file": ".codex-autorunner/telegram_state.sqlite3",
|
|
219
281
|
"app_server_command_env": "CAR_TELEGRAM_APP_SERVER_COMMAND",
|
|
220
282
|
"app_server_command": ["codex", "app-server"],
|
|
221
283
|
"app_server": {
|
|
222
284
|
"max_handles": 20,
|
|
223
285
|
"idle_ttl_seconds": 3600,
|
|
286
|
+
"turn_timeout_seconds": 28800,
|
|
287
|
+
},
|
|
288
|
+
"agent_timeouts": {
|
|
289
|
+
"codex": 28800,
|
|
290
|
+
"opencode": 28800,
|
|
224
291
|
},
|
|
225
292
|
"polling": {
|
|
226
293
|
"timeout_seconds": 30,
|
|
@@ -334,6 +401,16 @@ DEFAULT_REPO_CONFIG: Dict[str, Any] = {
|
|
|
334
401
|
"max_total_bytes": 100_000_000,
|
|
335
402
|
"max_age_days": 30,
|
|
336
403
|
},
|
|
404
|
+
{
|
|
405
|
+
"name": "review_runs",
|
|
406
|
+
"kind": "directory",
|
|
407
|
+
"path": ".codex-autorunner/review/runs",
|
|
408
|
+
"glob": "*",
|
|
409
|
+
"recursive": True,
|
|
410
|
+
"max_files": 100,
|
|
411
|
+
"max_total_bytes": 500_000_000,
|
|
412
|
+
"max_age_days": 30,
|
|
413
|
+
},
|
|
337
414
|
],
|
|
338
415
|
},
|
|
339
416
|
}
|
|
@@ -343,12 +420,16 @@ REPO_DEFAULT_KEYS = {
|
|
|
343
420
|
"codex",
|
|
344
421
|
"prompt",
|
|
345
422
|
"runner",
|
|
423
|
+
"ticket_flow",
|
|
346
424
|
"git",
|
|
347
425
|
"github",
|
|
426
|
+
"update",
|
|
348
427
|
"notifications",
|
|
349
428
|
"voice",
|
|
350
429
|
"log",
|
|
351
430
|
"server_log",
|
|
431
|
+
"review",
|
|
432
|
+
"opencode",
|
|
352
433
|
}
|
|
353
434
|
DEFAULT_REPO_DEFAULTS = {
|
|
354
435
|
key: json.loads(json.dumps(DEFAULT_REPO_CONFIG[key])) for key in REPO_DEFAULT_KEYS
|
|
@@ -357,10 +438,12 @@ REPO_SHARED_KEYS = {
|
|
|
357
438
|
"agents",
|
|
358
439
|
"server",
|
|
359
440
|
"app_server",
|
|
441
|
+
"opencode",
|
|
360
442
|
"telegram_bot",
|
|
361
443
|
"terminal",
|
|
362
444
|
"static_assets",
|
|
363
445
|
"housekeeping",
|
|
446
|
+
"update",
|
|
364
447
|
}
|
|
365
448
|
|
|
366
449
|
DEFAULT_HUB_CONFIG: Dict[str, Any] = {
|
|
@@ -373,6 +456,9 @@ DEFAULT_HUB_CONFIG: Dict[str, Any] = {
|
|
|
373
456
|
},
|
|
374
457
|
"opencode": {
|
|
375
458
|
"binary": "opencode",
|
|
459
|
+
"subagent_models": {
|
|
460
|
+
"subagent": "zai-coding-plan/glm-4.7-flashx",
|
|
461
|
+
},
|
|
376
462
|
},
|
|
377
463
|
},
|
|
378
464
|
"terminal": {
|
|
@@ -420,6 +506,20 @@ DEFAULT_HUB_CONFIG: Dict[str, Any] = {
|
|
|
420
506
|
"timeout_ms": 120000,
|
|
421
507
|
"max_output_chars": 3800,
|
|
422
508
|
},
|
|
509
|
+
"cache": {
|
|
510
|
+
"cleanup_interval_seconds": 300,
|
|
511
|
+
"coalesce_buffer_ttl_seconds": 60,
|
|
512
|
+
"media_batch_buffer_ttl_seconds": 60,
|
|
513
|
+
"model_pending_ttl_seconds": 1800,
|
|
514
|
+
"pending_approval_ttl_seconds": 600,
|
|
515
|
+
"pending_question_ttl_seconds": 600,
|
|
516
|
+
"reasoning_buffer_ttl_seconds": 900,
|
|
517
|
+
"selection_state_ttl_seconds": 1800,
|
|
518
|
+
"turn_preview_ttl_seconds": 900,
|
|
519
|
+
"progress_stream_ttl_seconds": 900,
|
|
520
|
+
"oversize_warning_ttl_seconds": 3600,
|
|
521
|
+
"update_id_persist_interval_seconds": 60,
|
|
522
|
+
},
|
|
423
523
|
"command_registration": {
|
|
424
524
|
"enabled": True,
|
|
425
525
|
"scopes": [
|
|
@@ -427,12 +527,14 @@ DEFAULT_HUB_CONFIG: Dict[str, Any] = {
|
|
|
427
527
|
{"type": "all_group_chats", "language_code": ""},
|
|
428
528
|
],
|
|
429
529
|
},
|
|
430
|
-
"
|
|
530
|
+
"opencode_command": None,
|
|
531
|
+
"state_file": ".codex-autorunner/telegram_state.sqlite3",
|
|
431
532
|
"app_server_command_env": "CAR_TELEGRAM_APP_SERVER_COMMAND",
|
|
432
533
|
"app_server_command": ["codex", "app-server"],
|
|
433
534
|
"app_server": {
|
|
434
535
|
"max_handles": 20,
|
|
435
536
|
"idle_ttl_seconds": 3600,
|
|
537
|
+
"turn_timeout_seconds": 28800,
|
|
436
538
|
},
|
|
437
539
|
"polling": {
|
|
438
540
|
"timeout_seconds": 30,
|
|
@@ -456,13 +558,28 @@ DEFAULT_HUB_CONFIG: Dict[str, Any] = {
|
|
|
456
558
|
"backup_count": 3,
|
|
457
559
|
},
|
|
458
560
|
},
|
|
561
|
+
"update": {
|
|
562
|
+
"skip_checks": False,
|
|
563
|
+
},
|
|
459
564
|
"app_server": {
|
|
460
565
|
"command": ["codex", "app-server"],
|
|
461
566
|
"state_root": "~/.codex-autorunner/workspaces",
|
|
567
|
+
"auto_restart": True,
|
|
462
568
|
"max_handles": 20,
|
|
463
569
|
"idle_ttl_seconds": 3600,
|
|
464
570
|
"turn_timeout_seconds": 28800,
|
|
571
|
+
"turn_stall_timeout_seconds": 60,
|
|
572
|
+
"turn_stall_poll_interval_seconds": 2,
|
|
573
|
+
"turn_stall_recovery_min_interval_seconds": 10,
|
|
465
574
|
"request_timeout": None,
|
|
575
|
+
"client": {
|
|
576
|
+
"max_message_bytes": 50 * 1024 * 1024,
|
|
577
|
+
"oversize_preview_bytes": 4096,
|
|
578
|
+
"max_oversize_drain_bytes": 100 * 1024 * 1024,
|
|
579
|
+
"restart_backoff_initial_seconds": 0.5,
|
|
580
|
+
"restart_backoff_max_seconds": 30.0,
|
|
581
|
+
"restart_backoff_jitter_ratio": 0.1,
|
|
582
|
+
},
|
|
466
583
|
"prompts": {
|
|
467
584
|
"doc_chat": {
|
|
468
585
|
"max_chars": 12000,
|
|
@@ -483,6 +600,9 @@ DEFAULT_HUB_CONFIG: Dict[str, Any] = {
|
|
|
483
600
|
},
|
|
484
601
|
},
|
|
485
602
|
},
|
|
603
|
+
"opencode": {
|
|
604
|
+
"session_stall_timeout_seconds": 60,
|
|
605
|
+
},
|
|
486
606
|
"server": {
|
|
487
607
|
"host": "127.0.0.1",
|
|
488
608
|
"port": 4173,
|
|
@@ -590,6 +710,12 @@ class ConfigError(Exception):
|
|
|
590
710
|
"""Raised when configuration is invalid."""
|
|
591
711
|
|
|
592
712
|
|
|
713
|
+
__all__ = [
|
|
714
|
+
"ConfigError",
|
|
715
|
+
"ConfigPathError",
|
|
716
|
+
]
|
|
717
|
+
|
|
718
|
+
|
|
593
719
|
@dataclasses.dataclass
|
|
594
720
|
class LogConfig:
|
|
595
721
|
path: Path
|
|
@@ -634,21 +760,43 @@ class AppServerPromptsConfig:
|
|
|
634
760
|
autorunner: AppServerAutorunnerPromptConfig
|
|
635
761
|
|
|
636
762
|
|
|
763
|
+
@dataclasses.dataclass
|
|
764
|
+
class AppServerClientConfig:
|
|
765
|
+
max_message_bytes: int
|
|
766
|
+
oversize_preview_bytes: int
|
|
767
|
+
max_oversize_drain_bytes: int
|
|
768
|
+
restart_backoff_initial_seconds: float
|
|
769
|
+
restart_backoff_max_seconds: float
|
|
770
|
+
restart_backoff_jitter_ratio: float
|
|
771
|
+
|
|
772
|
+
|
|
637
773
|
@dataclasses.dataclass
|
|
638
774
|
class AppServerConfig:
|
|
639
775
|
command: List[str]
|
|
640
776
|
state_root: Path
|
|
777
|
+
auto_restart: Optional[bool]
|
|
641
778
|
max_handles: Optional[int]
|
|
642
779
|
idle_ttl_seconds: Optional[int]
|
|
643
780
|
turn_timeout_seconds: Optional[float]
|
|
781
|
+
turn_stall_timeout_seconds: Optional[float]
|
|
782
|
+
turn_stall_poll_interval_seconds: Optional[float]
|
|
783
|
+
turn_stall_recovery_min_interval_seconds: Optional[float]
|
|
644
784
|
request_timeout: Optional[float]
|
|
785
|
+
client: AppServerClientConfig
|
|
645
786
|
prompts: AppServerPromptsConfig
|
|
646
787
|
|
|
647
788
|
|
|
789
|
+
@dataclasses.dataclass
|
|
790
|
+
class OpenCodeConfig:
|
|
791
|
+
session_stall_timeout_seconds: Optional[float]
|
|
792
|
+
|
|
793
|
+
|
|
648
794
|
@dataclasses.dataclass(frozen=True)
|
|
649
795
|
class AgentConfig:
|
|
650
796
|
binary: str
|
|
651
797
|
serve_command: Optional[List[str]]
|
|
798
|
+
base_url: Optional[str]
|
|
799
|
+
subagent_models: Optional[Dict[str, str]]
|
|
652
800
|
|
|
653
801
|
|
|
654
802
|
@dataclasses.dataclass
|
|
@@ -657,6 +805,7 @@ class RepoConfig:
|
|
|
657
805
|
root: Path
|
|
658
806
|
version: int
|
|
659
807
|
mode: str
|
|
808
|
+
security: Dict[str, Any]
|
|
660
809
|
docs: Dict[str, Path]
|
|
661
810
|
codex_binary: str
|
|
662
811
|
codex_args: List[str]
|
|
@@ -669,9 +818,13 @@ class RepoConfig:
|
|
|
669
818
|
runner_sleep_seconds: int
|
|
670
819
|
runner_stop_after_runs: Optional[int]
|
|
671
820
|
runner_max_wallclock_seconds: Optional[int]
|
|
821
|
+
runner_no_progress_threshold: int
|
|
822
|
+
ticket_flow: Dict[str, Any]
|
|
672
823
|
git_auto_commit: bool
|
|
673
824
|
git_commit_message_template: str
|
|
825
|
+
update_skip_checks: bool
|
|
674
826
|
app_server: AppServerConfig
|
|
827
|
+
opencode: OpenCodeConfig
|
|
675
828
|
server_host: str
|
|
676
829
|
server_port: int
|
|
677
830
|
server_base_path: str
|
|
@@ -719,7 +872,9 @@ class HubConfig:
|
|
|
719
872
|
repo_server_inherit: bool
|
|
720
873
|
update_repo_url: str
|
|
721
874
|
update_repo_ref: str
|
|
875
|
+
update_skip_checks: bool
|
|
722
876
|
app_server: AppServerConfig
|
|
877
|
+
opencode: OpenCodeConfig
|
|
723
878
|
server_host: str
|
|
724
879
|
server_port: int
|
|
725
880
|
server_base_path: str
|
|
@@ -950,9 +1105,19 @@ def _parse_app_server_config(
|
|
|
950
1105
|
cfg = cfg if isinstance(cfg, dict) else {}
|
|
951
1106
|
command = _parse_command(cfg.get("command", defaults.get("command")))
|
|
952
1107
|
state_root_raw = cfg.get("state_root", defaults.get("state_root"))
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1108
|
+
if state_root_raw is None:
|
|
1109
|
+
raise ConfigError("app_server.state_root is required")
|
|
1110
|
+
state_root = resolve_config_path(
|
|
1111
|
+
state_root_raw,
|
|
1112
|
+
root,
|
|
1113
|
+
allow_home=True,
|
|
1114
|
+
scope="app_server.state_root",
|
|
1115
|
+
)
|
|
1116
|
+
auto_restart_raw = cfg.get("auto_restart", defaults.get("auto_restart"))
|
|
1117
|
+
if auto_restart_raw is None:
|
|
1118
|
+
auto_restart = None
|
|
1119
|
+
else:
|
|
1120
|
+
auto_restart = bool(auto_restart_raw)
|
|
956
1121
|
max_handles_raw = cfg.get("max_handles", defaults.get("max_handles"))
|
|
957
1122
|
max_handles = int(max_handles_raw) if max_handles_raw is not None else None
|
|
958
1123
|
if max_handles is not None and max_handles <= 0:
|
|
@@ -969,29 +1134,119 @@ def _parse_app_server_config(
|
|
|
969
1134
|
)
|
|
970
1135
|
if turn_timeout_seconds is not None and turn_timeout_seconds <= 0:
|
|
971
1136
|
turn_timeout_seconds = None
|
|
1137
|
+
stall_timeout_raw = cfg.get(
|
|
1138
|
+
"turn_stall_timeout_seconds", defaults.get("turn_stall_timeout_seconds")
|
|
1139
|
+
)
|
|
1140
|
+
turn_stall_timeout_seconds = (
|
|
1141
|
+
float(stall_timeout_raw) if stall_timeout_raw is not None else None
|
|
1142
|
+
)
|
|
1143
|
+
if turn_stall_timeout_seconds is not None and turn_stall_timeout_seconds <= 0:
|
|
1144
|
+
turn_stall_timeout_seconds = None
|
|
1145
|
+
stall_poll_raw = cfg.get(
|
|
1146
|
+
"turn_stall_poll_interval_seconds",
|
|
1147
|
+
defaults.get("turn_stall_poll_interval_seconds"),
|
|
1148
|
+
)
|
|
1149
|
+
turn_stall_poll_interval_seconds = (
|
|
1150
|
+
float(stall_poll_raw) if stall_poll_raw is not None else None
|
|
1151
|
+
)
|
|
1152
|
+
if (
|
|
1153
|
+
turn_stall_poll_interval_seconds is not None
|
|
1154
|
+
and turn_stall_poll_interval_seconds <= 0
|
|
1155
|
+
):
|
|
1156
|
+
turn_stall_poll_interval_seconds = defaults.get(
|
|
1157
|
+
"turn_stall_poll_interval_seconds"
|
|
1158
|
+
)
|
|
1159
|
+
stall_recovery_raw = cfg.get(
|
|
1160
|
+
"turn_stall_recovery_min_interval_seconds",
|
|
1161
|
+
defaults.get("turn_stall_recovery_min_interval_seconds"),
|
|
1162
|
+
)
|
|
1163
|
+
turn_stall_recovery_min_interval_seconds = (
|
|
1164
|
+
float(stall_recovery_raw) if stall_recovery_raw is not None else None
|
|
1165
|
+
)
|
|
1166
|
+
if (
|
|
1167
|
+
turn_stall_recovery_min_interval_seconds is not None
|
|
1168
|
+
and turn_stall_recovery_min_interval_seconds < 0
|
|
1169
|
+
):
|
|
1170
|
+
turn_stall_recovery_min_interval_seconds = defaults.get(
|
|
1171
|
+
"turn_stall_recovery_min_interval_seconds"
|
|
1172
|
+
)
|
|
972
1173
|
request_timeout_raw = cfg.get("request_timeout", defaults.get("request_timeout"))
|
|
973
1174
|
request_timeout = (
|
|
974
1175
|
float(request_timeout_raw) if request_timeout_raw is not None else None
|
|
975
1176
|
)
|
|
976
1177
|
if request_timeout is not None and request_timeout <= 0:
|
|
977
1178
|
request_timeout = None
|
|
1179
|
+
client_defaults = defaults.get("client")
|
|
1180
|
+
client_defaults = client_defaults if isinstance(client_defaults, dict) else {}
|
|
1181
|
+
client_cfg_raw = cfg.get("client")
|
|
1182
|
+
client_cfg = client_cfg_raw if isinstance(client_cfg_raw, dict) else {}
|
|
1183
|
+
|
|
1184
|
+
def _client_int(key: str) -> int:
|
|
1185
|
+
value = client_cfg.get(key, client_defaults.get(key))
|
|
1186
|
+
value = int(value) if value is not None else 0
|
|
1187
|
+
if value <= 0:
|
|
1188
|
+
value = int(client_defaults.get(key) or 0)
|
|
1189
|
+
return value
|
|
1190
|
+
|
|
1191
|
+
def _client_float(key: str, *, allow_zero: bool = False) -> float:
|
|
1192
|
+
value = client_cfg.get(key, client_defaults.get(key))
|
|
1193
|
+
value = float(value) if value is not None else 0.0
|
|
1194
|
+
if value < 0 or (not allow_zero and value <= 0):
|
|
1195
|
+
value = float(client_defaults.get(key) or 0.0)
|
|
1196
|
+
return value
|
|
1197
|
+
|
|
978
1198
|
prompt_defaults = defaults.get("prompts")
|
|
979
1199
|
prompts = _parse_app_server_prompts_config(cfg.get("prompts"), prompt_defaults)
|
|
980
1200
|
return AppServerConfig(
|
|
981
1201
|
command=command,
|
|
982
1202
|
state_root=state_root,
|
|
1203
|
+
auto_restart=auto_restart,
|
|
983
1204
|
max_handles=max_handles,
|
|
984
1205
|
idle_ttl_seconds=idle_ttl_seconds,
|
|
985
1206
|
turn_timeout_seconds=turn_timeout_seconds,
|
|
1207
|
+
turn_stall_timeout_seconds=turn_stall_timeout_seconds,
|
|
1208
|
+
turn_stall_poll_interval_seconds=turn_stall_poll_interval_seconds,
|
|
1209
|
+
turn_stall_recovery_min_interval_seconds=turn_stall_recovery_min_interval_seconds,
|
|
986
1210
|
request_timeout=request_timeout,
|
|
1211
|
+
client=AppServerClientConfig(
|
|
1212
|
+
max_message_bytes=_client_int("max_message_bytes"),
|
|
1213
|
+
oversize_preview_bytes=_client_int("oversize_preview_bytes"),
|
|
1214
|
+
max_oversize_drain_bytes=_client_int("max_oversize_drain_bytes"),
|
|
1215
|
+
restart_backoff_initial_seconds=_client_float(
|
|
1216
|
+
"restart_backoff_initial_seconds"
|
|
1217
|
+
),
|
|
1218
|
+
restart_backoff_max_seconds=_client_float("restart_backoff_max_seconds"),
|
|
1219
|
+
restart_backoff_jitter_ratio=_client_float(
|
|
1220
|
+
"restart_backoff_jitter_ratio", allow_zero=True
|
|
1221
|
+
),
|
|
1222
|
+
),
|
|
987
1223
|
prompts=prompts,
|
|
988
1224
|
)
|
|
989
1225
|
|
|
990
1226
|
|
|
1227
|
+
def _parse_opencode_config(
|
|
1228
|
+
cfg: Optional[Dict[str, Any]],
|
|
1229
|
+
_root: Path,
|
|
1230
|
+
defaults: Optional[Dict[str, Any]],
|
|
1231
|
+
) -> OpenCodeConfig:
|
|
1232
|
+
cfg = cfg if isinstance(cfg, dict) else {}
|
|
1233
|
+
defaults = defaults if isinstance(defaults, dict) else {}
|
|
1234
|
+
stall_timeout_raw = cfg.get(
|
|
1235
|
+
"session_stall_timeout_seconds",
|
|
1236
|
+
defaults.get("session_stall_timeout_seconds"),
|
|
1237
|
+
)
|
|
1238
|
+
stall_timeout_seconds = (
|
|
1239
|
+
float(stall_timeout_raw) if stall_timeout_raw is not None else None
|
|
1240
|
+
)
|
|
1241
|
+
if stall_timeout_seconds is not None and stall_timeout_seconds <= 0:
|
|
1242
|
+
stall_timeout_seconds = None
|
|
1243
|
+
return OpenCodeConfig(session_stall_timeout_seconds=stall_timeout_seconds)
|
|
1244
|
+
|
|
1245
|
+
|
|
991
1246
|
def _parse_agents_config(
|
|
992
|
-
cfg: Dict[str, Any], defaults: Dict[str, Any]
|
|
1247
|
+
cfg: Optional[Dict[str, Any]], defaults: Dict[str, Any]
|
|
993
1248
|
) -> Dict[str, AgentConfig]:
|
|
994
|
-
raw_agents = cfg.get("agents")
|
|
1249
|
+
raw_agents = cfg.get("agents") if cfg else None
|
|
995
1250
|
if not isinstance(raw_agents, dict):
|
|
996
1251
|
raw_agents = defaults.get("agents", {})
|
|
997
1252
|
agents: Dict[str, AgentConfig] = {}
|
|
@@ -1004,9 +1259,15 @@ def _parse_agents_config(
|
|
|
1004
1259
|
serve_command = None
|
|
1005
1260
|
if "serve_command" in agent_cfg:
|
|
1006
1261
|
serve_command = _parse_command(agent_cfg.get("serve_command"))
|
|
1262
|
+
base_url = agent_cfg.get("base_url")
|
|
1263
|
+
subagent_models = agent_cfg.get("subagent_models")
|
|
1264
|
+
if not isinstance(subagent_models, dict):
|
|
1265
|
+
subagent_models = None
|
|
1007
1266
|
agents[str(agent_id)] = AgentConfig(
|
|
1008
|
-
binary=
|
|
1009
|
-
serve_command=serve_command
|
|
1267
|
+
binary=binary,
|
|
1268
|
+
serve_command=serve_command,
|
|
1269
|
+
base_url=base_url,
|
|
1270
|
+
subagent_models=subagent_models,
|
|
1010
1271
|
)
|
|
1011
1272
|
return agents
|
|
1012
1273
|
|
|
@@ -1019,9 +1280,14 @@ def _parse_static_assets_config(
|
|
|
1019
1280
|
if not isinstance(cfg, dict):
|
|
1020
1281
|
cfg = defaults
|
|
1021
1282
|
cache_root_raw = cfg.get("cache_root", defaults.get("cache_root"))
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1283
|
+
if cache_root_raw is None:
|
|
1284
|
+
raise ConfigError("static_assets.cache_root is required")
|
|
1285
|
+
cache_root = resolve_config_path(
|
|
1286
|
+
cache_root_raw,
|
|
1287
|
+
root,
|
|
1288
|
+
allow_home=True,
|
|
1289
|
+
scope="static_assets.cache_root",
|
|
1290
|
+
)
|
|
1025
1291
|
max_cache_entries = int(
|
|
1026
1292
|
cfg.get("max_cache_entries", defaults.get("max_cache_entries", 0))
|
|
1027
1293
|
)
|
|
@@ -1057,9 +1323,8 @@ def load_dotenv_for_root(root: Path) -> None:
|
|
|
1057
1323
|
# Prefer repo-local .env over inherited process env to avoid stale keys
|
|
1058
1324
|
# (common when running via launchd/daemon or with a global shell export).
|
|
1059
1325
|
load_dotenv(dotenv_path=candidate, override=True)
|
|
1060
|
-
except
|
|
1061
|
-
|
|
1062
|
-
pass
|
|
1326
|
+
except OSError as exc:
|
|
1327
|
+
logger.debug("Failed to load .env file: %s", exc)
|
|
1063
1328
|
|
|
1064
1329
|
|
|
1065
1330
|
def _parse_dotenv_fallback(path: Path) -> Dict[str, str]:
|
|
@@ -1126,6 +1391,21 @@ def load_hub_config_data(config_path: Path) -> Dict[str, Any]:
|
|
|
1126
1391
|
|
|
1127
1392
|
def _resolve_hub_config_path(start: Path) -> Path:
|
|
1128
1393
|
config_path = find_nearest_hub_config_path(start)
|
|
1394
|
+
if not config_path:
|
|
1395
|
+
# Auto-initialize hub config if missing in the current directory or parents.
|
|
1396
|
+
# If we are in a git repo, we'll initialize a hub there.
|
|
1397
|
+
try:
|
|
1398
|
+
from .utils import find_repo_root
|
|
1399
|
+
|
|
1400
|
+
target_root = find_repo_root(start)
|
|
1401
|
+
except Exception:
|
|
1402
|
+
target_root = start
|
|
1403
|
+
|
|
1404
|
+
from ..bootstrap import seed_hub_files
|
|
1405
|
+
|
|
1406
|
+
seed_hub_files(target_root)
|
|
1407
|
+
config_path = find_nearest_hub_config_path(target_root)
|
|
1408
|
+
|
|
1129
1409
|
if not config_path:
|
|
1130
1410
|
raise ConfigError(
|
|
1131
1411
|
f"Missing hub config file; expected to find {CONFIG_FILENAME} in {start} or parents (use --hub to specify)"
|
|
@@ -1171,7 +1451,7 @@ def derive_repo_config(
|
|
|
1171
1451
|
def _resolve_repo_root(start: Path) -> Path:
|
|
1172
1452
|
search_dir = start.resolve() if start.is_dir() else start.resolve().parent
|
|
1173
1453
|
for current in [search_dir] + list(search_dir.parents):
|
|
1174
|
-
if (current / ".codex-autorunner" / "state.
|
|
1454
|
+
if (current / ".codex-autorunner" / "state.sqlite3").exists():
|
|
1175
1455
|
return current
|
|
1176
1456
|
if (current / ".git").exists():
|
|
1177
1457
|
return current
|
|
@@ -1191,15 +1471,9 @@ def load_repo_config(start: Path, hub_path: Optional[Path] = None) -> RepoConfig
|
|
|
1191
1471
|
def _build_repo_config(config_path: Path, cfg: Dict[str, Any]) -> RepoConfig:
|
|
1192
1472
|
root = config_path.parent.parent.resolve()
|
|
1193
1473
|
docs = {
|
|
1194
|
-
"
|
|
1195
|
-
"
|
|
1196
|
-
"opinions": Path(cfg["docs"]["opinions"]),
|
|
1474
|
+
"active_context": Path(cfg["docs"]["active_context"]),
|
|
1475
|
+
"decisions": Path(cfg["docs"]["decisions"]),
|
|
1197
1476
|
"spec": Path(cfg["docs"]["spec"]),
|
|
1198
|
-
"summary": Path(cfg["docs"]["summary"]),
|
|
1199
|
-
"snapshot": Path(cfg["docs"].get("snapshot", ".codex-autorunner/SNAPSHOT.md")),
|
|
1200
|
-
"snapshot_state": Path(
|
|
1201
|
-
cfg["docs"].get("snapshot_state", ".codex-autorunner/snapshot_state.json")
|
|
1202
|
-
),
|
|
1203
1477
|
}
|
|
1204
1478
|
voice_cfg = cfg.get("voice") if isinstance(cfg.get("voice"), dict) else {}
|
|
1205
1479
|
voice_cfg = cast(Dict[str, Any], voice_cfg)
|
|
@@ -1220,12 +1494,19 @@ def _build_repo_config(config_path: Path, cfg: Dict[str, Any]) -> RepoConfig:
|
|
|
1220
1494
|
cfg.get("notifications") if isinstance(cfg.get("notifications"), dict) else {}
|
|
1221
1495
|
)
|
|
1222
1496
|
notifications_cfg = cast(Dict[str, Any], notifications_cfg)
|
|
1497
|
+
security_cfg = cfg.get("security") if isinstance(cfg.get("security"), dict) else {}
|
|
1498
|
+
security_cfg = cast(Dict[str, Any], security_cfg)
|
|
1223
1499
|
log_cfg = cfg.get("log", {})
|
|
1224
1500
|
log_cfg = cast(Dict[str, Any], log_cfg if isinstance(log_cfg, dict) else {})
|
|
1225
1501
|
server_log_cfg = cfg.get("server_log", {}) or {}
|
|
1226
1502
|
server_log_cfg = cast(
|
|
1227
1503
|
Dict[str, Any], server_log_cfg if isinstance(server_log_cfg, dict) else {}
|
|
1228
1504
|
)
|
|
1505
|
+
update_cfg = cfg.get("update")
|
|
1506
|
+
update_cfg = cast(
|
|
1507
|
+
Dict[str, Any], update_cfg if isinstance(update_cfg, dict) else {}
|
|
1508
|
+
)
|
|
1509
|
+
update_skip_checks = bool(update_cfg.get("skip_checks", False))
|
|
1229
1510
|
return RepoConfig(
|
|
1230
1511
|
raw=cfg,
|
|
1231
1512
|
root=root,
|
|
@@ -1243,13 +1524,20 @@ def _build_repo_config(config_path: Path, cfg: Dict[str, Any]) -> RepoConfig:
|
|
|
1243
1524
|
runner_sleep_seconds=int(cfg["runner"]["sleep_seconds"]),
|
|
1244
1525
|
runner_stop_after_runs=cfg["runner"].get("stop_after_runs"),
|
|
1245
1526
|
runner_max_wallclock_seconds=cfg["runner"].get("max_wallclock_seconds"),
|
|
1527
|
+
runner_no_progress_threshold=int(cfg["runner"].get("no_progress_threshold", 3)),
|
|
1246
1528
|
git_auto_commit=bool(cfg["git"].get("auto_commit", False)),
|
|
1247
1529
|
git_commit_message_template=str(cfg["git"].get("commit_message_template")),
|
|
1530
|
+
update_skip_checks=update_skip_checks,
|
|
1531
|
+
ticket_flow=cast(Dict[str, Any], cfg.get("ticket_flow") or {}),
|
|
1248
1532
|
app_server=_parse_app_server_config(
|
|
1249
1533
|
cfg.get("app_server"),
|
|
1250
1534
|
root,
|
|
1251
1535
|
DEFAULT_REPO_CONFIG["app_server"],
|
|
1252
1536
|
),
|
|
1537
|
+
opencode=_parse_opencode_config(
|
|
1538
|
+
cfg.get("opencode"), root, DEFAULT_REPO_CONFIG.get("opencode")
|
|
1539
|
+
),
|
|
1540
|
+
security=security_cfg,
|
|
1253
1541
|
server_host=str(cfg["server"].get("host")),
|
|
1254
1542
|
server_port=int(cfg["server"].get("port")),
|
|
1255
1543
|
server_base_path=_normalize_base_path(cfg["server"].get("base_path", "")),
|
|
@@ -1303,6 +1591,29 @@ def _build_hub_config(config_path: Path, cfg: Dict[str, Any]) -> HubConfig:
|
|
|
1303
1591
|
"max_bytes": log_cfg["max_bytes"],
|
|
1304
1592
|
"backup_count": log_cfg["backup_count"],
|
|
1305
1593
|
}
|
|
1594
|
+
|
|
1595
|
+
log_path_str = log_cfg["path"]
|
|
1596
|
+
try:
|
|
1597
|
+
log_path = resolve_config_path(log_path_str, root, scope="log.path")
|
|
1598
|
+
except ConfigPathError as exc:
|
|
1599
|
+
raise ConfigError(str(exc)) from exc
|
|
1600
|
+
|
|
1601
|
+
server_log_path_str = str(server_log_cfg.get("path", log_cfg["path"]))
|
|
1602
|
+
try:
|
|
1603
|
+
server_log_path = resolve_config_path(
|
|
1604
|
+
server_log_path_str,
|
|
1605
|
+
root,
|
|
1606
|
+
scope="server_log.path",
|
|
1607
|
+
)
|
|
1608
|
+
except ConfigPathError as exc:
|
|
1609
|
+
raise ConfigError(str(exc)) from exc
|
|
1610
|
+
|
|
1611
|
+
update_cfg = cfg.get("update")
|
|
1612
|
+
update_cfg = cast(
|
|
1613
|
+
Dict[str, Any], update_cfg if isinstance(update_cfg, dict) else {}
|
|
1614
|
+
)
|
|
1615
|
+
update_skip_checks = bool(update_cfg.get("skip_checks", False))
|
|
1616
|
+
|
|
1306
1617
|
return HubConfig(
|
|
1307
1618
|
raw=cfg,
|
|
1308
1619
|
root=root,
|
|
@@ -1318,11 +1629,15 @@ def _build_hub_config(config_path: Path, cfg: Dict[str, Any]) -> HubConfig:
|
|
|
1318
1629
|
repo_server_inherit=bool(hub_cfg.get("repo_server_inherit", True)),
|
|
1319
1630
|
update_repo_url=str(hub_cfg.get("update_repo_url", "")),
|
|
1320
1631
|
update_repo_ref=str(hub_cfg.get("update_repo_ref", "main")),
|
|
1632
|
+
update_skip_checks=update_skip_checks,
|
|
1321
1633
|
app_server=_parse_app_server_config(
|
|
1322
1634
|
cfg.get("app_server"),
|
|
1323
1635
|
root,
|
|
1324
1636
|
DEFAULT_HUB_CONFIG["app_server"],
|
|
1325
1637
|
),
|
|
1638
|
+
opencode=_parse_opencode_config(
|
|
1639
|
+
cfg.get("opencode"), root, DEFAULT_HUB_CONFIG.get("opencode")
|
|
1640
|
+
),
|
|
1326
1641
|
server_host=str(cfg["server"]["host"]),
|
|
1327
1642
|
server_port=int(cfg["server"]["port"]),
|
|
1328
1643
|
server_base_path=_normalize_base_path(cfg["server"].get("base_path", "")),
|
|
@@ -1331,12 +1646,12 @@ def _build_hub_config(config_path: Path, cfg: Dict[str, Any]) -> HubConfig:
|
|
|
1331
1646
|
server_allowed_hosts=list(cfg["server"].get("allowed_hosts") or []),
|
|
1332
1647
|
server_allowed_origins=list(cfg["server"].get("allowed_origins") or []),
|
|
1333
1648
|
log=LogConfig(
|
|
1334
|
-
path=
|
|
1649
|
+
path=log_path,
|
|
1335
1650
|
max_bytes=int(log_cfg["max_bytes"]),
|
|
1336
1651
|
backup_count=int(log_cfg["backup_count"]),
|
|
1337
1652
|
),
|
|
1338
1653
|
server_log=LogConfig(
|
|
1339
|
-
path=
|
|
1654
|
+
path=server_log_path,
|
|
1340
1655
|
max_bytes=int(server_log_cfg.get("max_bytes", log_cfg["max_bytes"])),
|
|
1341
1656
|
backup_count=int(
|
|
1342
1657
|
server_log_cfg.get("backup_count", log_cfg["backup_count"])
|
|
@@ -1402,6 +1717,12 @@ def _validate_app_server_config(cfg: Dict[str, Any]) -> None:
|
|
|
1402
1717
|
app_server_cfg.get("state_root", ""), str
|
|
1403
1718
|
):
|
|
1404
1719
|
raise ConfigError("app_server.state_root must be a string path")
|
|
1720
|
+
if (
|
|
1721
|
+
"auto_restart" in app_server_cfg
|
|
1722
|
+
and app_server_cfg.get("auto_restart") is not None
|
|
1723
|
+
):
|
|
1724
|
+
if not isinstance(app_server_cfg.get("auto_restart"), bool):
|
|
1725
|
+
raise ConfigError("app_server.auto_restart must be boolean or null")
|
|
1405
1726
|
for key in ("max_handles", "idle_ttl_seconds"):
|
|
1406
1727
|
if key in app_server_cfg and app_server_cfg.get(key) is not None:
|
|
1407
1728
|
if not isinstance(app_server_cfg.get(key), int):
|
|
@@ -1420,6 +1741,47 @@ def _validate_app_server_config(cfg: Dict[str, Any]) -> None:
|
|
|
1420
1741
|
):
|
|
1421
1742
|
if not isinstance(app_server_cfg.get("request_timeout"), (int, float)):
|
|
1422
1743
|
raise ConfigError("app_server.request_timeout must be a number or null")
|
|
1744
|
+
for key in (
|
|
1745
|
+
"turn_stall_timeout_seconds",
|
|
1746
|
+
"turn_stall_poll_interval_seconds",
|
|
1747
|
+
"turn_stall_recovery_min_interval_seconds",
|
|
1748
|
+
):
|
|
1749
|
+
if key in app_server_cfg and app_server_cfg.get(key) is not None:
|
|
1750
|
+
if not isinstance(app_server_cfg.get(key), (int, float)):
|
|
1751
|
+
raise ConfigError(f"app_server.{key} must be a number or null")
|
|
1752
|
+
client_cfg = app_server_cfg.get("client")
|
|
1753
|
+
if client_cfg is not None:
|
|
1754
|
+
if not isinstance(client_cfg, dict):
|
|
1755
|
+
raise ConfigError("app_server.client must be a mapping if provided")
|
|
1756
|
+
for key in (
|
|
1757
|
+
"max_message_bytes",
|
|
1758
|
+
"oversize_preview_bytes",
|
|
1759
|
+
"max_oversize_drain_bytes",
|
|
1760
|
+
):
|
|
1761
|
+
if key in client_cfg:
|
|
1762
|
+
value = client_cfg.get(key)
|
|
1763
|
+
if not isinstance(value, int):
|
|
1764
|
+
raise ConfigError(f"app_server.client.{key} must be an integer")
|
|
1765
|
+
if value <= 0:
|
|
1766
|
+
raise ConfigError(f"app_server.client.{key} must be > 0")
|
|
1767
|
+
for key in (
|
|
1768
|
+
"restart_backoff_initial_seconds",
|
|
1769
|
+
"restart_backoff_max_seconds",
|
|
1770
|
+
"restart_backoff_jitter_ratio",
|
|
1771
|
+
):
|
|
1772
|
+
if key in client_cfg:
|
|
1773
|
+
value = client_cfg.get(key)
|
|
1774
|
+
if not isinstance(value, (int, float)):
|
|
1775
|
+
raise ConfigError(
|
|
1776
|
+
f"app_server.client.{key} must be a number if provided"
|
|
1777
|
+
)
|
|
1778
|
+
if key == "restart_backoff_jitter_ratio":
|
|
1779
|
+
if value < 0:
|
|
1780
|
+
raise ConfigError(
|
|
1781
|
+
"app_server.client.restart_backoff_jitter_ratio must be >= 0"
|
|
1782
|
+
)
|
|
1783
|
+
elif value <= 0:
|
|
1784
|
+
raise ConfigError(f"app_server.client.{key} must be > 0")
|
|
1423
1785
|
prompts = app_server_cfg.get("prompts")
|
|
1424
1786
|
if prompts is not None:
|
|
1425
1787
|
if not isinstance(prompts, dict):
|
|
@@ -1463,6 +1825,35 @@ def _validate_app_server_config(cfg: Dict[str, Any]) -> None:
|
|
|
1463
1825
|
)
|
|
1464
1826
|
|
|
1465
1827
|
|
|
1828
|
+
def _validate_opencode_config(cfg: Dict[str, Any]) -> None:
|
|
1829
|
+
opencode_cfg = cfg.get("opencode")
|
|
1830
|
+
if opencode_cfg is None:
|
|
1831
|
+
return
|
|
1832
|
+
if not isinstance(opencode_cfg, dict):
|
|
1833
|
+
raise ConfigError("opencode section must be a mapping if provided")
|
|
1834
|
+
if (
|
|
1835
|
+
"session_stall_timeout_seconds" in opencode_cfg
|
|
1836
|
+
and opencode_cfg.get("session_stall_timeout_seconds") is not None
|
|
1837
|
+
):
|
|
1838
|
+
if not isinstance(
|
|
1839
|
+
opencode_cfg.get("session_stall_timeout_seconds"), (int, float)
|
|
1840
|
+
):
|
|
1841
|
+
raise ConfigError(
|
|
1842
|
+
"opencode.session_stall_timeout_seconds must be a number or null"
|
|
1843
|
+
)
|
|
1844
|
+
|
|
1845
|
+
|
|
1846
|
+
def _validate_update_config(cfg: Dict[str, Any]) -> None:
|
|
1847
|
+
update_cfg = cfg.get("update")
|
|
1848
|
+
if update_cfg is None:
|
|
1849
|
+
return
|
|
1850
|
+
if not isinstance(update_cfg, dict):
|
|
1851
|
+
raise ConfigError("update section must be a mapping if provided")
|
|
1852
|
+
if "skip_checks" in update_cfg and update_cfg.get("skip_checks") is not None:
|
|
1853
|
+
if not isinstance(update_cfg.get("skip_checks"), bool):
|
|
1854
|
+
raise ConfigError("update.skip_checks must be boolean or null")
|
|
1855
|
+
|
|
1856
|
+
|
|
1466
1857
|
def _validate_agents_config(cfg: Dict[str, Any]) -> None:
|
|
1467
1858
|
agents_cfg = cfg.get("agents")
|
|
1468
1859
|
if agents_cfg is None:
|
|
@@ -1491,18 +1882,15 @@ def _validate_repo_config(cfg: Dict[str, Any], *, root: Path) -> None:
|
|
|
1491
1882
|
for key, value in docs.items():
|
|
1492
1883
|
if not isinstance(value, str) or not value:
|
|
1493
1884
|
raise ConfigError(f"docs.{key} must be a non-empty string path")
|
|
1494
|
-
path = Path(value)
|
|
1495
|
-
if path.is_absolute():
|
|
1496
|
-
raise ConfigError(f"docs.{key} must be a relative path under repo root")
|
|
1497
|
-
if ".." in path.parts:
|
|
1498
|
-
raise ConfigError(f"docs.{key} must not contain '..' segments")
|
|
1499
1885
|
try:
|
|
1500
|
-
(
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
f"docs.{key}
|
|
1504
|
-
)
|
|
1505
|
-
|
|
1886
|
+
resolve_config_path(
|
|
1887
|
+
value,
|
|
1888
|
+
root,
|
|
1889
|
+
scope=f"docs.{key}",
|
|
1890
|
+
)
|
|
1891
|
+
except ConfigPathError as exc:
|
|
1892
|
+
raise ConfigError(str(exc)) from exc
|
|
1893
|
+
for key in ("active_context", "decisions", "spec"):
|
|
1506
1894
|
if not isinstance(docs.get(key), str) or not docs[key]:
|
|
1507
1895
|
raise ConfigError(f"docs.{key} must be a non-empty string path")
|
|
1508
1896
|
_validate_agents_config(cfg)
|
|
@@ -1555,6 +1943,18 @@ def _validate_repo_config(cfg: Dict[str, Any], *, root: Path) -> None:
|
|
|
1555
1943
|
val = runner.get(k)
|
|
1556
1944
|
if val is not None and not isinstance(val, int):
|
|
1557
1945
|
raise ConfigError(f"runner.{k} must be an integer or null")
|
|
1946
|
+
ticket_flow_cfg = cfg.get("ticket_flow")
|
|
1947
|
+
if ticket_flow_cfg is not None and not isinstance(ticket_flow_cfg, dict):
|
|
1948
|
+
raise ConfigError("ticket_flow section must be a mapping if provided")
|
|
1949
|
+
if isinstance(ticket_flow_cfg, dict):
|
|
1950
|
+
if "approval_mode" in ticket_flow_cfg and not isinstance(
|
|
1951
|
+
ticket_flow_cfg.get("approval_mode"), str
|
|
1952
|
+
):
|
|
1953
|
+
raise ConfigError("ticket_flow.approval_mode must be a string")
|
|
1954
|
+
if "default_approval_decision" in ticket_flow_cfg and not isinstance(
|
|
1955
|
+
ticket_flow_cfg.get("default_approval_decision"), str
|
|
1956
|
+
):
|
|
1957
|
+
raise ConfigError("ticket_flow.default_approval_decision must be a string")
|
|
1558
1958
|
git = cfg.get("git")
|
|
1559
1959
|
if not isinstance(git, dict):
|
|
1560
1960
|
raise ConfigError("git section must be a mapping")
|
|
@@ -1578,69 +1978,7 @@ def _validate_repo_config(cfg: Dict[str, Any], *, root: Path) -> None:
|
|
|
1578
1978
|
github.get("sync_agent_timeout_seconds"), int
|
|
1579
1979
|
):
|
|
1580
1980
|
raise ConfigError("github.sync_agent_timeout_seconds must be an integer")
|
|
1581
|
-
|
|
1582
|
-
if pr_flow is not None:
|
|
1583
|
-
if not isinstance(pr_flow, dict):
|
|
1584
|
-
raise ConfigError("github.pr_flow must be a mapping if provided")
|
|
1585
|
-
if "enabled" in pr_flow and not isinstance(pr_flow.get("enabled"), bool):
|
|
1586
|
-
raise ConfigError("github.pr_flow.enabled must be boolean")
|
|
1587
|
-
if "max_cycles" in pr_flow and not isinstance(
|
|
1588
|
-
pr_flow.get("max_cycles"), int
|
|
1589
|
-
):
|
|
1590
|
-
raise ConfigError("github.pr_flow.max_cycles must be an integer")
|
|
1591
|
-
if "stop_condition" in pr_flow and not isinstance(
|
|
1592
|
-
pr_flow.get("stop_condition"), str
|
|
1593
|
-
):
|
|
1594
|
-
raise ConfigError("github.pr_flow.stop_condition must be a string")
|
|
1595
|
-
for key in ("max_implementation_runs", "max_wallclock_seconds"):
|
|
1596
|
-
val = pr_flow.get(key)
|
|
1597
|
-
if val is not None and not isinstance(val, int):
|
|
1598
|
-
raise ConfigError(
|
|
1599
|
-
f"github.pr_flow.{key} must be an integer or null"
|
|
1600
|
-
)
|
|
1601
|
-
review_cfg = pr_flow.get("review")
|
|
1602
|
-
if review_cfg is not None:
|
|
1603
|
-
if not isinstance(review_cfg, dict):
|
|
1604
|
-
raise ConfigError("github.pr_flow.review must be a mapping")
|
|
1605
|
-
for key in ("include_codex", "include_github", "include_checks"):
|
|
1606
|
-
if key in review_cfg and not isinstance(review_cfg.get(key), bool):
|
|
1607
|
-
raise ConfigError(
|
|
1608
|
-
f"github.pr_flow.review.{key} must be boolean"
|
|
1609
|
-
)
|
|
1610
|
-
chatops_cfg = pr_flow.get("chatops")
|
|
1611
|
-
if chatops_cfg is not None:
|
|
1612
|
-
if not isinstance(chatops_cfg, dict):
|
|
1613
|
-
raise ConfigError("github.pr_flow.chatops must be a mapping")
|
|
1614
|
-
if "enabled" in chatops_cfg and not isinstance(
|
|
1615
|
-
chatops_cfg.get("enabled"), bool
|
|
1616
|
-
):
|
|
1617
|
-
raise ConfigError("github.pr_flow.chatops.enabled must be boolean")
|
|
1618
|
-
if "poll_interval_seconds" in chatops_cfg and not isinstance(
|
|
1619
|
-
chatops_cfg.get("poll_interval_seconds"), int
|
|
1620
|
-
):
|
|
1621
|
-
raise ConfigError(
|
|
1622
|
-
"github.pr_flow.chatops.poll_interval_seconds must be an integer"
|
|
1623
|
-
)
|
|
1624
|
-
for key in ("allow_users", "allow_associations"):
|
|
1625
|
-
if key in chatops_cfg and not isinstance(
|
|
1626
|
-
chatops_cfg.get(key), list
|
|
1627
|
-
):
|
|
1628
|
-
raise ConfigError(
|
|
1629
|
-
f"github.pr_flow.chatops.{key} must be a list"
|
|
1630
|
-
)
|
|
1631
|
-
if "ignore_bots" in chatops_cfg and not isinstance(
|
|
1632
|
-
chatops_cfg.get("ignore_bots"), bool
|
|
1633
|
-
):
|
|
1634
|
-
raise ConfigError(
|
|
1635
|
-
"github.pr_flow.chatops.ignore_bots must be boolean"
|
|
1636
|
-
)
|
|
1637
|
-
if chatops_cfg.get("enabled", False):
|
|
1638
|
-
allow_users = chatops_cfg.get("allow_users") or []
|
|
1639
|
-
allow_assoc = chatops_cfg.get("allow_associations") or []
|
|
1640
|
-
if not allow_users and not allow_assoc:
|
|
1641
|
-
raise ConfigError(
|
|
1642
|
-
"github.pr_flow.chatops.enabled requires at least one of allow_users or allow_associations to be non-empty"
|
|
1643
|
-
)
|
|
1981
|
+
|
|
1644
1982
|
server = cfg.get("server")
|
|
1645
1983
|
if not isinstance(server, dict):
|
|
1646
1984
|
raise ConfigError("server section must be a mapping")
|
|
@@ -1658,6 +1996,8 @@ def _validate_repo_config(cfg: Dict[str, Any], *, root: Path) -> None:
|
|
|
1658
1996
|
raise ConfigError("server.auth_token_env must be a string if provided")
|
|
1659
1997
|
_validate_server_security(server)
|
|
1660
1998
|
_validate_app_server_config(cfg)
|
|
1999
|
+
_validate_opencode_config(cfg)
|
|
2000
|
+
_validate_update_config(cfg)
|
|
1661
2001
|
notifications_cfg = cfg.get("notifications")
|
|
1662
2002
|
if notifications_cfg is not None:
|
|
1663
2003
|
if not isinstance(notifications_cfg, dict):
|
|
@@ -1760,9 +2100,13 @@ def _validate_repo_config(cfg: Dict[str, Any], *, root: Path) -> None:
|
|
|
1760
2100
|
log_cfg = cfg.get("log")
|
|
1761
2101
|
if not isinstance(log_cfg, dict):
|
|
1762
2102
|
raise ConfigError("log section must be a mapping")
|
|
1763
|
-
|
|
1764
|
-
if not isinstance(log_cfg
|
|
1765
|
-
raise ConfigError(
|
|
2103
|
+
if "path" in log_cfg:
|
|
2104
|
+
if not isinstance(log_cfg["path"], str):
|
|
2105
|
+
raise ConfigError("log.path must be a string path")
|
|
2106
|
+
try:
|
|
2107
|
+
resolve_config_path(log_cfg["path"], root, scope="log.path")
|
|
2108
|
+
except ConfigPathError as exc:
|
|
2109
|
+
raise ConfigError(str(exc)) from exc
|
|
1766
2110
|
for key in ("max_bytes", "backup_count"):
|
|
1767
2111
|
if not isinstance(log_cfg.get(key, 0), int):
|
|
1768
2112
|
raise ConfigError(f"log.{key} must be an integer")
|
|
@@ -1770,10 +2114,15 @@ def _validate_repo_config(cfg: Dict[str, Any], *, root: Path) -> None:
|
|
|
1770
2114
|
if server_log_cfg is not None and not isinstance(server_log_cfg, dict):
|
|
1771
2115
|
raise ConfigError("server_log section must be a mapping or null")
|
|
1772
2116
|
if isinstance(server_log_cfg, dict):
|
|
1773
|
-
if "path" in server_log_cfg
|
|
1774
|
-
server_log_cfg
|
|
1775
|
-
|
|
1776
|
-
|
|
2117
|
+
if "path" in server_log_cfg:
|
|
2118
|
+
if not isinstance(server_log_cfg["path"], str):
|
|
2119
|
+
raise ConfigError("server_log.path must be a string path")
|
|
2120
|
+
try:
|
|
2121
|
+
resolve_config_path(
|
|
2122
|
+
server_log_cfg["path"], root, scope="server_log.path"
|
|
2123
|
+
)
|
|
2124
|
+
except ConfigPathError as exc:
|
|
2125
|
+
raise ConfigError(str(exc)) from exc
|
|
1777
2126
|
for key in ("max_bytes", "backup_count"):
|
|
1778
2127
|
if key in server_log_cfg and not isinstance(server_log_cfg.get(key), int):
|
|
1779
2128
|
raise ConfigError(f"server_log.{key} must be an integer")
|
|
@@ -1792,6 +2141,8 @@ def _validate_hub_config(cfg: Dict[str, Any]) -> None:
|
|
|
1792
2141
|
if "repo" in cfg:
|
|
1793
2142
|
raise ConfigError("repo section is no longer supported; use repo_defaults")
|
|
1794
2143
|
_validate_agents_config(cfg)
|
|
2144
|
+
_validate_opencode_config(cfg)
|
|
2145
|
+
_validate_update_config(cfg)
|
|
1795
2146
|
repo_defaults = cfg.get("repo_defaults")
|
|
1796
2147
|
if repo_defaults is not None:
|
|
1797
2148
|
if not isinstance(repo_defaults, dict):
|
|
@@ -1918,6 +2269,21 @@ def _validate_housekeeping_config(cfg: Dict[str, Any]) -> None:
|
|
|
1918
2269
|
)
|
|
1919
2270
|
if "path" in rule and not isinstance(rule.get("path"), str):
|
|
1920
2271
|
raise ConfigError(f"housekeeping.rules[{idx}].path must be a string")
|
|
2272
|
+
if "path" in rule:
|
|
2273
|
+
path_value = rule.get("path")
|
|
2274
|
+
if not isinstance(path_value, str) or not path_value:
|
|
2275
|
+
raise ConfigError(
|
|
2276
|
+
f"housekeeping.rules[{idx}].path must be a non-empty string path"
|
|
2277
|
+
)
|
|
2278
|
+
path = Path(path_value)
|
|
2279
|
+
if path.is_absolute():
|
|
2280
|
+
raise ConfigError(
|
|
2281
|
+
f"housekeeping.rules[{idx}].path must be relative or start with '~'"
|
|
2282
|
+
)
|
|
2283
|
+
if ".." in path.parts:
|
|
2284
|
+
raise ConfigError(
|
|
2285
|
+
f"housekeeping.rules[{idx}].path must not contain '..' segments"
|
|
2286
|
+
)
|
|
1921
2287
|
if "glob" in rule and not isinstance(rule.get("glob"), str):
|
|
1922
2288
|
raise ConfigError(
|
|
1923
2289
|
f"housekeeping.rules[{idx}].glob must be a string if provided"
|
|
@@ -2075,6 +2441,29 @@ def _validate_telegram_bot_config(cfg: Dict[str, Any]) -> None:
|
|
|
2075
2441
|
raise ConfigError(f"telegram_bot.shell.{key} must be an integer")
|
|
2076
2442
|
if isinstance(value, int) and value <= 0:
|
|
2077
2443
|
raise ConfigError(f"telegram_bot.shell.{key} must be greater than 0")
|
|
2444
|
+
cache_cfg = telegram_cfg.get("cache")
|
|
2445
|
+
if cache_cfg is not None and not isinstance(cache_cfg, dict):
|
|
2446
|
+
raise ConfigError("telegram_bot.cache must be a mapping if provided")
|
|
2447
|
+
if isinstance(cache_cfg, dict):
|
|
2448
|
+
for key in (
|
|
2449
|
+
"cleanup_interval_seconds",
|
|
2450
|
+
"coalesce_buffer_ttl_seconds",
|
|
2451
|
+
"media_batch_buffer_ttl_seconds",
|
|
2452
|
+
"model_pending_ttl_seconds",
|
|
2453
|
+
"pending_approval_ttl_seconds",
|
|
2454
|
+
"pending_question_ttl_seconds",
|
|
2455
|
+
"reasoning_buffer_ttl_seconds",
|
|
2456
|
+
"selection_state_ttl_seconds",
|
|
2457
|
+
"turn_preview_ttl_seconds",
|
|
2458
|
+
"progress_stream_ttl_seconds",
|
|
2459
|
+
"oversize_warning_ttl_seconds",
|
|
2460
|
+
"update_id_persist_interval_seconds",
|
|
2461
|
+
):
|
|
2462
|
+
value = cache_cfg.get(key)
|
|
2463
|
+
if value is not None and not isinstance(value, (int, float)):
|
|
2464
|
+
raise ConfigError(f"telegram_bot.cache.{key} must be a number")
|
|
2465
|
+
if isinstance(value, (int, float)) and value <= 0:
|
|
2466
|
+
raise ConfigError(f"telegram_bot.cache.{key} must be > 0")
|
|
2078
2467
|
command_reg_cfg = telegram_cfg.get("command_registration")
|
|
2079
2468
|
if command_reg_cfg is not None and not isinstance(command_reg_cfg, dict):
|
|
2080
2469
|
raise ConfigError("telegram_bot.command_registration must be a mapping")
|
|
@@ -2112,14 +2501,47 @@ def _validate_telegram_bot_config(cfg: Dict[str, Any]) -> None:
|
|
|
2112
2501
|
raise ConfigError(
|
|
2113
2502
|
"telegram_bot.command_registration.scopes.language_code must be a string or null"
|
|
2114
2503
|
)
|
|
2504
|
+
if "trigger_mode" in telegram_cfg and not isinstance(
|
|
2505
|
+
telegram_cfg.get("trigger_mode"), str
|
|
2506
|
+
):
|
|
2507
|
+
raise ConfigError("telegram_bot.trigger_mode must be a string")
|
|
2115
2508
|
if "state_file" in telegram_cfg and not isinstance(
|
|
2116
2509
|
telegram_cfg.get("state_file"), str
|
|
2117
2510
|
):
|
|
2118
2511
|
raise ConfigError("telegram_bot.state_file must be a string path")
|
|
2512
|
+
if (
|
|
2513
|
+
"opencode_command" in telegram_cfg
|
|
2514
|
+
and not isinstance(telegram_cfg.get("opencode_command"), (list, str))
|
|
2515
|
+
and telegram_cfg.get("opencode_command") is not None
|
|
2516
|
+
):
|
|
2517
|
+
raise ConfigError("telegram_bot.opencode_command must be a list or string")
|
|
2119
2518
|
if "app_server_command" in telegram_cfg and not isinstance(
|
|
2120
2519
|
telegram_cfg.get("app_server_command"), (list, str)
|
|
2121
2520
|
):
|
|
2122
2521
|
raise ConfigError("telegram_bot.app_server_command must be a list or string")
|
|
2522
|
+
app_server_cfg = telegram_cfg.get("app_server")
|
|
2523
|
+
if app_server_cfg is not None and not isinstance(app_server_cfg, dict):
|
|
2524
|
+
raise ConfigError("telegram_bot.app_server must be a mapping if provided")
|
|
2525
|
+
if isinstance(app_server_cfg, dict):
|
|
2526
|
+
if (
|
|
2527
|
+
"turn_timeout_seconds" in app_server_cfg
|
|
2528
|
+
and app_server_cfg.get("turn_timeout_seconds") is not None
|
|
2529
|
+
and not isinstance(app_server_cfg.get("turn_timeout_seconds"), (int, float))
|
|
2530
|
+
):
|
|
2531
|
+
raise ConfigError(
|
|
2532
|
+
"telegram_bot.app_server.turn_timeout_seconds must be a number or null"
|
|
2533
|
+
)
|
|
2534
|
+
agent_timeouts_cfg = telegram_cfg.get("agent_timeouts")
|
|
2535
|
+
if agent_timeouts_cfg is not None and not isinstance(agent_timeouts_cfg, dict):
|
|
2536
|
+
raise ConfigError("telegram_bot.agent_timeouts must be a mapping if provided")
|
|
2537
|
+
if isinstance(agent_timeouts_cfg, dict):
|
|
2538
|
+
for _key, value in agent_timeouts_cfg.items():
|
|
2539
|
+
if value is None:
|
|
2540
|
+
continue
|
|
2541
|
+
if not isinstance(value, (int, float)):
|
|
2542
|
+
raise ConfigError(
|
|
2543
|
+
"telegram_bot.agent_timeouts values must be numbers or null"
|
|
2544
|
+
)
|
|
2123
2545
|
polling_cfg = telegram_cfg.get("polling")
|
|
2124
2546
|
if polling_cfg is not None and not isinstance(polling_cfg, dict):
|
|
2125
2547
|
raise ConfigError("telegram_bot.polling must be a mapping if provided")
|