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.
Files changed (58) hide show
  1. aicortexpy/__init__.py +3 -0
  2. aicortexpy/__main__.py +5 -0
  3. aicortexpy/acp_client.py +559 -0
  4. aicortexpy/acp_conn.py +853 -0
  5. aicortexpy/acp_errors.py +107 -0
  6. aicortexpy/agent_env.py +88 -0
  7. aicortexpy/artifact_ws.py +233 -0
  8. aicortexpy/artifacts.py +67 -0
  9. aicortexpy/attachments.py +85 -0
  10. aicortexpy/auto_commit.py +106 -0
  11. aicortexpy/capabilities.py +92 -0
  12. aicortexpy/chat_acp_pool.py +321 -0
  13. aicortexpy/cli.py +133 -0
  14. aicortexpy/client.py +438 -0
  15. aicortexpy/codex_config.py +98 -0
  16. aicortexpy/config.py +298 -0
  17. aicortexpy/daemon.py +677 -0
  18. aicortexpy/data/default_skills/agent-memory/SKILL.md +67 -0
  19. aicortexpy/data/default_skills/aicorkit-atlassian/SKILL.md +144 -0
  20. aicortexpy/data/default_skills/aicorkit-graph/SKILL.md +145 -0
  21. aicortexpy/data/default_skills/aicorkit-outlook/SKILL.md +87 -0
  22. aicortexpy/data/default_skills/aicorkit-wxmsg/SKILL.md +96 -0
  23. aicortexpy/data/default_skills/aicortex-basics/SKILL.md +10 -0
  24. aicortexpy/data/default_skills/aicortex-bridge-cdp/SKILL.md +118 -0
  25. aicortexpy/data/default_skills/aicortex-cli-ops/SKILL.md +100 -0
  26. aicortexpy/data/default_skills/aicortex-graph/SKILL.md +30 -0
  27. aicortexpy/data/default_skills/graphify/SKILL.md +97 -0
  28. aicortexpy/data/default_skills/question-form/SKILL.md +162 -0
  29. aicortexpy/data/default_skills/test-report-generator/SKILL.md +179 -0
  30. aicortexpy/data/default_skills/test-report-generator/scripts/generate_cases.py +146 -0
  31. aicortexpy/data/default_skills/test-report-generator/scripts/generate_report.py +275 -0
  32. aicortexpy/design_seed.py +59 -0
  33. aicortexpy/diskusage.py +209 -0
  34. aicortexpy/execenv.py +264 -0
  35. aicortexpy/gc.py +286 -0
  36. aicortexpy/handoff_bundle.py +65 -0
  37. aicortexpy/health.py +178 -0
  38. aicortexpy/local_skills.py +157 -0
  39. aicortexpy/models.py +385 -0
  40. aicortexpy/openclaw_config.py +153 -0
  41. aicortexpy/pinned_workdir.py +60 -0
  42. aicortexpy/platform_info.py +61 -0
  43. aicortexpy/poisoned.py +44 -0
  44. aicortexpy/process.py +168 -0
  45. aicortexpy/procutil.py +61 -0
  46. aicortexpy/prompt.py +235 -0
  47. aicortexpy/repo.py +462 -0
  48. aicortexpy/runner.py +969 -0
  49. aicortexpy/runtime_config.py +540 -0
  50. aicortexpy/sandbox.py +264 -0
  51. aicortexpy/stream.py +422 -0
  52. aicortexpy/terminal.py +147 -0
  53. aicortexpy/tunnel.py +82 -0
  54. aicortexpy/wakeup.py +222 -0
  55. aicortexpy-0.1.0.dist-info/METADATA +106 -0
  56. aicortexpy-0.1.0.dist-info/RECORD +58 -0
  57. aicortexpy-0.1.0.dist-info/WHEEL +5 -0
  58. aicortexpy-0.1.0.dist-info/top_level.txt +1 -0
aicortexpy/models.py ADDED
@@ -0,0 +1,385 @@
1
+ """Model list discovery for heartbeat pending_model_list."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import os
8
+ import re
9
+ import subprocess
10
+ import tempfile
11
+ import threading
12
+ import time
13
+ from typing import Any, Callable
14
+
15
+ log = logging.getLogger("aicortexpy.models")
16
+
17
+ _CACHE_TTL = 60.0
18
+ _cache_mu = threading.Lock()
19
+ _cache: dict[str, tuple[float, list[dict[str, Any]]]] = {}
20
+
21
+ _STATIC: dict[str, list[dict[str, Any]]] = {
22
+ "claude": [
23
+ {"id": "sonnet", "label": "Claude Sonnet", "provider": "claude", "default": True},
24
+ {"id": "opus", "label": "Claude Opus", "provider": "claude"},
25
+ {"id": "haiku", "label": "Claude Haiku", "provider": "claude"},
26
+ ],
27
+ "codex": [
28
+ {"id": "o4-mini", "label": "o4-mini", "provider": "codex", "default": True},
29
+ {"id": "gpt-5", "label": "GPT-5", "provider": "codex"},
30
+ ],
31
+ "cursor": [
32
+ {"id": "auto", "label": "Auto", "provider": "cursor", "default": True},
33
+ ],
34
+ "gemini": [
35
+ {"id": "gemini-2.5-pro", "label": "Gemini 2.5 Pro", "provider": "gemini", "default": True},
36
+ ],
37
+ "copilot": [
38
+ {"id": "gpt-4.1", "label": "GPT-4.1", "provider": "copilot", "default": True},
39
+ {"id": "claude-sonnet-4", "label": "Claude Sonnet 4", "provider": "copilot"},
40
+ ],
41
+ }
42
+
43
+ _ACP_PROVIDERS = frozenset({"hermes", "kimi", "kiro", "dsh", "copilot"})
44
+
45
+
46
+ def list_models(provider: str, executable: str) -> list[dict[str, Any]]:
47
+ key = f"{provider}:{executable}"
48
+ now = time.time()
49
+ with _cache_mu:
50
+ hit = _cache.get(key)
51
+ if hit and now - hit[0] < _CACHE_TTL:
52
+ return list(hit[1])
53
+
54
+ models = _discover(provider, executable)
55
+ if not models:
56
+ models = list(_STATIC.get(provider, []))
57
+ with _cache_mu:
58
+ _cache[key] = (now, list(models))
59
+ return models
60
+
61
+
62
+ def model_selection_supported(provider: str) -> bool:
63
+ return provider in _STATIC or provider in {
64
+ "claude",
65
+ "codex",
66
+ "cursor",
67
+ "gemini",
68
+ "copilot",
69
+ "opencode",
70
+ "pi",
71
+ "hermes",
72
+ "kimi",
73
+ "kiro",
74
+ "dsh",
75
+ "openclaw",
76
+ }
77
+
78
+
79
+ def _discover(provider: str, executable: str) -> list[dict[str, Any]]:
80
+ if provider in ("claude", "codex", "gemini"):
81
+ return list(_STATIC.get(provider, []))
82
+ if provider == "opencode":
83
+ return _probe_args(executable, ["models"], provider) or _probe_cli(provider, executable)
84
+ if provider == "pi":
85
+ return _probe_args(executable, ["--list-models"], provider) or _probe_cli(
86
+ provider, executable
87
+ )
88
+ if provider == "cursor":
89
+ return _probe_args(executable, ["--list-models"], provider) or _probe_cli(
90
+ provider, executable
91
+ )
92
+ if provider == "openclaw":
93
+ return _discover_openclaw_agents(executable)
94
+ if provider in _ACP_PROVIDERS:
95
+ acp_args = _acp_args_for(provider)
96
+ extra_env = {"HERMES_YOLO_MODE": "1"} if provider == "hermes" else None
97
+ parse = _parse_dsh_session_models if provider == "dsh" else _parse_acp_session_models
98
+ models = discover_acp_models(executable, acp_args, extra_env=extra_env, parse_fn=parse)
99
+ if models:
100
+ return models
101
+ if provider == "copilot":
102
+ return list(_STATIC["copilot"])
103
+ return []
104
+ return _probe_cli(provider, executable)
105
+
106
+
107
+ def _acp_args_for(provider: str) -> list[str]:
108
+ if provider == "copilot":
109
+ return ["--acp"]
110
+ if provider == "dsh":
111
+ return ["--profile", "acp"]
112
+ if provider == "kiro":
113
+ return ["acp"]
114
+ return ["acp"]
115
+
116
+
117
+ def discover_acp_models(
118
+ executable: str,
119
+ acp_args: list[str],
120
+ *,
121
+ extra_env: dict[str, str] | None = None,
122
+ parse_fn: Callable[[dict[str, Any], str], list[dict[str, Any]]] | None = None,
123
+ provider: str = "",
124
+ ) -> list[dict[str, Any]]:
125
+ """Spawn ACP, initialize + session/new, parse availableModels (Go discoverACPModels)."""
126
+ parse_fn = parse_fn or _parse_acp_session_models
127
+ env = os.environ.copy()
128
+ if extra_env:
129
+ env.update(extra_env)
130
+ try:
131
+ proc = subprocess.Popen(
132
+ [executable, *acp_args],
133
+ stdin=subprocess.PIPE,
134
+ stdout=subprocess.PIPE,
135
+ stderr=subprocess.DEVNULL,
136
+ text=True,
137
+ env=env,
138
+ bufsize=1,
139
+ )
140
+ except OSError as e:
141
+ log.debug("acp model discovery spawn failed: %s", e)
142
+ return []
143
+
144
+ cwd = tempfile.gettempdir()
145
+ reqs = [
146
+ {
147
+ "jsonrpc": "2.0",
148
+ "id": 1,
149
+ "method": "initialize",
150
+ "params": {
151
+ "protocolVersion": 1,
152
+ "clientInfo": {"name": "aicortex-model-discovery", "version": "0.1.0"},
153
+ "clientCapabilities": {},
154
+ },
155
+ },
156
+ {
157
+ "jsonrpc": "2.0",
158
+ "id": 2,
159
+ "method": "session/new",
160
+ "params": {"cwd": cwd, "mcpServers": []},
161
+ },
162
+ ]
163
+ try:
164
+ assert proc.stdin is not None
165
+ for req in reqs:
166
+ proc.stdin.write(json.dumps(req) + "\n")
167
+ proc.stdin.flush()
168
+ except OSError:
169
+ _kill(proc)
170
+ return []
171
+
172
+ deadline = time.time() + 12.0
173
+ result_obj: dict[str, Any] | None = None
174
+ try:
175
+ assert proc.stdout is not None
176
+ while time.time() < deadline:
177
+ line = proc.stdout.readline()
178
+ if not line:
179
+ if proc.poll() is not None:
180
+ break
181
+ time.sleep(0.05)
182
+ continue
183
+ try:
184
+ msg = json.loads(line)
185
+ except json.JSONDecodeError:
186
+ continue
187
+ if not isinstance(msg, dict):
188
+ continue
189
+ if msg.get("id") == 2 and "result" in msg:
190
+ result_obj = msg.get("result") if isinstance(msg.get("result"), dict) else {}
191
+ break
192
+ finally:
193
+ _kill(proc)
194
+
195
+ if not isinstance(result_obj, dict):
196
+ return []
197
+ return parse_fn(result_obj, provider or "acp")
198
+
199
+
200
+ def _kill(proc: subprocess.Popen) -> None:
201
+ try:
202
+ if proc.stdin:
203
+ proc.stdin.close()
204
+ except OSError:
205
+ pass
206
+ try:
207
+ proc.kill()
208
+ except OSError:
209
+ pass
210
+ try:
211
+ proc.wait(timeout=2)
212
+ except Exception:
213
+ pass
214
+
215
+
216
+ def _parse_acp_session_models(result: dict[str, Any], provider: str) -> list[dict[str, Any]]:
217
+ models_obj = result.get("models")
218
+ available: list[Any] = []
219
+ current = ""
220
+ if isinstance(models_obj, dict):
221
+ available = models_obj.get("availableModels") or []
222
+ current = str(models_obj.get("currentModelId") or "")
223
+ elif isinstance(result.get("availableModels"), list):
224
+ available = result["availableModels"]
225
+ current = str(result.get("currentModelId") or "")
226
+ out: list[dict[str, Any]] = []
227
+ for i, m in enumerate(available):
228
+ if not isinstance(m, dict):
229
+ continue
230
+ mid = str(m.get("modelId") or m.get("id") or "").strip()
231
+ if not mid:
232
+ continue
233
+ label = str(m.get("name") or m.get("label") or mid)
234
+ out.append(
235
+ {
236
+ "id": mid,
237
+ "label": label,
238
+ "provider": provider or "acp",
239
+ "default": (current and mid == current) or (not current and i == 0),
240
+ }
241
+ )
242
+ return out
243
+
244
+
245
+ def _parse_dsh_session_models(result: dict[str, Any], provider: str) -> list[dict[str, Any]]:
246
+ opts = result.get("configOptions")
247
+ if not isinstance(opts, list):
248
+ return _parse_acp_session_models(result, provider)
249
+ for opt in opts:
250
+ if not isinstance(opt, dict):
251
+ continue
252
+ if str(opt.get("id") or "") != "model":
253
+ continue
254
+ choices = opt.get("options") or opt.get("values") or []
255
+ out: list[dict[str, Any]] = []
256
+ current = str(opt.get("value") or "")
257
+ if isinstance(choices, list):
258
+ for i, c in enumerate(choices):
259
+ if isinstance(c, dict):
260
+ mid = str(c.get("value") or c.get("id") or "").strip()
261
+ label = str(c.get("label") or c.get("name") or mid)
262
+ else:
263
+ mid = str(c).strip()
264
+ label = mid
265
+ if not mid:
266
+ continue
267
+ out.append(
268
+ {
269
+ "id": mid,
270
+ "label": label,
271
+ "provider": provider or "dsh",
272
+ "default": (current and mid == current) or (not current and i == 0),
273
+ }
274
+ )
275
+ return out
276
+ return _parse_acp_session_models(result, provider)
277
+
278
+
279
+ def _discover_openclaw_agents(executable: str) -> list[dict[str, Any]]:
280
+ for args in (["agents", "list", "--json"], ["agents", "list"]):
281
+ try:
282
+ proc = subprocess.run(
283
+ [executable, *args],
284
+ capture_output=True,
285
+ text=True,
286
+ timeout=20,
287
+ check=False,
288
+ )
289
+ except Exception:
290
+ continue
291
+ out = (proc.stdout or "").strip()
292
+ if proc.returncode != 0 or not out:
293
+ continue
294
+ try:
295
+ data = json.loads(out)
296
+ except json.JSONDecodeError:
297
+ continue
298
+ entries = data if isinstance(data, list) else (data.get("agents") if isinstance(data, dict) else None)
299
+ if not isinstance(entries, list):
300
+ continue
301
+ models: list[dict[str, Any]] = []
302
+ for i, e in enumerate(entries):
303
+ if isinstance(e, dict):
304
+ mid = str(e.get("id") or e.get("name") or "").strip()
305
+ label = str(e.get("name") or e.get("id") or mid)
306
+ else:
307
+ mid = str(e).strip()
308
+ label = mid
309
+ if not mid:
310
+ continue
311
+ models.append(
312
+ {"id": mid, "label": label, "provider": "openclaw", "default": i == 0}
313
+ )
314
+ if models:
315
+ return models
316
+ return []
317
+
318
+
319
+ def _probe_args(executable: str, args: list[str], provider: str) -> list[dict[str, Any]]:
320
+ try:
321
+ proc = subprocess.run(
322
+ [executable, *args],
323
+ capture_output=True,
324
+ text=True,
325
+ timeout=20,
326
+ check=False,
327
+ )
328
+ except Exception:
329
+ return []
330
+ out = (proc.stdout or "") + "\n" + (proc.stderr or "")
331
+ if proc.returncode != 0 or not out.strip():
332
+ return []
333
+ return _parse_model_lines(out, provider)
334
+
335
+
336
+ def _probe_cli(provider: str, executable: str) -> list[dict[str, Any]]:
337
+ for args in (
338
+ [executable, "models"],
339
+ [executable, "--list-models"],
340
+ [executable, "model", "list"],
341
+ ):
342
+ try:
343
+ proc = subprocess.run(
344
+ args,
345
+ capture_output=True,
346
+ text=True,
347
+ timeout=20,
348
+ check=False,
349
+ )
350
+ except Exception:
351
+ continue
352
+ out = (proc.stdout or "") + "\n" + (proc.stderr or "")
353
+ if proc.returncode != 0 or not out.strip():
354
+ continue
355
+ models = _parse_model_lines(out, provider)
356
+ if models:
357
+ return models
358
+ return []
359
+
360
+
361
+ def _parse_model_lines(text: str, provider: str) -> list[dict[str, Any]]:
362
+ models: list[dict[str, Any]] = []
363
+ seen: set[str] = set()
364
+ for line in text.splitlines():
365
+ line = line.strip().strip("-*• ")
366
+ if not line or line.lower().startswith("model"):
367
+ continue
368
+ m = re.match(r"([A-Za-z0-9._:/-]+)", line)
369
+ if not m:
370
+ continue
371
+ mid = m.group(1)
372
+ if mid.lower() in {"id", "name", "models"} or mid in seen:
373
+ continue
374
+ seen.add(mid)
375
+ models.append(
376
+ {
377
+ "id": mid,
378
+ "label": line if len(line) < 80 else mid,
379
+ "provider": provider,
380
+ "default": len(models) == 0,
381
+ }
382
+ )
383
+ if len(models) >= 40:
384
+ break
385
+ return models
@@ -0,0 +1,153 @@
1
+ """Per-task OpenClaw config wrapper (OPENCLAW_CONFIG_PATH + workspace pin)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import os
8
+ import shutil
9
+ import subprocess
10
+ from copy import deepcopy
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+ log = logging.getLogger("aicortexpy.openclaw_config")
15
+
16
+ OPENCLAW_CONFIG_FILE = "openclaw-config.json"
17
+ CLI_TIMEOUT = 5.0
18
+
19
+
20
+ class OpenclawConfigError(RuntimeError):
21
+ """Fail-closed prepare error (matches Go execenv prepareOpenclawConfig)."""
22
+
23
+
24
+ @dataclass
25
+ class OpenclawConfigResult:
26
+ config_path: str = ""
27
+ include_root: str = ""
28
+ env_updates: dict[str, str] | None = None
29
+
30
+
31
+ def prepare_openclaw_config(
32
+ env_root: Path,
33
+ work_dir: Path,
34
+ *,
35
+ openclaw_bin: str = "",
36
+ ) -> OpenclawConfigResult:
37
+ """Write env_root/openclaw-config.json pinning agents.*.workspace to work_dir."""
38
+ bin_name = openclaw_bin or shutil.which("openclaw") or "openclaw"
39
+ active_path, exists = _active_config_path(bin_name)
40
+
41
+ work = str(work_dir.resolve())
42
+ agents_list = _resolved_agents_list(bin_name) if exists else []
43
+ rewritten = _rewrite_agents_list_workspaces(agents_list, work)
44
+
45
+ wrapper: dict = {
46
+ "agents": {
47
+ "defaults": {"workspace": work},
48
+ "list": rewritten,
49
+ }
50
+ }
51
+ include_root = ""
52
+ if exists and active_path:
53
+ wrapper["$include"] = [active_path]
54
+ include_root = str(Path(active_path).resolve().parent)
55
+
56
+ env_root.mkdir(parents=True, exist_ok=True)
57
+ out_path = env_root / OPENCLAW_CONFIG_FILE
58
+ out_path.write_text(json.dumps(wrapper, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
59
+
60
+ env_updates = {"OPENCLAW_CONFIG_PATH": str(out_path.resolve())}
61
+ if include_root:
62
+ prev = os.environ.get("OPENCLAW_INCLUDE_ROOTS", "")
63
+ roots = [include_root] + ([p for p in prev.split(os.pathsep) if p] if prev else [])
64
+ seen: set[str] = set()
65
+ ordered: list[str] = []
66
+ for r in roots:
67
+ if r not in seen:
68
+ seen.add(r)
69
+ ordered.append(r)
70
+ env_updates["OPENCLAW_INCLUDE_ROOTS"] = os.pathsep.join(ordered)
71
+
72
+ log.info("openclaw config written path=%s include_root=%s", out_path, include_root or "-")
73
+ return OpenclawConfigResult(
74
+ config_path=str(out_path),
75
+ include_root=include_root,
76
+ env_updates=env_updates,
77
+ )
78
+
79
+
80
+ def _run(bin_name: str, args: list[str]) -> str:
81
+ try:
82
+ proc = subprocess.run(
83
+ [bin_name, *args],
84
+ capture_output=True,
85
+ text=True,
86
+ timeout=CLI_TIMEOUT,
87
+ check=False,
88
+ )
89
+ except FileNotFoundError as e:
90
+ raise OpenclawConfigError(f"openclaw executable not found: {bin_name}") from e
91
+ except subprocess.TimeoutExpired as e:
92
+ raise OpenclawConfigError(f"openclaw {' '.join(args)} timed out") from e
93
+ except OSError as e:
94
+ raise OpenclawConfigError(f"openclaw cli failed: {e}") from e
95
+ if proc.returncode != 0:
96
+ err = (proc.stderr or proc.stdout or "").strip()[:500]
97
+ raise OpenclawConfigError(
98
+ f"openclaw {' '.join(args)} exited {proc.returncode}: {err or 'no output'}"
99
+ )
100
+ return (proc.stdout or "").strip()
101
+
102
+
103
+ def _active_config_path(bin_name: str) -> tuple[str, bool]:
104
+ try:
105
+ out = _run(bin_name, ["config", "file"])
106
+ except OpenclawConfigError:
107
+ default = Path.home() / ".openclaw" / "openclaw.json"
108
+ if default.is_file():
109
+ return str(default), True
110
+ # Fresh install: allow minimal wrapper with no $include.
111
+ return str(default), False
112
+ path = out.splitlines()[0].strip()
113
+ if not path:
114
+ default = Path.home() / ".openclaw" / "openclaw.json"
115
+ return str(default), default.is_file()
116
+ p = Path(path)
117
+ if not p.is_file():
118
+ # Path reported but missing → fresh install; fail-open to minimal wrapper.
119
+ return path, False
120
+ return path, True
121
+
122
+
123
+ def _resolved_agents_list(bin_name: str) -> list[dict]:
124
+ out = _run(bin_name, ["config", "get", "agents.list", "--json"])
125
+ if not out:
126
+ return []
127
+ try:
128
+ data = json.loads(out)
129
+ except json.JSONDecodeError as e:
130
+ raise OpenclawConfigError(f"openclaw agents.list JSON invalid: {e}") from e
131
+ if data is None:
132
+ return []
133
+ if not isinstance(data, list):
134
+ raise OpenclawConfigError("openclaw agents.list is not a JSON array")
135
+ entries: list[dict] = []
136
+ for item in data:
137
+ if isinstance(item, dict):
138
+ entries.append(item)
139
+ elif isinstance(item, str) and item.strip():
140
+ entries.append({"id": item.strip()})
141
+ return entries
142
+
143
+
144
+ def _rewrite_agents_list_workspaces(entries: list[dict], work: str) -> list[dict]:
145
+ """Deep-copy agent entries and pin workspace (Go rewriteAgentsListWorkspaces)."""
146
+ out: list[dict] = []
147
+ for entry in entries:
148
+ copied = deepcopy(entry)
149
+ if not isinstance(copied, dict):
150
+ continue
151
+ copied["workspace"] = work
152
+ out.append(copied)
153
+ return out
@@ -0,0 +1,60 @@
1
+ """Shared project workdir layout."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from .client import Task
11
+
12
+ log = logging.getLogger("aicortexpy.pinned")
13
+
14
+ STAGE_DIRS = ("docs", "design", "app", "test", "release")
15
+
16
+
17
+ def project_uses_pinned_workdir(task: Task) -> bool:
18
+ return bool(task.project_id.strip())
19
+
20
+
21
+ def extract_local_path(resources: list[dict[str, Any]]) -> str:
22
+ for r in resources:
23
+ if r.get("resource_type") != "local_path":
24
+ continue
25
+ ref = r.get("resource_ref")
26
+ if isinstance(ref, str):
27
+ try:
28
+ ref = json.loads(ref)
29
+ except json.JSONDecodeError:
30
+ continue
31
+ if isinstance(ref, dict) and ref.get("path"):
32
+ return str(ref["path"])
33
+ return ""
34
+
35
+
36
+ def pinned_workdir_path(workspaces_root: Path, workspace_id: str, project_id: str) -> Path:
37
+ return workspaces_root / workspace_id / "projects" / project_id
38
+
39
+
40
+ def ensure_project_layout(root: Path) -> None:
41
+ root.mkdir(parents=True, exist_ok=True)
42
+ for name in STAGE_DIRS:
43
+ (root / name).mkdir(parents=True, exist_ok=True)
44
+
45
+
46
+ def prepare_pinned_workdir(
47
+ workspaces_root: Path,
48
+ task: Task,
49
+ ) -> Path:
50
+ local = extract_local_path(task.project_resources)
51
+ if local:
52
+ root = Path(local)
53
+ ensure_project_layout(root)
54
+ log.info("using local_path project workdir %s", root)
55
+ return root
56
+ root = pinned_workdir_path(
57
+ workspaces_root, task.workspace_id or "default", task.project_id
58
+ )
59
+ ensure_project_layout(root)
60
+ return root
@@ -0,0 +1,61 @@
1
+ """Cross-platform helpers shared by daemon / sandbox / process."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import platform
7
+ import shutil
8
+ import sys
9
+ from typing import Literal
10
+
11
+ HostOS = Literal["windows", "macos", "linux", "unknown"]
12
+
13
+
14
+ def host_os() -> HostOS:
15
+ if sys.platform == "win32":
16
+ return "windows"
17
+ if sys.platform == "darwin":
18
+ return "macos"
19
+ if sys.platform.startswith("linux"):
20
+ return "linux"
21
+ return "unknown"
22
+
23
+
24
+ def is_wsl() -> bool:
25
+ if host_os() != "linux":
26
+ return False
27
+ try:
28
+ data = open("/proc/version", encoding="utf-8").read().lower()
29
+ return "microsoft" in data or "wsl" in data
30
+ except OSError:
31
+ return False
32
+
33
+
34
+ def which(cmd: str) -> str | None:
35
+ return shutil.which(cmd)
36
+
37
+
38
+ def has_bubblewrap() -> bool:
39
+ return which("bwrap") is not None
40
+
41
+
42
+ def has_sandbox_exec() -> bool:
43
+ return host_os() == "macos" and which("sandbox-exec") is not None
44
+
45
+
46
+ def platform_label() -> str:
47
+ bits = [
48
+ host_os(),
49
+ platform.machine(),
50
+ f"py{sys.version_info.major}.{sys.version_info.minor}",
51
+ ]
52
+ if is_wsl():
53
+ bits.insert(1, "wsl")
54
+ return "/".join(bits)
55
+
56
+
57
+ def env_truthy(name: str, default: bool = False) -> bool:
58
+ raw = os.environ.get(name)
59
+ if raw is None:
60
+ return default
61
+ return raw.strip().lower() in {"1", "true", "yes", "on"}
aicortexpy/poisoned.py ADDED
@@ -0,0 +1,44 @@
1
+ """Classify poisoned agent outputs/errors (aligned with Go poisoned.go)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ POISONED_OUTPUT_MAX_LEN = 320
6
+
7
+ FAILURE_ITERATION_LIMIT = "iteration_limit"
8
+ FAILURE_AGENT_FALLBACK = "agent_fallback_message"
9
+ FAILURE_API_INVALID = "api_invalid_request"
10
+
11
+ _MARKERS = (
12
+ ("i reached the iteration limit", FAILURE_ITERATION_LIMIT),
13
+ ("put your final update inside the content string", FAILURE_AGENT_FALLBACK),
14
+ )
15
+
16
+
17
+ def classify_poisoned_output(output: str) -> tuple[str, bool]:
18
+ trimmed = (output or "").strip()
19
+ if not trimmed or len(trimmed) > POISONED_OUTPUT_MAX_LEN:
20
+ return "", False
21
+ lowered = trimmed.lower()
22
+ for substr, reason in _MARKERS:
23
+ if substr in lowered:
24
+ return reason, True
25
+ return "", False
26
+
27
+
28
+ def classify_poisoned_error(err: str) -> tuple[str, bool]:
29
+ lowered = (err or "").lower()
30
+ needles = (
31
+ "invalid_request_error",
32
+ "invalid request",
33
+ "prompt is too long",
34
+ "context_length_exceeded",
35
+ "image exceeds",
36
+ "could not process image",
37
+ )
38
+ if any(n in lowered for n in needles) and (
39
+ "400" in lowered or "invalid" in lowered or "exceed" in lowered
40
+ ):
41
+ return FAILURE_API_INVALID, True
42
+ if "invalid_request_error" in lowered:
43
+ return FAILURE_API_INVALID, True
44
+ return "", False