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
codex_autorunner/web/schemas.py
CHANGED
|
@@ -4,7 +4,7 @@ Pydantic request/response schemas for web and API routes.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
from typing import Any, Dict, List, Optional
|
|
7
|
+
from typing import Any, Dict, List, Literal, Optional
|
|
8
8
|
|
|
9
9
|
from pydantic import AliasChoices, BaseModel, ConfigDict, Field
|
|
10
10
|
|
|
@@ -13,6 +13,65 @@ class Payload(BaseModel):
|
|
|
13
13
|
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
class ResponseModel(BaseModel):
|
|
17
|
+
model_config = ConfigDict(extra="ignore")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WorkspaceWriteRequest(Payload):
|
|
21
|
+
content: str = ""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WorkspaceResponse(ResponseModel):
|
|
25
|
+
active_context: str
|
|
26
|
+
decisions: str
|
|
27
|
+
spec: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class WorkspaceFileItem(ResponseModel):
|
|
31
|
+
name: str
|
|
32
|
+
path: str
|
|
33
|
+
is_pinned: bool = False
|
|
34
|
+
modified_at: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class WorkspaceFileListResponse(ResponseModel):
|
|
38
|
+
files: List[WorkspaceFileItem]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class WorkspaceNode(ResponseModel):
|
|
42
|
+
name: str
|
|
43
|
+
path: str
|
|
44
|
+
type: Literal["file", "folder"]
|
|
45
|
+
is_pinned: bool = False
|
|
46
|
+
modified_at: Optional[str] = None
|
|
47
|
+
size: Optional[int] = None
|
|
48
|
+
children: Optional[List["WorkspaceNode"]] = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
WorkspaceNode.model_rebuild()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class WorkspaceTreeResponse(ResponseModel):
|
|
55
|
+
tree: List[WorkspaceNode]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class WorkspaceUploadedItem(ResponseModel):
|
|
59
|
+
filename: str
|
|
60
|
+
path: str
|
|
61
|
+
size: int
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class WorkspaceUploadResponse(ResponseModel):
|
|
65
|
+
status: str
|
|
66
|
+
uploaded: List[WorkspaceUploadedItem]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SpecIngestTicketsResponse(ResponseModel):
|
|
70
|
+
status: str
|
|
71
|
+
created: int
|
|
72
|
+
first_ticket_path: Optional[str] = None
|
|
73
|
+
|
|
74
|
+
|
|
16
75
|
class RunControlRequest(Payload):
|
|
17
76
|
once: bool = False
|
|
18
77
|
agent: Optional[str] = None
|
|
@@ -60,37 +119,6 @@ class HubCleanupWorktreeRequest(Payload):
|
|
|
60
119
|
delete_remote: bool = False
|
|
61
120
|
|
|
62
121
|
|
|
63
|
-
class DocContentRequest(Payload):
|
|
64
|
-
content: str = ""
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
class SnapshotRequest(Payload):
|
|
68
|
-
pass
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
class DocChatPayload(Payload):
|
|
72
|
-
message: Optional[str] = None
|
|
73
|
-
stream: bool = False
|
|
74
|
-
targets: Optional[List[str]] = None
|
|
75
|
-
target: Optional[str] = None
|
|
76
|
-
agent: Optional[str] = None
|
|
77
|
-
model: Optional[str] = None
|
|
78
|
-
reasoning: Optional[str] = None
|
|
79
|
-
context_doc: Optional[str] = Field(
|
|
80
|
-
default=None,
|
|
81
|
-
validation_alias=AliasChoices("context_doc", "contextDoc", "viewing"),
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
class IngestSpecRequest(Payload):
|
|
86
|
-
force: bool = False
|
|
87
|
-
spec_path: Optional[str] = None
|
|
88
|
-
message: Optional[str] = None
|
|
89
|
-
agent: Optional[str] = None
|
|
90
|
-
model: Optional[str] = None
|
|
91
|
-
reasoning: Optional[str] = None
|
|
92
|
-
|
|
93
|
-
|
|
94
122
|
class AppServerThreadResetRequest(Payload):
|
|
95
123
|
key: str = Field(
|
|
96
124
|
validation_alias=AliasChoices("key", "feature", "feature_key", "featureKey")
|
|
@@ -125,39 +153,6 @@ class GithubPrSyncRequest(Payload):
|
|
|
125
153
|
mode: Optional[str] = None
|
|
126
154
|
|
|
127
155
|
|
|
128
|
-
class GithubPrFlowStartRequest(Payload):
|
|
129
|
-
mode: Optional[str] = "issue"
|
|
130
|
-
issue: Optional[str] = None
|
|
131
|
-
pr: Optional[str] = None
|
|
132
|
-
draft: Optional[bool] = None
|
|
133
|
-
base_branch: Optional[str] = Field(
|
|
134
|
-
default=None, validation_alias=AliasChoices("base_branch", "baseBranch", "base")
|
|
135
|
-
)
|
|
136
|
-
stop_condition: Optional[str] = Field(
|
|
137
|
-
default=None,
|
|
138
|
-
validation_alias=AliasChoices("stop_condition", "stopCondition", "until"),
|
|
139
|
-
)
|
|
140
|
-
max_cycles: Optional[int] = Field(
|
|
141
|
-
default=None, validation_alias=AliasChoices("max_cycles", "maxCycles")
|
|
142
|
-
)
|
|
143
|
-
max_implementation_runs: Optional[int] = Field(
|
|
144
|
-
default=None,
|
|
145
|
-
validation_alias=AliasChoices(
|
|
146
|
-
"max_implementation_runs", "maxImplementationRuns"
|
|
147
|
-
),
|
|
148
|
-
)
|
|
149
|
-
max_wallclock_seconds: Optional[int] = Field(
|
|
150
|
-
default=None,
|
|
151
|
-
validation_alias=AliasChoices(
|
|
152
|
-
"max_wallclock_seconds", "maxWallclockSeconds", "timeout_seconds"
|
|
153
|
-
),
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
class GithubPrFlowActionRequest(Payload):
|
|
158
|
-
pass
|
|
159
|
-
|
|
160
|
-
|
|
161
156
|
class SessionStopRequest(Payload):
|
|
162
157
|
session_id: Optional[str] = None
|
|
163
158
|
repo_path: Optional[str] = None
|
|
@@ -167,10 +162,6 @@ class SystemUpdateRequest(Payload):
|
|
|
167
162
|
target: Optional[str] = None
|
|
168
163
|
|
|
169
164
|
|
|
170
|
-
class ResponseModel(BaseModel):
|
|
171
|
-
model_config = ConfigDict(extra="ignore")
|
|
172
|
-
|
|
173
|
-
|
|
174
165
|
class HubJobResponse(ResponseModel):
|
|
175
166
|
job_id: str
|
|
176
167
|
kind: str
|
|
@@ -248,30 +239,9 @@ class SessionStopResponse(ResponseModel):
|
|
|
248
239
|
session_id: str
|
|
249
240
|
|
|
250
241
|
|
|
251
|
-
class DocsResponse(ResponseModel):
|
|
252
|
-
todo: str
|
|
253
|
-
progress: str
|
|
254
|
-
opinions: str
|
|
255
|
-
spec: str
|
|
256
|
-
summary: str
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
class IngestSpecResponse(ResponseModel):
|
|
260
|
-
status: str
|
|
261
|
-
todo: str
|
|
262
|
-
progress: str
|
|
263
|
-
opinions: str
|
|
264
|
-
spec: str
|
|
265
|
-
summary: str
|
|
266
|
-
patch: Optional[str] = None
|
|
267
|
-
agent_message: Optional[str] = None
|
|
268
|
-
|
|
269
|
-
|
|
270
242
|
class AppServerThreadsResponse(ResponseModel):
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
spec_ingest: Optional[str] = None
|
|
274
|
-
spec_ingest_opencode: Optional[str] = None
|
|
243
|
+
file_chat: Optional[str] = None
|
|
244
|
+
file_chat_opencode: Optional[str] = None
|
|
275
245
|
autorunner: Optional[str] = None
|
|
276
246
|
autorunner_opencode: Optional[str] = None
|
|
277
247
|
corruption: Optional[Dict[str, Any]] = None
|
|
@@ -294,23 +264,6 @@ class AppServerThreadResetAllResponse(ResponseModel):
|
|
|
294
264
|
cleared: bool
|
|
295
265
|
|
|
296
266
|
|
|
297
|
-
class DocWriteResponse(ResponseModel):
|
|
298
|
-
kind: str
|
|
299
|
-
content: str
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
class SnapshotResponse(ResponseModel):
|
|
303
|
-
exists: bool
|
|
304
|
-
content: str
|
|
305
|
-
state: Dict[str, Any]
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
class SnapshotCreateResponse(ResponseModel):
|
|
309
|
-
content: str
|
|
310
|
-
truncated: bool
|
|
311
|
-
state: Dict[str, Any]
|
|
312
|
-
|
|
313
|
-
|
|
314
267
|
class TokenTotalsResponse(ResponseModel):
|
|
315
268
|
input_tokens: int
|
|
316
269
|
cached_input_tokens: int
|
|
@@ -395,3 +348,30 @@ class ReviewStatusResponse(ResponseModel):
|
|
|
395
348
|
class ReviewControlResponse(ResponseModel):
|
|
396
349
|
status: str
|
|
397
350
|
detail: Optional[str] = None
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# Ticket CRUD schemas
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class TicketCreateRequest(Payload):
|
|
357
|
+
agent: str = "codex"
|
|
358
|
+
title: Optional[str] = None
|
|
359
|
+
goal: Optional[str] = None
|
|
360
|
+
body: str = ""
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class TicketUpdateRequest(Payload):
|
|
364
|
+
content: str # Full markdown with frontmatter
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class TicketResponse(ResponseModel):
|
|
368
|
+
path: str
|
|
369
|
+
index: int
|
|
370
|
+
frontmatter: Dict[str, Any]
|
|
371
|
+
body: str
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class TicketDeleteResponse(ResponseModel):
|
|
375
|
+
status: str
|
|
376
|
+
index: int
|
|
377
|
+
path: str
|
|
@@ -6,36 +6,14 @@ import os
|
|
|
6
6
|
import shutil
|
|
7
7
|
import time
|
|
8
8
|
from contextlib import ExitStack
|
|
9
|
-
from importlib import resources
|
|
10
9
|
from pathlib import Path
|
|
11
10
|
from typing import Iterable, Optional
|
|
12
11
|
from uuid import uuid4
|
|
13
12
|
|
|
14
13
|
from ..core.logging_utils import safe_log
|
|
14
|
+
from ..core.static_assets import missing_static_assets, resolve_static_dir
|
|
15
15
|
|
|
16
16
|
_ASSET_VERSION_TOKEN = "__CAR_ASSET_VERSION__"
|
|
17
|
-
_REQUIRED_STATIC_ASSETS = (
|
|
18
|
-
"index.html",
|
|
19
|
-
"styles.css",
|
|
20
|
-
"bootstrap.js",
|
|
21
|
-
"loader.js",
|
|
22
|
-
"app.js",
|
|
23
|
-
"github.js",
|
|
24
|
-
"vendor/xterm.js",
|
|
25
|
-
"vendor/xterm-addon-fit.js",
|
|
26
|
-
"vendor/xterm.css",
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def missing_static_assets(static_dir: Path) -> list[str]:
|
|
31
|
-
missing: list[str] = []
|
|
32
|
-
for rel_path in _REQUIRED_STATIC_ASSETS:
|
|
33
|
-
try:
|
|
34
|
-
if not (static_dir / rel_path).exists():
|
|
35
|
-
missing.append(rel_path)
|
|
36
|
-
except OSError:
|
|
37
|
-
missing.append(rel_path)
|
|
38
|
-
return missing
|
|
39
17
|
|
|
40
18
|
|
|
41
19
|
def _iter_static_source_files(source_dir: Path) -> Iterable[Path]:
|
|
@@ -177,27 +155,6 @@ def index_response_headers() -> dict[str, str]:
|
|
|
177
155
|
return headers
|
|
178
156
|
|
|
179
157
|
|
|
180
|
-
def resolve_static_dir() -> tuple[Path, Optional[ExitStack]]:
|
|
181
|
-
static_root = resources.files("codex_autorunner").joinpath("static")
|
|
182
|
-
if isinstance(static_root, Path):
|
|
183
|
-
if static_root.exists():
|
|
184
|
-
return static_root, None
|
|
185
|
-
fallback = Path(__file__).resolve().parent.parent / "static"
|
|
186
|
-
return fallback, None
|
|
187
|
-
stack = ExitStack()
|
|
188
|
-
try:
|
|
189
|
-
static_path = stack.enter_context(resources.as_file(static_root))
|
|
190
|
-
except Exception:
|
|
191
|
-
stack.close()
|
|
192
|
-
fallback = Path(__file__).resolve().parent.parent / "static"
|
|
193
|
-
return fallback, None
|
|
194
|
-
if static_path.exists():
|
|
195
|
-
return static_path, stack
|
|
196
|
-
stack.close()
|
|
197
|
-
fallback = Path(__file__).resolve().parent.parent / "static"
|
|
198
|
-
return fallback, None
|
|
199
|
-
|
|
200
|
-
|
|
201
158
|
def _cleanup_temp_dir(path: Path, logger: logging.Logger) -> None:
|
|
202
159
|
try:
|
|
203
160
|
shutil.rmtree(path)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Workspace docs helpers (active context, decisions, spec).
|
|
2
|
+
|
|
3
|
+
Workspace docs are optional and live under `.codex-autorunner/workspace/`.
|
|
4
|
+
They are distinct from tickets, which live under `.codex-autorunner/tickets/`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ..core.utils import canonicalize_path
|
|
11
|
+
from .paths import (
|
|
12
|
+
WORKSPACE_DOC_KINDS,
|
|
13
|
+
WorkspaceDocKind,
|
|
14
|
+
read_workspace_doc,
|
|
15
|
+
workspace_doc_path,
|
|
16
|
+
write_workspace_doc,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
WORKSPACE_ID_HEX_LEN = 12
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def canonical_workspace_root(path: Path) -> Path:
|
|
23
|
+
return canonicalize_path(path)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def workspace_id_for_path(path: Path) -> str:
|
|
27
|
+
canonical = canonical_workspace_root(path)
|
|
28
|
+
digest = hashlib.sha256(str(canonical).encode("utf-8")).hexdigest()
|
|
29
|
+
return digest[:WORKSPACE_ID_HEX_LEN]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"WORKSPACE_DOC_KINDS",
|
|
34
|
+
"WorkspaceDocKind",
|
|
35
|
+
"workspace_doc_path",
|
|
36
|
+
"read_workspace_doc",
|
|
37
|
+
"write_workspace_doc",
|
|
38
|
+
"canonical_workspace_root",
|
|
39
|
+
"workspace_id_for_path",
|
|
40
|
+
]
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
|
+
from typing import Literal, cast
|
|
8
|
+
|
|
9
|
+
from ..core import drafts as draft_utils
|
|
10
|
+
|
|
11
|
+
WorkspaceDocKind = Literal["active_context", "decisions", "spec"]
|
|
12
|
+
WORKSPACE_DOC_KINDS: tuple[WorkspaceDocKind, ...] = (
|
|
13
|
+
"active_context",
|
|
14
|
+
"decisions",
|
|
15
|
+
"spec",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class WorkspaceFile:
|
|
21
|
+
name: str
|
|
22
|
+
path: str # path relative to the workspace directory (POSIX)
|
|
23
|
+
is_pinned: bool = False
|
|
24
|
+
modified_at: str | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _normalize_kind(kind: str) -> WorkspaceDocKind:
|
|
28
|
+
key = (kind or "").strip().lower()
|
|
29
|
+
if key not in WORKSPACE_DOC_KINDS:
|
|
30
|
+
raise ValueError("invalid workspace doc kind")
|
|
31
|
+
return cast(WorkspaceDocKind, key)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def workspace_dir(repo_root: Path) -> Path:
|
|
35
|
+
return repo_root / ".codex-autorunner" / "workspace"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
PINNED_DOC_FILENAMES = {f"{kind}.md" for kind in WORKSPACE_DOC_KINDS}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class WorkspaceNode:
|
|
43
|
+
name: str
|
|
44
|
+
path: str # relative to workspace dir
|
|
45
|
+
type: Literal["file", "folder"]
|
|
46
|
+
is_pinned: bool = False
|
|
47
|
+
modified_at: str | None = None
|
|
48
|
+
size: int | None = None # files only
|
|
49
|
+
children: list["WorkspaceNode"] | None = None # folders only
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def normalize_workspace_rel_path(repo_root: Path, rel_path: str) -> tuple[Path, str]:
|
|
53
|
+
"""Normalize a user-supplied workspace path and ensure it stays in-tree.
|
|
54
|
+
|
|
55
|
+
We accept POSIX-style relative paths only, then resolve the full path and
|
|
56
|
+
verify the result is still under the workspace directory. This guards
|
|
57
|
+
against ".." traversal and symlink escapes that CodeQL flagged.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
base = workspace_dir(repo_root)
|
|
61
|
+
base_real = os.path.realpath(base)
|
|
62
|
+
cleaned = (rel_path or "").strip()
|
|
63
|
+
if not cleaned:
|
|
64
|
+
raise ValueError("invalid workspace file path")
|
|
65
|
+
|
|
66
|
+
relative = PurePosixPath(cleaned)
|
|
67
|
+
if relative.is_absolute() or ".." in relative.parts:
|
|
68
|
+
raise ValueError("invalid workspace file path")
|
|
69
|
+
|
|
70
|
+
# Normalize the relative path to collapse any sneaky segments
|
|
71
|
+
norm_relative = os.path.normpath(relative.as_posix())
|
|
72
|
+
if norm_relative in {".", ""}:
|
|
73
|
+
normalized = ""
|
|
74
|
+
else:
|
|
75
|
+
normalized = norm_relative
|
|
76
|
+
|
|
77
|
+
# Reject traversal or absolute inputs after normalization
|
|
78
|
+
if (
|
|
79
|
+
normalized.startswith("..")
|
|
80
|
+
or normalized.startswith("/")
|
|
81
|
+
or normalized.startswith("\\")
|
|
82
|
+
):
|
|
83
|
+
raise ValueError("invalid workspace file path")
|
|
84
|
+
|
|
85
|
+
candidate_str = os.path.realpath(os.path.join(base_real, normalized))
|
|
86
|
+
# Ensure the resolved path stays under the workspace directory
|
|
87
|
+
if not (candidate_str == base_real or candidate_str.startswith(base_real + os.sep)):
|
|
88
|
+
raise ValueError("invalid workspace file path")
|
|
89
|
+
|
|
90
|
+
candidate = Path(candidate_str)
|
|
91
|
+
rel_posix = candidate.relative_to(base_real).as_posix()
|
|
92
|
+
return candidate, rel_posix
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def sanitize_workspace_filename(filename: str) -> str:
|
|
96
|
+
"""Return a safe filename for workspace uploads.
|
|
97
|
+
|
|
98
|
+
We strip any directory components, collapse whitespace, and guard against
|
|
99
|
+
empty names. Caller is responsible for applying any per-workspace policy
|
|
100
|
+
(e.g., overwrite vs. reject).
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
cleaned = (filename or "").strip()
|
|
104
|
+
# Drop any path fragments that may be embedded in the upload
|
|
105
|
+
base = PurePosixPath(cleaned).name
|
|
106
|
+
# Remove remaining separators/backslashes that PurePosixPath.name could keep
|
|
107
|
+
base = base.replace("/", "").replace("\\", "")
|
|
108
|
+
if base in {".", ".."}:
|
|
109
|
+
base = ""
|
|
110
|
+
# Collapse whitespace to single spaces to keep names readable
|
|
111
|
+
base = " ".join(base.split())
|
|
112
|
+
if not base:
|
|
113
|
+
return "upload"
|
|
114
|
+
return base
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def workspace_doc_path(repo_root: Path, kind: str) -> Path:
|
|
118
|
+
key = _normalize_kind(kind)
|
|
119
|
+
return workspace_dir(repo_root) / f"{key}.md"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def read_workspace_file(
|
|
123
|
+
repo_root: Path, rel_path: str
|
|
124
|
+
) -> str: # codeql[py/path-injection]
|
|
125
|
+
path, _ = normalize_workspace_rel_path(repo_root, rel_path)
|
|
126
|
+
if (
|
|
127
|
+
path.is_dir()
|
|
128
|
+
): # codeql[py/path-injection] validated by normalize_workspace_rel_path
|
|
129
|
+
raise ValueError("path points to a directory")
|
|
130
|
+
if (
|
|
131
|
+
not path.exists()
|
|
132
|
+
): # codeql[py/path-injection] validated by normalize_workspace_rel_path
|
|
133
|
+
return ""
|
|
134
|
+
return path.read_text(
|
|
135
|
+
encoding="utf-8"
|
|
136
|
+
) # codeql[py/path-injection] validated by normalize_workspace_rel_path
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def write_workspace_file( # codeql[py/path-injection]
|
|
140
|
+
repo_root: Path, rel_path: str, content: str
|
|
141
|
+
) -> str:
|
|
142
|
+
path, rel_posix = normalize_workspace_rel_path(repo_root, rel_path)
|
|
143
|
+
if (
|
|
144
|
+
path.exists() and path.is_dir()
|
|
145
|
+
): # codeql[py/path-injection] validated by normalize_workspace_rel_path
|
|
146
|
+
raise ValueError("path points to a directory")
|
|
147
|
+
path.parent.mkdir(
|
|
148
|
+
parents=True, exist_ok=True
|
|
149
|
+
) # codeql[py/path-injection] validated by normalize_workspace_rel_path
|
|
150
|
+
path.write_text(
|
|
151
|
+
content or "", encoding="utf-8"
|
|
152
|
+
) # codeql[py/path-injection] validated by normalize_workspace_rel_path
|
|
153
|
+
try:
|
|
154
|
+
rel = path.relative_to(repo_root).as_posix()
|
|
155
|
+
draft_utils.invalidate_drafts_for_path(repo_root, rel)
|
|
156
|
+
state_key = f"workspace_{rel_posix.replace('/', '_')}"
|
|
157
|
+
draft_utils.remove_draft(repo_root, state_key)
|
|
158
|
+
except Exception:
|
|
159
|
+
# best effort; do not block writes
|
|
160
|
+
pass
|
|
161
|
+
return path.read_text(encoding="utf-8")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def read_workspace_doc(repo_root: Path, kind: str) -> str:
|
|
165
|
+
path = workspace_doc_path(repo_root, kind)
|
|
166
|
+
if not path.exists():
|
|
167
|
+
return ""
|
|
168
|
+
return path.read_text(encoding="utf-8")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def write_workspace_doc( # codeql[py/path-injection]
|
|
172
|
+
repo_root: Path, kind: str, content: str
|
|
173
|
+
) -> str:
|
|
174
|
+
path = workspace_doc_path(repo_root, kind)
|
|
175
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
176
|
+
path.write_text(
|
|
177
|
+
content or "", encoding="utf-8"
|
|
178
|
+
) # codeql[py/path-injection] workspace_doc_path is deterministic
|
|
179
|
+
try:
|
|
180
|
+
rel = path.relative_to(repo_root).as_posix()
|
|
181
|
+
draft_utils.invalidate_drafts_for_path(repo_root, rel)
|
|
182
|
+
state_key = f"workspace_{rel.replace('/', '_')}"
|
|
183
|
+
draft_utils.remove_draft(repo_root, state_key)
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
return path.read_text(encoding="utf-8")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _format_mtime(path: Path) -> str | None:
|
|
190
|
+
if not path.exists():
|
|
191
|
+
return None
|
|
192
|
+
ts = datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc)
|
|
193
|
+
return ts.isoformat()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def list_workspace_files(
|
|
197
|
+
repo_root: Path,
|
|
198
|
+
) -> list[WorkspaceFile]: # codeql[py/path-injection]
|
|
199
|
+
base = workspace_dir(repo_root)
|
|
200
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
201
|
+
|
|
202
|
+
pinned: list[WorkspaceFile] = []
|
|
203
|
+
for kind in WORKSPACE_DOC_KINDS:
|
|
204
|
+
path = workspace_doc_path(repo_root, kind)
|
|
205
|
+
rel = path.relative_to(base).as_posix()
|
|
206
|
+
pinned.append(
|
|
207
|
+
WorkspaceFile(
|
|
208
|
+
name=path.name,
|
|
209
|
+
path=rel,
|
|
210
|
+
is_pinned=True,
|
|
211
|
+
modified_at=_format_mtime(path),
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
others: list[WorkspaceFile] = []
|
|
216
|
+
if base.exists():
|
|
217
|
+
for file_path in base.rglob("*"):
|
|
218
|
+
if file_path.is_dir():
|
|
219
|
+
continue
|
|
220
|
+
try:
|
|
221
|
+
rel = file_path.relative_to(base).as_posix()
|
|
222
|
+
except ValueError:
|
|
223
|
+
continue
|
|
224
|
+
if any(rel == pinned_file.path for pinned_file in pinned):
|
|
225
|
+
continue
|
|
226
|
+
others.append(
|
|
227
|
+
WorkspaceFile(
|
|
228
|
+
name=file_path.name,
|
|
229
|
+
path=rel,
|
|
230
|
+
is_pinned=False,
|
|
231
|
+
modified_at=_format_mtime(file_path),
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
others.sort(key=lambda f: f.path)
|
|
236
|
+
return [*pinned, *others]
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _sort_workspace_children(path: Path) -> tuple[int, str]:
|
|
240
|
+
# Folders first, then files, both alphabetized (case-insensitive)
|
|
241
|
+
return (0 if path.is_dir() else 1, path.name.lower())
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _is_within_workspace(base_real: Path, candidate: Path) -> bool:
|
|
245
|
+
try:
|
|
246
|
+
candidate.resolve().relative_to(base_real)
|
|
247
|
+
return True
|
|
248
|
+
except Exception:
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _file_node(base: Path, path: Path, is_pinned: bool = False) -> WorkspaceNode:
|
|
253
|
+
rel = path.relative_to(base).as_posix()
|
|
254
|
+
size: int | None = None
|
|
255
|
+
if path.exists() and path.is_file():
|
|
256
|
+
try:
|
|
257
|
+
size = path.stat().st_size
|
|
258
|
+
except OSError:
|
|
259
|
+
size = None
|
|
260
|
+
return WorkspaceNode(
|
|
261
|
+
name=path.name,
|
|
262
|
+
path=rel,
|
|
263
|
+
type="file",
|
|
264
|
+
is_pinned=is_pinned,
|
|
265
|
+
modified_at=_format_mtime(path),
|
|
266
|
+
size=size,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _build_workspace_tree(base: Path, path: Path) -> WorkspaceNode:
|
|
271
|
+
is_symlink = path.is_symlink()
|
|
272
|
+
is_folder = path.is_dir() and not is_symlink
|
|
273
|
+
is_pinned = path.name in PINNED_DOC_FILENAMES and path.parent == base
|
|
274
|
+
|
|
275
|
+
if not is_folder:
|
|
276
|
+
return _file_node(base, path, is_pinned=is_pinned)
|
|
277
|
+
|
|
278
|
+
children: list[WorkspaceNode] = []
|
|
279
|
+
for child in sorted(path.iterdir(), key=_sort_workspace_children):
|
|
280
|
+
# Avoid duplicating pinned docs surfaced at the root list
|
|
281
|
+
if child.parent == base and child.name in PINNED_DOC_FILENAMES:
|
|
282
|
+
continue
|
|
283
|
+
# Skip symlink escapes that resolve outside the workspace
|
|
284
|
+
if child.is_symlink() and not _is_within_workspace(base.resolve(), child):
|
|
285
|
+
continue
|
|
286
|
+
children.append(_build_workspace_tree(base, child))
|
|
287
|
+
|
|
288
|
+
return WorkspaceNode(
|
|
289
|
+
name=path.name,
|
|
290
|
+
path=path.relative_to(base).as_posix(),
|
|
291
|
+
type="folder",
|
|
292
|
+
is_pinned=False,
|
|
293
|
+
modified_at=_format_mtime(path),
|
|
294
|
+
children=children,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def list_workspace_tree(repo_root: Path) -> list[WorkspaceNode]:
|
|
299
|
+
"""Return hierarchical workspace structure (folders + files)."""
|
|
300
|
+
|
|
301
|
+
base = workspace_dir(repo_root)
|
|
302
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
303
|
+
base_real = base.resolve()
|
|
304
|
+
|
|
305
|
+
nodes: list[WorkspaceNode] = []
|
|
306
|
+
|
|
307
|
+
# Pinned docs first (even if missing)
|
|
308
|
+
for name in sorted(PINNED_DOC_FILENAMES):
|
|
309
|
+
pinned_path = base / name
|
|
310
|
+
nodes.append(_file_node(base, pinned_path, is_pinned=True))
|
|
311
|
+
|
|
312
|
+
for child in sorted(base.iterdir(), key=_sort_workspace_children):
|
|
313
|
+
if child.parent == base and child.name in PINNED_DOC_FILENAMES:
|
|
314
|
+
continue
|
|
315
|
+
if child.is_symlink() and not _is_within_workspace(base_real, child):
|
|
316
|
+
continue
|
|
317
|
+
nodes.append(_build_workspace_tree(base, child))
|
|
318
|
+
|
|
319
|
+
return nodes
|