aicortexpy 0.1.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.
- aicortexpy/__init__.py +3 -0
- aicortexpy/__main__.py +5 -0
- aicortexpy/acp_client.py +559 -0
- aicortexpy/acp_conn.py +853 -0
- aicortexpy/acp_errors.py +107 -0
- aicortexpy/agent_env.py +88 -0
- aicortexpy/artifact_ws.py +233 -0
- aicortexpy/artifacts.py +67 -0
- aicortexpy/attachments.py +85 -0
- aicortexpy/auto_commit.py +106 -0
- aicortexpy/capabilities.py +92 -0
- aicortexpy/chat_acp_pool.py +321 -0
- aicortexpy/cli.py +133 -0
- aicortexpy/client.py +438 -0
- aicortexpy/codex_config.py +98 -0
- aicortexpy/config.py +298 -0
- aicortexpy/daemon.py +677 -0
- aicortexpy/data/default_skills/agent-memory/SKILL.md +67 -0
- aicortexpy/data/default_skills/aicorkit-atlassian/SKILL.md +144 -0
- aicortexpy/data/default_skills/aicorkit-graph/SKILL.md +145 -0
- aicortexpy/data/default_skills/aicorkit-outlook/SKILL.md +87 -0
- aicortexpy/data/default_skills/aicorkit-wxmsg/SKILL.md +96 -0
- aicortexpy/data/default_skills/aicortex-basics/SKILL.md +10 -0
- aicortexpy/data/default_skills/aicortex-bridge-cdp/SKILL.md +118 -0
- aicortexpy/data/default_skills/aicortex-cli-ops/SKILL.md +100 -0
- aicortexpy/data/default_skills/aicortex-graph/SKILL.md +30 -0
- aicortexpy/data/default_skills/graphify/SKILL.md +97 -0
- aicortexpy/data/default_skills/question-form/SKILL.md +162 -0
- aicortexpy/data/default_skills/test-report-generator/SKILL.md +179 -0
- aicortexpy/data/default_skills/test-report-generator/scripts/generate_cases.py +146 -0
- aicortexpy/data/default_skills/test-report-generator/scripts/generate_report.py +275 -0
- aicortexpy/design_seed.py +59 -0
- aicortexpy/diskusage.py +209 -0
- aicortexpy/execenv.py +264 -0
- aicortexpy/gc.py +286 -0
- aicortexpy/handoff_bundle.py +65 -0
- aicortexpy/health.py +178 -0
- aicortexpy/local_skills.py +157 -0
- aicortexpy/models.py +385 -0
- aicortexpy/openclaw_config.py +153 -0
- aicortexpy/pinned_workdir.py +60 -0
- aicortexpy/platform_info.py +61 -0
- aicortexpy/poisoned.py +44 -0
- aicortexpy/process.py +168 -0
- aicortexpy/procutil.py +61 -0
- aicortexpy/prompt.py +235 -0
- aicortexpy/repo.py +462 -0
- aicortexpy/runner.py +969 -0
- aicortexpy/runtime_config.py +540 -0
- aicortexpy/sandbox.py +264 -0
- aicortexpy/stream.py +422 -0
- aicortexpy/terminal.py +147 -0
- aicortexpy/tunnel.py +82 -0
- aicortexpy/wakeup.py +222 -0
- aicortexpy-0.1.0.dist-info/METADATA +106 -0
- aicortexpy-0.1.0.dist-info/RECORD +58 -0
- aicortexpy-0.1.0.dist-info/WHEEL +5 -0
- aicortexpy-0.1.0.dist-info/top_level.txt +1 -0
aicortexpy/client.py
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
"""HTTP client for AICortex daemon API endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import ssl
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.request
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .platform_info import host_os
|
|
13
|
+
|
|
14
|
+
MAX_ATTACHMENT_BYTES = 100 << 20
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class APIError(Exception):
|
|
18
|
+
def __init__(self, method: str, path: str, status: int, body: str):
|
|
19
|
+
self.method = method
|
|
20
|
+
self.path = path
|
|
21
|
+
self.status = status
|
|
22
|
+
self.body = body
|
|
23
|
+
super().__init__(f"{method} {path} returned {status}: {body[:500]}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def is_not_found(err: Exception, needle: str) -> bool:
|
|
27
|
+
return (
|
|
28
|
+
isinstance(err, APIError)
|
|
29
|
+
and err.status == 404
|
|
30
|
+
and needle.lower() in err.body.lower()
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Task:
|
|
36
|
+
raw: dict[str, Any]
|
|
37
|
+
|
|
38
|
+
def _s(self, key: str) -> str:
|
|
39
|
+
return str(self.raw.get(key) or "")
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def id(self) -> str:
|
|
43
|
+
return self._s("id")
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def runtime_id(self) -> str:
|
|
47
|
+
return self._s("runtime_id")
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def issue_id(self) -> str:
|
|
51
|
+
return self._s("issue_id")
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def workspace_id(self) -> str:
|
|
55
|
+
return self._s("workspace_id")
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def chat_session_id(self) -> str:
|
|
59
|
+
return self._s("chat_session_id")
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def chat_message(self) -> str:
|
|
63
|
+
return self._s("chat_message")
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def autopilot_run_id(self) -> str:
|
|
67
|
+
return self._s("autopilot_run_id")
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def autopilot_id(self) -> str:
|
|
71
|
+
return self._s("autopilot_id")
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def autopilot_description(self) -> str:
|
|
75
|
+
return self._s("autopilot_description")
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def autopilot_title(self) -> str:
|
|
79
|
+
return self._s("autopilot_title")
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def autopilot_source(self) -> str:
|
|
83
|
+
return self._s("autopilot_source")
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def autopilot_trigger_payload(self) -> str:
|
|
87
|
+
p = self.raw.get("autopilot_trigger_payload")
|
|
88
|
+
if p is None:
|
|
89
|
+
return ""
|
|
90
|
+
if isinstance(p, (dict, list)):
|
|
91
|
+
return json.dumps(p, ensure_ascii=False)
|
|
92
|
+
return str(p)
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def quick_create_prompt(self) -> str:
|
|
96
|
+
return self._s("quick_create_prompt")
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def trigger_comment_id(self) -> str:
|
|
100
|
+
return self._s("trigger_comment_id")
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def trigger_comment_content(self) -> str:
|
|
104
|
+
return self._s("trigger_comment_content")
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def trigger_author_type(self) -> str:
|
|
108
|
+
return self._s("trigger_author_type")
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def trigger_author_name(self) -> str:
|
|
112
|
+
return self._s("trigger_author_name")
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def project_id(self) -> str:
|
|
116
|
+
return self._s("project_id")
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def project_title(self) -> str:
|
|
120
|
+
return self._s("project_title")
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def project_prompt(self) -> str:
|
|
124
|
+
return self._s("project_prompt")
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def project_pinned_workdir(self) -> bool:
|
|
128
|
+
return bool(self.raw.get("project_pinned_workdir"))
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def project_resources(self) -> list[dict[str, Any]]:
|
|
132
|
+
r = self.raw.get("project_resources")
|
|
133
|
+
return [x for x in r if isinstance(x, dict)] if isinstance(r, list) else []
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def prior_session_id(self) -> str:
|
|
137
|
+
return self._s("prior_session_id")
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def prior_work_dir(self) -> str:
|
|
141
|
+
return self._s("prior_work_dir")
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def design_mode(self) -> str:
|
|
145
|
+
return self._s("design_mode")
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def design_system_content(self) -> str:
|
|
149
|
+
return self._s("design_system_content")
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def design_system_name(self) -> str:
|
|
153
|
+
return self._s("design_system_name")
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def design_example_hint(self) -> str:
|
|
157
|
+
return self._s("design_example_hint")
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def design_example_id(self) -> str:
|
|
161
|
+
return self._s("design_example_id")
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def design_example_bundles(self) -> list[dict[str, Any]]:
|
|
165
|
+
b = self.raw.get("design_example_bundles")
|
|
166
|
+
return [x for x in b if isinstance(x, dict)] if isinstance(b, list) else []
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def handoff_bundle_url(self) -> str:
|
|
170
|
+
return self._s("handoff_bundle_url")
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def artifact_entry(self) -> str:
|
|
174
|
+
return self._s("artifact_entry")
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def session_kind(self) -> str:
|
|
178
|
+
return self._s("session_kind")
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def squad_id(self) -> str:
|
|
182
|
+
return self._s("squad_id")
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def squad_name(self) -> str:
|
|
186
|
+
return self._s("squad_name")
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def agent(self) -> dict[str, Any] | None:
|
|
190
|
+
a = self.raw.get("agent")
|
|
191
|
+
return a if isinstance(a, dict) else None
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def repos(self) -> list[dict[str, Any]]:
|
|
195
|
+
repos = self.raw.get("repos")
|
|
196
|
+
if not isinstance(repos, list):
|
|
197
|
+
return []
|
|
198
|
+
return [r for r in repos if isinstance(r, dict)]
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def chat_history(self) -> list[dict[str, Any]]:
|
|
202
|
+
h = self.raw.get("chat_history")
|
|
203
|
+
return [x for x in h if isinstance(x, dict)] if isinstance(h, list) else []
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def chat_message_attachments(self) -> list[dict[str, Any]]:
|
|
207
|
+
a = self.raw.get("chat_message_attachments")
|
|
208
|
+
if not isinstance(a, list):
|
|
209
|
+
a = self.raw.get("attachments")
|
|
210
|
+
return [x for x in a if isinstance(x, dict)] if isinstance(a, list) else []
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def attachments(self) -> list[dict[str, Any]]:
|
|
214
|
+
return self.chat_message_attachments
|
|
215
|
+
|
|
216
|
+
# Daemon-local: filled after materialize
|
|
217
|
+
design_incoming_rel_paths: list[str] | None = None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class DaemonClient:
|
|
221
|
+
def __init__(
|
|
222
|
+
self,
|
|
223
|
+
base_url: str,
|
|
224
|
+
token: str,
|
|
225
|
+
*,
|
|
226
|
+
version: str = "",
|
|
227
|
+
timeout: float = 30.0,
|
|
228
|
+
):
|
|
229
|
+
self.base_url = base_url.rstrip("/")
|
|
230
|
+
self.token = token
|
|
231
|
+
self.version = version
|
|
232
|
+
self.timeout = timeout
|
|
233
|
+
self._ctx = ssl.create_default_context()
|
|
234
|
+
|
|
235
|
+
def _headers(self, *, json_body: bool = False) -> dict[str, str]:
|
|
236
|
+
headers = {
|
|
237
|
+
"Accept": "application/json",
|
|
238
|
+
"Authorization": f"Bearer {self.token}",
|
|
239
|
+
"X-Client-Platform": "daemon",
|
|
240
|
+
"X-Client-OS": host_os(),
|
|
241
|
+
}
|
|
242
|
+
if self.version:
|
|
243
|
+
headers["X-Client-Version"] = self.version
|
|
244
|
+
if json_body:
|
|
245
|
+
headers["Content-Type"] = "application/json"
|
|
246
|
+
return headers
|
|
247
|
+
|
|
248
|
+
def _request(
|
|
249
|
+
self,
|
|
250
|
+
method: str,
|
|
251
|
+
path: str,
|
|
252
|
+
body: Any | None = None,
|
|
253
|
+
) -> Any:
|
|
254
|
+
url = self.base_url + path
|
|
255
|
+
data = None
|
|
256
|
+
headers = self._headers(json_body=body is not None)
|
|
257
|
+
if body is not None:
|
|
258
|
+
data = json.dumps(body).encode("utf-8")
|
|
259
|
+
|
|
260
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
261
|
+
try:
|
|
262
|
+
with urllib.request.urlopen(req, timeout=self.timeout, context=self._ctx) as resp:
|
|
263
|
+
raw = resp.read()
|
|
264
|
+
if not raw:
|
|
265
|
+
return None
|
|
266
|
+
return json.loads(raw.decode("utf-8"))
|
|
267
|
+
except urllib.error.HTTPError as e:
|
|
268
|
+
err_body = e.read().decode("utf-8", errors="replace")
|
|
269
|
+
raise APIError(method, path, e.code, err_body) from e
|
|
270
|
+
|
|
271
|
+
def get(self, path: str) -> Any:
|
|
272
|
+
return self._request("GET", path)
|
|
273
|
+
|
|
274
|
+
def post(self, path: str, body: Any | None = None) -> Any:
|
|
275
|
+
return self._request("POST", path, body if body is not None else {})
|
|
276
|
+
|
|
277
|
+
def list_workspaces(self) -> list[dict[str, Any]]:
|
|
278
|
+
data = self.get("/api/workspaces")
|
|
279
|
+
return data if isinstance(data, list) else []
|
|
280
|
+
|
|
281
|
+
def register(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
282
|
+
data = self.post("/api/daemon/register", payload)
|
|
283
|
+
return data if isinstance(data, dict) else {}
|
|
284
|
+
|
|
285
|
+
def deregister(self, runtime_ids: list[str]) -> None:
|
|
286
|
+
self.post("/api/daemon/deregister", {"runtime_ids": runtime_ids})
|
|
287
|
+
|
|
288
|
+
def recover_orphans(self, runtime_id: str) -> None:
|
|
289
|
+
self.post(f"/api/daemon/runtimes/{runtime_id}/recover-orphans", {})
|
|
290
|
+
|
|
291
|
+
def heartbeat(self, runtime_id: str) -> dict[str, Any]:
|
|
292
|
+
data = self.post("/api/daemon/heartbeat", {"runtime_id": runtime_id})
|
|
293
|
+
return data if isinstance(data, dict) else {}
|
|
294
|
+
|
|
295
|
+
def claim_task(self, runtime_id: str) -> Task | None:
|
|
296
|
+
data = self.post(f"/api/daemon/runtimes/{runtime_id}/tasks/claim", {})
|
|
297
|
+
if not isinstance(data, dict):
|
|
298
|
+
return None
|
|
299
|
+
task = data.get("task")
|
|
300
|
+
if not task:
|
|
301
|
+
return None
|
|
302
|
+
return Task(task)
|
|
303
|
+
|
|
304
|
+
def start_task(self, task_id: str) -> None:
|
|
305
|
+
self.post(f"/api/daemon/tasks/{task_id}/start", {})
|
|
306
|
+
|
|
307
|
+
def report_progress(self, task_id: str, summary: str, step: int, total: int) -> None:
|
|
308
|
+
self.post(
|
|
309
|
+
f"/api/daemon/tasks/{task_id}/progress",
|
|
310
|
+
{"summary": summary, "step": step, "total": total},
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
def complete_task(
|
|
314
|
+
self,
|
|
315
|
+
task_id: str,
|
|
316
|
+
output: str,
|
|
317
|
+
*,
|
|
318
|
+
work_dir: str = "",
|
|
319
|
+
session_id: str = "",
|
|
320
|
+
branch_name: str = "",
|
|
321
|
+
) -> None:
|
|
322
|
+
body: dict[str, Any] = {"output": output}
|
|
323
|
+
if work_dir:
|
|
324
|
+
body["work_dir"] = work_dir
|
|
325
|
+
if session_id:
|
|
326
|
+
body["session_id"] = session_id
|
|
327
|
+
if branch_name:
|
|
328
|
+
body["branch_name"] = branch_name
|
|
329
|
+
self.post(f"/api/daemon/tasks/{task_id}/complete", body)
|
|
330
|
+
|
|
331
|
+
def fail_task(
|
|
332
|
+
self,
|
|
333
|
+
task_id: str,
|
|
334
|
+
error: str,
|
|
335
|
+
*,
|
|
336
|
+
work_dir: str = "",
|
|
337
|
+
session_id: str = "",
|
|
338
|
+
failure_reason: str = "agent_error",
|
|
339
|
+
) -> None:
|
|
340
|
+
body: dict[str, Any] = {"error": error, "failure_reason": failure_reason}
|
|
341
|
+
if work_dir:
|
|
342
|
+
body["work_dir"] = work_dir
|
|
343
|
+
if session_id:
|
|
344
|
+
body["session_id"] = session_id
|
|
345
|
+
self.post(f"/api/daemon/tasks/{task_id}/fail", body)
|
|
346
|
+
|
|
347
|
+
def pin_task_session(
|
|
348
|
+
self,
|
|
349
|
+
task_id: str,
|
|
350
|
+
*,
|
|
351
|
+
session_id: str = "",
|
|
352
|
+
work_dir: str = "",
|
|
353
|
+
) -> None:
|
|
354
|
+
if not session_id and not work_dir:
|
|
355
|
+
return
|
|
356
|
+
body: dict[str, Any] = {}
|
|
357
|
+
if session_id:
|
|
358
|
+
body["session_id"] = session_id
|
|
359
|
+
if work_dir:
|
|
360
|
+
body["work_dir"] = work_dir
|
|
361
|
+
self.post(f"/api/daemon/tasks/{task_id}/session", body)
|
|
362
|
+
|
|
363
|
+
def get_task_status(self, task_id: str) -> str:
|
|
364
|
+
data = self.get(f"/api/daemon/tasks/{task_id}/status")
|
|
365
|
+
if isinstance(data, dict):
|
|
366
|
+
return str(data.get("status") or "")
|
|
367
|
+
return ""
|
|
368
|
+
|
|
369
|
+
def report_task_messages(self, task_id: str, messages: list[dict[str, Any]]) -> None:
|
|
370
|
+
if not messages:
|
|
371
|
+
return
|
|
372
|
+
self.post(f"/api/daemon/tasks/{task_id}/messages", {"messages": messages})
|
|
373
|
+
|
|
374
|
+
def report_task_usage(self, task_id: str, usage: list[dict[str, Any]]) -> None:
|
|
375
|
+
if not usage:
|
|
376
|
+
return
|
|
377
|
+
self.post(f"/api/daemon/tasks/{task_id}/usage", {"usage": usage})
|
|
378
|
+
|
|
379
|
+
def report_task_artifacts(self, task_id: str, artifacts: list[dict[str, str]]) -> None:
|
|
380
|
+
if not artifacts:
|
|
381
|
+
return
|
|
382
|
+
self.post(f"/api/daemon/tasks/{task_id}/artifacts", {"artifacts": artifacts})
|
|
383
|
+
|
|
384
|
+
def report_model_list_result(
|
|
385
|
+
self, runtime_id: str, request_id: str, result: dict[str, Any]
|
|
386
|
+
) -> None:
|
|
387
|
+
self.post(f"/api/daemon/runtimes/{runtime_id}/models/{request_id}/result", result)
|
|
388
|
+
|
|
389
|
+
def report_local_skill_list_result(
|
|
390
|
+
self, runtime_id: str, request_id: str, result: dict[str, Any]
|
|
391
|
+
) -> None:
|
|
392
|
+
self.post(
|
|
393
|
+
f"/api/daemon/runtimes/{runtime_id}/local-skills/{request_id}/result",
|
|
394
|
+
result,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
def report_local_skill_import_result(
|
|
398
|
+
self, runtime_id: str, request_id: str, result: dict[str, Any]
|
|
399
|
+
) -> None:
|
|
400
|
+
self.post(
|
|
401
|
+
f"/api/daemon/runtimes/{runtime_id}/local-skills/import/{request_id}/result",
|
|
402
|
+
result,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
def get_issue_gc_check(self, issue_id: str) -> dict[str, Any]:
|
|
406
|
+
data = self.get(f"/api/daemon/issues/{issue_id}/gc-check")
|
|
407
|
+
return data if isinstance(data, dict) else {}
|
|
408
|
+
|
|
409
|
+
def get_chat_session_gc_check(self, session_id: str) -> dict[str, Any]:
|
|
410
|
+
data = self.get(f"/api/daemon/chat-sessions/{session_id}/gc-check")
|
|
411
|
+
return data if isinstance(data, dict) else {}
|
|
412
|
+
|
|
413
|
+
def get_autopilot_run_gc_check(self, run_id: str) -> dict[str, Any]:
|
|
414
|
+
data = self.get(f"/api/daemon/autopilot-runs/{run_id}/gc-check")
|
|
415
|
+
return data if isinstance(data, dict) else {}
|
|
416
|
+
|
|
417
|
+
def get_task_gc_check(self, task_id: str) -> dict[str, Any]:
|
|
418
|
+
data = self.get(f"/api/daemon/tasks/{task_id}/gc-check")
|
|
419
|
+
return data if isinstance(data, dict) else {}
|
|
420
|
+
|
|
421
|
+
def get_workspace_repos(self, workspace_id: str) -> dict[str, Any]:
|
|
422
|
+
data = self.get(f"/api/daemon/workspaces/{workspace_id}/repos")
|
|
423
|
+
return data if isinstance(data, dict) else {}
|
|
424
|
+
|
|
425
|
+
def fetch_attachment_bytes(self, attachment_id: str) -> tuple[bytes, str]:
|
|
426
|
+
path = f"/api/daemon/attachments/{attachment_id}/bytes"
|
|
427
|
+
url = self.base_url + path
|
|
428
|
+
req = urllib.request.Request(url, headers=self._headers(), method="GET")
|
|
429
|
+
try:
|
|
430
|
+
with urllib.request.urlopen(req, timeout=self.timeout, context=self._ctx) as resp:
|
|
431
|
+
body = resp.read(MAX_ATTACHMENT_BYTES + 1)
|
|
432
|
+
if len(body) > MAX_ATTACHMENT_BYTES:
|
|
433
|
+
raise APIError("GET", path, 413, "attachment exceeds size limit")
|
|
434
|
+
filename = resp.headers.get("X-Attachment-Filename") or ""
|
|
435
|
+
return body, filename
|
|
436
|
+
except urllib.error.HTTPError as e:
|
|
437
|
+
err_body = e.read().decode("utf-8", errors="replace")
|
|
438
|
+
raise APIError("GET", path, e.code, err_body) from e
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Codex per-task CODEX_HOME config helpers (skill strip + multi-agent disable)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger("aicortexpy.codex_config")
|
|
11
|
+
|
|
12
|
+
MULTI_AGENT_BEGIN = "# BEGIN aicortex-managed multi-agent (do not edit; regenerated by daemon)"
|
|
13
|
+
MULTI_AGENT_END = "# END aicortex-managed multi-agent"
|
|
14
|
+
_MULTI_AGENT_BLOCK_RE = re.compile(
|
|
15
|
+
rf"(?ms)^{re.escape(MULTI_AGENT_BEGIN)}.*?^{re.escape(MULTI_AGENT_END)}\n*"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def strip_skills_config_entries(content: str) -> str:
|
|
20
|
+
"""Remove [[skills.config]] blocks that break Codex CLI (missing path)."""
|
|
21
|
+
if "[[skills.config]]" not in content:
|
|
22
|
+
return content
|
|
23
|
+
out: list[str] = []
|
|
24
|
+
in_skills = False
|
|
25
|
+
for line in content.splitlines(keepends=True):
|
|
26
|
+
trimmed = line.strip()
|
|
27
|
+
if trimmed.startswith("["):
|
|
28
|
+
if trimmed == "[[skills.config]]":
|
|
29
|
+
in_skills = True
|
|
30
|
+
continue
|
|
31
|
+
in_skills = False
|
|
32
|
+
out.append(line)
|
|
33
|
+
continue
|
|
34
|
+
if in_skills:
|
|
35
|
+
continue
|
|
36
|
+
out.append(line)
|
|
37
|
+
stripped = "".join(out).rstrip("\n") + "\n"
|
|
38
|
+
return "" if not stripped.strip() else stripped
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def sanitize_copied_codex_config(config_path: Path) -> None:
|
|
42
|
+
if not config_path.is_file():
|
|
43
|
+
return
|
|
44
|
+
try:
|
|
45
|
+
text = config_path.read_text(encoding="utf-8")
|
|
46
|
+
except OSError as e:
|
|
47
|
+
log.warning("read codex config failed: %s", e)
|
|
48
|
+
return
|
|
49
|
+
stripped = strip_skills_config_entries(text)
|
|
50
|
+
if stripped == text:
|
|
51
|
+
return
|
|
52
|
+
try:
|
|
53
|
+
config_path.write_text(stripped, encoding="utf-8")
|
|
54
|
+
log.info("stripped [[skills.config]] from %s", config_path)
|
|
55
|
+
except OSError as e:
|
|
56
|
+
log.warning("write sanitized codex config failed: %s", e)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def codex_multi_agent_enabled() -> bool:
|
|
60
|
+
raw = (os.environ.get("AICORTEX_CODEX_MULTI_AGENT") or "").strip().lower()
|
|
61
|
+
return raw in ("1", "true", "yes", "on")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def ensure_codex_multi_agent_config(config_path: Path) -> None:
|
|
65
|
+
"""Disable Codex native multi_agent unless AICORTEX_CODEX_MULTI_AGENT is set."""
|
|
66
|
+
if codex_multi_agent_enabled():
|
|
67
|
+
return
|
|
68
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
existing = ""
|
|
70
|
+
if config_path.is_file():
|
|
71
|
+
try:
|
|
72
|
+
existing = config_path.read_text(encoding="utf-8")
|
|
73
|
+
except OSError:
|
|
74
|
+
existing = ""
|
|
75
|
+
existing = _MULTI_AGENT_BLOCK_RE.sub("", existing)
|
|
76
|
+
in_features = bool(re.search(r"(?m)^\s*\[\s*features\s*\]\s*(?:#.*)?$", existing))
|
|
77
|
+
if in_features:
|
|
78
|
+
body = "multi_agent = false\n"
|
|
79
|
+
else:
|
|
80
|
+
body = "features.multi_agent = false\n"
|
|
81
|
+
block = f"{MULTI_AGENT_BEGIN}\n{body}{MULTI_AGENT_END}\n"
|
|
82
|
+
# Prefer injecting inside [features] when present.
|
|
83
|
+
if in_features:
|
|
84
|
+
lines = existing.splitlines(keepends=True)
|
|
85
|
+
out: list[str] = []
|
|
86
|
+
injected = False
|
|
87
|
+
for i, line in enumerate(lines):
|
|
88
|
+
out.append(line)
|
|
89
|
+
if not injected and re.match(r"^\s*\[\s*features\s*\]\s*(?:#.*)?$", line.strip()):
|
|
90
|
+
out.append(block)
|
|
91
|
+
injected = True
|
|
92
|
+
text = "".join(out) if injected else block + existing
|
|
93
|
+
else:
|
|
94
|
+
text = block + (existing if existing.endswith("\n") or not existing else existing + "\n")
|
|
95
|
+
try:
|
|
96
|
+
config_path.write_text(text, encoding="utf-8")
|
|
97
|
+
except OSError as e:
|
|
98
|
+
log.warning("write multi-agent config failed: %s", e)
|