aicortexpy 0.2.1__tar.gz → 0.2.3__tar.gz
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-0.2.1 → aicortexpy-0.2.3}/PKG-INFO +10 -7
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/README.md +9 -6
- aicortexpy-0.2.3/aicortex/__init__.py +9 -0
- aicortexpy-0.2.3/aicortex/__main__.py +5 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/__init__.py +1 -1
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/acp_client.py +92 -3
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/acp_conn.py +62 -13
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/acp_errors.py +23 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/agent_env.py +14 -3
- aicortexpy-0.2.3/aicortexpy/app_workdir.py +72 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/artifact_ws.py +5 -1
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/artifacts.py +6 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/auto_commit.py +3 -1
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/chat_acp_pool.py +10 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/cli.py +1 -1
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/client.py +116 -1
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/agent_cmd.py +5 -0
- aicortexpy-0.2.3/aicortexpy/commands/graph_cmd.py +204 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/memory_cmd.py +1 -1
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/config.py +4 -2
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/daemon.py +46 -10
- aicortexpy-0.2.3/aicortexpy/data/default_skills/aicortex-graph/SKILL.md +46 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/execenv.py +13 -1
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/models.py +12 -3
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/openclaw_config.py +3 -1
- aicortexpy-0.2.3/aicortexpy/permission.py +107 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/process.py +35 -16
- aicortexpy-0.2.3/aicortexpy/procutil.py +113 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/repo.py +13 -12
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/runner.py +76 -9
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/runtime_config.py +73 -1
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/sandbox.py +2 -2
- aicortexpy-0.2.3/aicortexpy/security.py +168 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/terminal.py +3 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/wakeup.py +15 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy.egg-info/PKG-INFO +10 -7
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy.egg-info/SOURCES.txt +9 -1
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy.egg-info/entry_points.txt +0 -1
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy.egg-info/top_level.txt +1 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/pyproject.toml +9 -4
- aicortexpy-0.2.3/scripts/aicortex +3 -0
- aicortexpy-0.2.3/scripts/aicortex.cmd +3 -0
- aicortexpy-0.2.3/tests/test_app_workdir.py +59 -0
- aicortexpy-0.2.1/aicortexpy/commands/graph_cmd.py +0 -69
- aicortexpy-0.2.1/aicortexpy/data/default_skills/aicortex-graph/SKILL.md +0 -30
- aicortexpy-0.2.1/aicortexpy/procutil.py +0 -61
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/MANIFEST.in +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/__main__.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/attachments.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/capabilities.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/cli_context.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/codex_config.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/__init__.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/attachment_cmd.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/auth_cmd.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/autopilot_cmd.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/config_cmd.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/daemon_cmd.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/issue_cmd.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/label_cmd.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/meeting_cmd.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/project_cmd.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/repo_cmd.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/runtime_cmd.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/setup_cmd.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/skill_cmd.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/squad_cmd.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/update_cmd.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/version_cmd.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/workspace_cmd.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/agent-memory/SKILL.md +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/aicorkit-atlassian/SKILL.md +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/aicorkit-graph/SKILL.md +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/aicorkit-outlook/SKILL.md +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/aicorkit-wxmsg/SKILL.md +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/aicortex-basics/SKILL.md +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/aicortex-bridge-cdp/SKILL.md +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/aicortex-cli-ops/SKILL.md +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/graphify/SKILL.md +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/question-form/SKILL.md +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/test-report-generator/SKILL.md +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/test-report-generator/scripts/generate_cases.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/test-report-generator/scripts/generate_report.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/design_seed.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/diskusage.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/gc.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/handoff_bundle.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/health.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/id_resolve.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/local_skills.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/oauth_login.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/output.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/pinned_workdir.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/platform_client.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/platform_info.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/poisoned.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/prompt.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/stream.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/tunnel.py +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy.egg-info/dependency_links.txt +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy.egg-info/requires.txt +0 -0
- {aicortexpy-0.2.1 → aicortexpy-0.2.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: aicortexpy
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: AICortex Python CLI and local agent runtime daemon (Go CLI compatible)
|
|
5
5
|
Author: AICortex
|
|
6
6
|
License: Apache-2.0
|
|
@@ -26,20 +26,23 @@ aicortex daemon start
|
|
|
26
26
|
aicortex issue list --output json
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
等价入口:`python -m aicortex`(推荐)/ `python -m aicortexpy` / `aicortexpy`。
|
|
30
30
|
|
|
31
31
|
| | |
|
|
32
32
|
|--|--|
|
|
33
|
-
| PyPI
|
|
34
|
-
|
|
|
33
|
+
| PyPI 包名 | `aicortexpy` |
|
|
34
|
+
| 模块 | `aicortex`(`-m`)、`aicortexpy` |
|
|
35
|
+
| PATH 命令 | Windows:`aicortex.cmd` → `python -m aicortex`(**不**生成 `aicortex.exe`,避免与 Go 安装器 / `.exe` 探测冲突);另有 `aicortexpy.exe` |
|
|
35
36
|
| 配置 | 与 Go 共用 `~/.aicortex/config.json` |
|
|
36
37
|
|
|
37
38
|
## 安装
|
|
38
39
|
|
|
39
40
|
```powershell
|
|
40
41
|
pip install -U aicortexpy
|
|
42
|
+
# 若以前装过会留下 Scripts\aicortex.exe,可手动删掉,让 aicortex.cmd 生效:
|
|
43
|
+
# Remove-Item "$env:LOCALAPPDATA\Programs\Python\Python3*\Scripts\aicortex.exe" -ErrorAction SilentlyContinue
|
|
41
44
|
# 源码:
|
|
42
|
-
cd python-
|
|
45
|
+
cd python-aicortex
|
|
43
46
|
pip install -e .
|
|
44
47
|
```
|
|
45
48
|
|
|
@@ -73,7 +76,7 @@ Health 默认端口 **19514**。不要与 Go daemon 同时跑。
|
|
|
73
76
|
## 发布
|
|
74
77
|
|
|
75
78
|
```powershell
|
|
76
|
-
cd python-
|
|
79
|
+
cd python-aicortex
|
|
77
80
|
python -m build
|
|
78
81
|
python -m twine upload dist/*
|
|
79
82
|
```
|
|
@@ -84,4 +87,4 @@ python -m twine upload dist/*
|
|
|
84
87
|
|
|
85
88
|
已覆盖 Agent/日常平台命令(issue/workspace/agent/squad/project/label/attachment/autopilot/meeting/skill/memory/graph/runtime/repo/auth/config/daemon)。
|
|
86
89
|
|
|
87
|
-
|
|
90
|
+
相对 Go:`setup` / `update`(pip) / OAuth login / 平台命令已对齐;二进制自更新请用 `pip install -U aicortexpy`。
|
|
@@ -9,20 +9,23 @@ aicortex daemon start
|
|
|
9
9
|
aicortex issue list --output json
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
等价入口:`python -m aicortex`(推荐)/ `python -m aicortexpy` / `aicortexpy`。
|
|
13
13
|
|
|
14
14
|
| | |
|
|
15
15
|
|--|--|
|
|
16
|
-
| PyPI
|
|
17
|
-
|
|
|
16
|
+
| PyPI 包名 | `aicortexpy` |
|
|
17
|
+
| 模块 | `aicortex`(`-m`)、`aicortexpy` |
|
|
18
|
+
| PATH 命令 | Windows:`aicortex.cmd` → `python -m aicortex`(**不**生成 `aicortex.exe`,避免与 Go 安装器 / `.exe` 探测冲突);另有 `aicortexpy.exe` |
|
|
18
19
|
| 配置 | 与 Go 共用 `~/.aicortex/config.json` |
|
|
19
20
|
|
|
20
21
|
## 安装
|
|
21
22
|
|
|
22
23
|
```powershell
|
|
23
24
|
pip install -U aicortexpy
|
|
25
|
+
# 若以前装过会留下 Scripts\aicortex.exe,可手动删掉,让 aicortex.cmd 生效:
|
|
26
|
+
# Remove-Item "$env:LOCALAPPDATA\Programs\Python\Python3*\Scripts\aicortex.exe" -ErrorAction SilentlyContinue
|
|
24
27
|
# 源码:
|
|
25
|
-
cd python-
|
|
28
|
+
cd python-aicortex
|
|
26
29
|
pip install -e .
|
|
27
30
|
```
|
|
28
31
|
|
|
@@ -56,7 +59,7 @@ Health 默认端口 **19514**。不要与 Go daemon 同时跑。
|
|
|
56
59
|
## 发布
|
|
57
60
|
|
|
58
61
|
```powershell
|
|
59
|
-
cd python-
|
|
62
|
+
cd python-aicortex
|
|
60
63
|
python -m build
|
|
61
64
|
python -m twine upload dist/*
|
|
62
65
|
```
|
|
@@ -67,4 +70,4 @@ python -m twine upload dist/*
|
|
|
67
70
|
|
|
68
71
|
已覆盖 Agent/日常平台命令(issue/workspace/agent/squad/project/label/attachment/autopilot/meeting/skill/memory/graph/runtime/repo/auth/config/daemon)。
|
|
69
72
|
|
|
70
|
-
|
|
73
|
+
相对 Go:`setup` / `update`(pip) / OAuth login / 平台命令已对齐;二进制自更新请用 `pip install -U aicortexpy`。
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Top-level alias package so `python -m aicortex` works after install.
|
|
2
|
+
|
|
3
|
+
The implementation lives in ``aicortexpy``; this package exists only as the
|
|
4
|
+
``-m aicortex`` / PATH shim target (especially on Windows via ``aicortex.cmd``).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from aicortexpy import __version__
|
|
8
|
+
|
|
9
|
+
__all__ = ["__version__"]
|
|
@@ -51,7 +51,15 @@ class _PendingTool:
|
|
|
51
51
|
class ACPClient:
|
|
52
52
|
"""Thread-safe JSON-RPC client for one ACP child process."""
|
|
53
53
|
|
|
54
|
-
def __init__(
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
stdin: TextIO,
|
|
57
|
+
*,
|
|
58
|
+
tool_name_fn: Callable[[str, str], str] | None = None,
|
|
59
|
+
security_policy: Any = None,
|
|
60
|
+
permissions: Any = None,
|
|
61
|
+
task_id: str = "",
|
|
62
|
+
) -> None:
|
|
55
63
|
self._stdin = stdin
|
|
56
64
|
self._write_lock = threading.Lock()
|
|
57
65
|
self._mu = threading.Lock()
|
|
@@ -60,6 +68,9 @@ class ACPClient:
|
|
|
60
68
|
self._tool_mu = threading.Lock()
|
|
61
69
|
self._pending_tools: dict[str, _PendingTool] = {}
|
|
62
70
|
self.tool_name_fn = tool_name_fn or default_tool_name
|
|
71
|
+
self.security_policy = security_policy
|
|
72
|
+
self.permissions = permissions
|
|
73
|
+
self.task_id = task_id
|
|
63
74
|
self.session_id = ""
|
|
64
75
|
self.on_message: MessageCB | None = None
|
|
65
76
|
self.on_prompt_done: PromptDoneCB | None = None
|
|
@@ -155,9 +166,9 @@ class ACPClient:
|
|
|
155
166
|
method = str(raw.get("method") or "")
|
|
156
167
|
req_id = raw.get("id")
|
|
157
168
|
if method == "session/request_permission":
|
|
158
|
-
|
|
169
|
+
params = raw.get("params")
|
|
170
|
+
option_id = self._resolve_permission_option(params)
|
|
159
171
|
result = {"outcome": {"outcome": "selected", "optionId": option_id}}
|
|
160
|
-
log.debug("auto-approved permission option_id=%s", option_id)
|
|
161
172
|
elif method == "cursor/ask_question":
|
|
162
173
|
result = {
|
|
163
174
|
"outcome": {
|
|
@@ -194,6 +205,83 @@ class ACPClient:
|
|
|
194
205
|
except OSError as e:
|
|
195
206
|
log.warning("write agent-request response failed: %s", e)
|
|
196
207
|
|
|
208
|
+
def _resolve_permission_option(self, params: Any) -> str:
|
|
209
|
+
from .permission import (
|
|
210
|
+
DEFAULT_PERMISSION_TIMEOUT,
|
|
211
|
+
extract_tool_meta,
|
|
212
|
+
parse_permission_options,
|
|
213
|
+
)
|
|
214
|
+
from .security import (
|
|
215
|
+
extract_permission_paths,
|
|
216
|
+
permission_should_deny,
|
|
217
|
+
select_reject_permission_option_id,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
policy = self.security_policy
|
|
221
|
+
if policy is not None and permission_should_deny(policy, params):
|
|
222
|
+
option_id = select_reject_permission_option_id(params)
|
|
223
|
+
log.info("denied permission outside allowed roots option_id=%s", option_id)
|
|
224
|
+
return option_id
|
|
225
|
+
|
|
226
|
+
permit_all = True if policy is None else bool(getattr(policy, "permit_all", True))
|
|
227
|
+
if permit_all or self.permissions is None:
|
|
228
|
+
option_id = _select_permission_option_id(params)
|
|
229
|
+
log.debug("auto-approved permission option_id=%s", option_id)
|
|
230
|
+
return option_id
|
|
231
|
+
|
|
232
|
+
title, tool_call_id = extract_tool_meta(params)
|
|
233
|
+
options = parse_permission_options(params)
|
|
234
|
+
permission_id, wait_ev = self.permissions.register("")
|
|
235
|
+
paths = extract_permission_paths(params)
|
|
236
|
+
input_payload = {
|
|
237
|
+
"permission_id": permission_id,
|
|
238
|
+
"task_id": self.task_id,
|
|
239
|
+
"options": [o.to_dict() for o in options],
|
|
240
|
+
"paths": paths,
|
|
241
|
+
"tool_call_id": tool_call_id,
|
|
242
|
+
}
|
|
243
|
+
if self.on_message is not None:
|
|
244
|
+
self.on_message(
|
|
245
|
+
{
|
|
246
|
+
"type": "permission-request",
|
|
247
|
+
"tool": title,
|
|
248
|
+
"call_id": permission_id,
|
|
249
|
+
"content": title,
|
|
250
|
+
"input": input_payload,
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
option_id = self.permissions.wait(
|
|
255
|
+
permission_id, wait_ev, DEFAULT_PERMISSION_TIMEOUT
|
|
256
|
+
)
|
|
257
|
+
if not option_id:
|
|
258
|
+
option_id = select_reject_permission_option_id(params)
|
|
259
|
+
log.info(
|
|
260
|
+
"permission request timed out or cancelled; denying permission_id=%s option_id=%s",
|
|
261
|
+
permission_id,
|
|
262
|
+
option_id,
|
|
263
|
+
)
|
|
264
|
+
else:
|
|
265
|
+
log.info(
|
|
266
|
+
"permission resolved by user permission_id=%s option_id=%s",
|
|
267
|
+
permission_id,
|
|
268
|
+
option_id,
|
|
269
|
+
)
|
|
270
|
+
if self.on_message is not None:
|
|
271
|
+
self.on_message(
|
|
272
|
+
{
|
|
273
|
+
"type": "permission-result",
|
|
274
|
+
"tool": title,
|
|
275
|
+
"call_id": permission_id,
|
|
276
|
+
"content": option_id,
|
|
277
|
+
"input": {
|
|
278
|
+
"permission_id": permission_id,
|
|
279
|
+
"option_id": option_id,
|
|
280
|
+
},
|
|
281
|
+
}
|
|
282
|
+
)
|
|
283
|
+
return option_id
|
|
284
|
+
|
|
197
285
|
def _handle_notification(self, raw: dict[str, Any]) -> None:
|
|
198
286
|
method = str(raw.get("method") or "")
|
|
199
287
|
if method not in ("session/update", "session/notification"):
|
|
@@ -414,6 +502,7 @@ def _select_permission_option_id(params: Any) -> str:
|
|
|
414
502
|
return fallback
|
|
415
503
|
|
|
416
504
|
|
|
505
|
+
|
|
417
506
|
def _normalize_update(data: Any) -> tuple[str, Any]:
|
|
418
507
|
if not isinstance(data, dict):
|
|
419
508
|
return "", data
|
|
@@ -23,8 +23,13 @@ from .acp_client import (
|
|
|
23
23
|
kiro_tool_name,
|
|
24
24
|
resolve_resumed_session_id,
|
|
25
25
|
)
|
|
26
|
-
from .acp_errors import
|
|
26
|
+
from .acp_errors import (
|
|
27
|
+
ACPProviderErrorSniffer,
|
|
28
|
+
enrich_kiro_acp_error,
|
|
29
|
+
promote_acp_result_on_provider_error,
|
|
30
|
+
)
|
|
27
31
|
from .procutil import kill_process_tree, popen_kwargs_for_killable_group
|
|
32
|
+
from .security import SecurityPolicy, build_cursor_acp_global_flags, default_security_policy
|
|
28
33
|
|
|
29
34
|
log = logging.getLogger("aicortexpy.acp")
|
|
30
35
|
|
|
@@ -72,15 +77,16 @@ def kiro_acp_supported(executable: str) -> bool:
|
|
|
72
77
|
|
|
73
78
|
|
|
74
79
|
def _acp_help_ok(executable: str, *, extra_before: list[str]) -> bool:
|
|
80
|
+
from .procutil import run_no_window
|
|
81
|
+
|
|
75
82
|
path = shutil.which(executable) or executable
|
|
76
83
|
args = [path, *extra_before]
|
|
77
84
|
try:
|
|
78
|
-
proc =
|
|
85
|
+
proc = run_no_window(
|
|
79
86
|
args,
|
|
80
87
|
capture_output=True,
|
|
81
88
|
timeout=5,
|
|
82
89
|
check=False,
|
|
83
|
-
**_hide_window(),
|
|
84
90
|
)
|
|
85
91
|
return proc.returncode == 0
|
|
86
92
|
except (OSError, subprocess.TimeoutExpired):
|
|
@@ -88,11 +94,9 @@ def _acp_help_ok(executable: str, *, extra_before: list[str]) -> bool:
|
|
|
88
94
|
|
|
89
95
|
|
|
90
96
|
def _hide_window() -> dict:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
startup.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
95
|
-
return {"startupinfo": startup}
|
|
97
|
+
from .procutil import popen_kwargs_hidden_console
|
|
98
|
+
|
|
99
|
+
return popen_kwargs_hidden_console()
|
|
96
100
|
|
|
97
101
|
|
|
98
102
|
def choose_cursor_invocation(executable: str, args: list[str]) -> tuple[str, list[str]]:
|
|
@@ -203,6 +207,8 @@ class _BaseACPConn:
|
|
|
203
207
|
on_message: MessageCB | None = None,
|
|
204
208
|
idle_watchdog: float = 0,
|
|
205
209
|
cancel_event: threading.Event | None = None,
|
|
210
|
+
permissions: Any = None,
|
|
211
|
+
task_id: str = "",
|
|
206
212
|
) -> ACPPromptResult:
|
|
207
213
|
if self._closed or not self._client or not self._session_id:
|
|
208
214
|
return ACPPromptResult(
|
|
@@ -211,6 +217,10 @@ class _BaseACPConn:
|
|
|
211
217
|
error=f"{self.provider} acp connection closed",
|
|
212
218
|
)
|
|
213
219
|
client = self._client
|
|
220
|
+
if permissions is not None:
|
|
221
|
+
client.permissions = permissions
|
|
222
|
+
if task_id:
|
|
223
|
+
client.task_id = task_id
|
|
214
224
|
output_parts: list[str] = []
|
|
215
225
|
messages: list[dict[str, Any]] = []
|
|
216
226
|
prompt_done = threading.Event()
|
|
@@ -337,6 +347,8 @@ class _BaseACPConn:
|
|
|
337
347
|
status, error = promote_acp_result_on_provider_error(
|
|
338
348
|
status, error, output, self._sniffer
|
|
339
349
|
)
|
|
350
|
+
if self.provider == "kiro" and status == "failed" and error:
|
|
351
|
+
error = enrich_kiro_acp_error(error)
|
|
340
352
|
|
|
341
353
|
usage = None
|
|
342
354
|
with client._usage_lock:
|
|
@@ -368,6 +380,7 @@ class _BaseACPConn:
|
|
|
368
380
|
env: dict[str, str] | None,
|
|
369
381
|
*,
|
|
370
382
|
tool_name_fn: Callable[[str, str], str],
|
|
383
|
+
security_policy: SecurityPolicy | None = None,
|
|
371
384
|
) -> None:
|
|
372
385
|
kwargs = popen_kwargs_for_killable_group()
|
|
373
386
|
kwargs.update(_hide_window())
|
|
@@ -393,7 +406,11 @@ class _BaseACPConn:
|
|
|
393
406
|
write_through=True,
|
|
394
407
|
line_buffering=True,
|
|
395
408
|
)
|
|
396
|
-
self._client = ACPClient(
|
|
409
|
+
self._client = ACPClient(
|
|
410
|
+
self._stdin_text,
|
|
411
|
+
tool_name_fn=tool_name_fn,
|
|
412
|
+
security_policy=security_policy or default_security_policy(),
|
|
413
|
+
)
|
|
397
414
|
threading.Thread(target=self._read_stdout, name=f"{self.provider}-acp-out", daemon=True).start()
|
|
398
415
|
threading.Thread(target=self._read_stderr, name=f"{self.provider}-acp-err", daemon=True).start()
|
|
399
416
|
log.info("%s acp started pid=%s cwd=%s", self.provider, self._proc.pid, cwd)
|
|
@@ -557,16 +574,26 @@ class CursorACPConn(_BaseACPConn):
|
|
|
557
574
|
timeout: float = 0,
|
|
558
575
|
env: dict[str, str] | None = None,
|
|
559
576
|
allow_resume: bool = False,
|
|
577
|
+
security_policy: SecurityPolicy | None = None,
|
|
560
578
|
) -> CursorACPConn:
|
|
561
579
|
path = shutil.which(executable) or executable
|
|
580
|
+
policy = security_policy or default_security_policy()
|
|
562
581
|
blocked = {"acp", "--yolo", "--force", "--sandbox", "--trust"}
|
|
563
582
|
filtered = _filter_blocked(list(custom_args or []), blocked, {"--sandbox"})
|
|
564
|
-
acp_args = [
|
|
583
|
+
acp_args = [*build_cursor_acp_global_flags(policy), "acp", *filtered]
|
|
565
584
|
argv0, cmd_args = choose_cursor_invocation(path, acp_args)
|
|
566
585
|
argv = [argv0, *cmd_args]
|
|
567
586
|
conn = cls()
|
|
568
587
|
work = cwd or str(Path.cwd())
|
|
569
|
-
|
|
588
|
+
if policy.allowed_roots_enabled and work and work not in policy.allowed_roots:
|
|
589
|
+
policy.allowed_roots = [*policy.allowed_roots, work]
|
|
590
|
+
conn._start_process(
|
|
591
|
+
argv,
|
|
592
|
+
work,
|
|
593
|
+
env,
|
|
594
|
+
tool_name_fn=hermes_tool_name_from_title,
|
|
595
|
+
security_policy=policy,
|
|
596
|
+
)
|
|
570
597
|
hs = HANDSHAKE_TIMEOUT
|
|
571
598
|
if timeout > 0:
|
|
572
599
|
hs = min(hs, timeout)
|
|
@@ -600,16 +627,29 @@ class KiroACPConn(_BaseACPConn):
|
|
|
600
627
|
custom_args: list[str] | None = None,
|
|
601
628
|
timeout: float = 0,
|
|
602
629
|
env: dict[str, str] | None = None,
|
|
630
|
+
security_policy: SecurityPolicy | None = None,
|
|
603
631
|
) -> KiroACPConn:
|
|
604
632
|
path = shutil.which(executable) or executable
|
|
633
|
+
policy = security_policy or default_security_policy()
|
|
605
634
|
blocked = {"acp", "-a", "--trust-all-tools", "--trust-tools"}
|
|
606
635
|
filtered = _filter_blocked(
|
|
607
636
|
list(custom_args or []), blocked, {"--trust-tools"}
|
|
608
637
|
)
|
|
609
|
-
argv = [path, "acp"
|
|
638
|
+
argv = [path, "acp"]
|
|
639
|
+
if policy.permit_all:
|
|
640
|
+
argv.append("--trust-all-tools")
|
|
641
|
+
argv.extend(filtered)
|
|
610
642
|
conn = cls()
|
|
611
643
|
work = cwd or str(Path.cwd())
|
|
612
|
-
|
|
644
|
+
if policy.allowed_roots_enabled and work and work not in policy.allowed_roots:
|
|
645
|
+
policy.allowed_roots = [*policy.allowed_roots, work]
|
|
646
|
+
conn._start_process(
|
|
647
|
+
argv,
|
|
648
|
+
work,
|
|
649
|
+
env,
|
|
650
|
+
tool_name_fn=kiro_tool_name,
|
|
651
|
+
security_policy=policy,
|
|
652
|
+
)
|
|
613
653
|
hs = timeout if timeout > 0 else HANDSHAKE_TIMEOUT
|
|
614
654
|
try:
|
|
615
655
|
conn._handshake(
|
|
@@ -754,6 +794,7 @@ def open_acp_conn(
|
|
|
754
794
|
custom_args: list[str] | None = None,
|
|
755
795
|
timeout: float = 0,
|
|
756
796
|
env: dict[str, str] | None = None,
|
|
797
|
+
security_policy: SecurityPolicy | None = None,
|
|
757
798
|
) -> _BaseACPConn:
|
|
758
799
|
if provider == "cursor":
|
|
759
800
|
return CursorACPConn.open(
|
|
@@ -765,6 +806,7 @@ def open_acp_conn(
|
|
|
765
806
|
timeout=timeout,
|
|
766
807
|
env=env,
|
|
767
808
|
allow_resume=False,
|
|
809
|
+
security_policy=security_policy,
|
|
768
810
|
)
|
|
769
811
|
if provider == "kiro":
|
|
770
812
|
return KiroACPConn.open(
|
|
@@ -775,6 +817,7 @@ def open_acp_conn(
|
|
|
775
817
|
custom_args=custom_args,
|
|
776
818
|
timeout=timeout,
|
|
777
819
|
env=env,
|
|
820
|
+
security_policy=security_policy,
|
|
778
821
|
)
|
|
779
822
|
if provider == "hermes":
|
|
780
823
|
return HermesACPConn.open(
|
|
@@ -824,6 +867,9 @@ def run_acp_oneshot(
|
|
|
824
867
|
on_message: MessageCB | None = None,
|
|
825
868
|
cancel_event: threading.Event | None = None,
|
|
826
869
|
idle_watchdog: float = 0,
|
|
870
|
+
security_policy: SecurityPolicy | None = None,
|
|
871
|
+
permissions: Any = None,
|
|
872
|
+
task_id: str = "",
|
|
827
873
|
) -> ACPPromptResult:
|
|
828
874
|
"""One-shot ACP turn (open → prompt → close). Used for non-chat kiro/hermes/kimi/dsh."""
|
|
829
875
|
t = timeout if timeout > 0 else PROMPT_TIMEOUT
|
|
@@ -837,6 +883,7 @@ def run_acp_oneshot(
|
|
|
837
883
|
custom_args=custom_args,
|
|
838
884
|
timeout=t,
|
|
839
885
|
env=env,
|
|
886
|
+
security_policy=security_policy,
|
|
840
887
|
)
|
|
841
888
|
except Exception as e:
|
|
842
889
|
return ACPPromptResult(status="failed", output="", error=f"{provider} acp open failed: {e}")
|
|
@@ -848,6 +895,8 @@ def run_acp_oneshot(
|
|
|
848
895
|
on_message=on_message,
|
|
849
896
|
idle_watchdog=idle_watchdog,
|
|
850
897
|
cancel_event=cancel_event,
|
|
898
|
+
permissions=permissions,
|
|
899
|
+
task_id=task_id,
|
|
851
900
|
)
|
|
852
901
|
finally:
|
|
853
902
|
conn.close()
|
|
@@ -19,6 +19,10 @@ _ACP_TERMINAL_ERROR_RE = re.compile(
|
|
|
19
19
|
_ACP_AGENT_OUTPUT_TERMINAL_RE = re.compile(r"API call failed after \d+ retr(?:y|ies)")
|
|
20
20
|
_ACP_MAX_ERROR_LINES = 8
|
|
21
21
|
|
|
22
|
+
_KIRO_EXPLICIT_AUTH_RE = re.compile(r"no token|not logged in", re.IGNORECASE)
|
|
23
|
+
_KIRO_DISPATCH_RE = re.compile(r"dispatch failure", re.IGNORECASE)
|
|
24
|
+
_KIRO_LOGIN_HINT_RE = re.compile(r"kiro-cli login", re.IGNORECASE)
|
|
25
|
+
|
|
22
26
|
|
|
23
27
|
class ACPProviderErrorSniffer:
|
|
24
28
|
def __init__(self, provider: str) -> None:
|
|
@@ -105,3 +109,22 @@ def promote_acp_result_on_provider_error(
|
|
|
105
109
|
if msg:
|
|
106
110
|
return "failed", msg
|
|
107
111
|
return final_status, final_error
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def enrich_kiro_acp_error(error: str) -> str:
|
|
115
|
+
"""Append login/retry hints from Kiro ACP error text alone (no extra CLI probes)."""
|
|
116
|
+
if not error or _KIRO_LOGIN_HINT_RE.search(error):
|
|
117
|
+
return error
|
|
118
|
+
if _KIRO_EXPLICIT_AUTH_RE.search(error):
|
|
119
|
+
return (
|
|
120
|
+
"kiro-cli is not logged in. Run `kiro-cli login`, then retry. "
|
|
121
|
+
f"({error})"
|
|
122
|
+
)
|
|
123
|
+
if _KIRO_DISPATCH_RE.search(error):
|
|
124
|
+
# Kiro often surfaces logout/network problems only as "dispatch failure".
|
|
125
|
+
return (
|
|
126
|
+
f"{error} "
|
|
127
|
+
"(common causes: kiro-cli not logged in - run `kiro-cli login`; "
|
|
128
|
+
"or transient AWS Q network error - retry)"
|
|
129
|
+
)
|
|
130
|
+
return error
|
|
@@ -51,7 +51,11 @@ def _aicortex_bin_dir() -> str | None:
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
def ensure_cli_shim_dir(state_dir: Path) -> Path | None:
|
|
54
|
-
"""Write a tiny aicortex shim so agents resolve to python -m
|
|
54
|
+
"""Write a tiny aicortex shim so agents resolve to `python -m aicortex`.
|
|
55
|
+
|
|
56
|
+
Prefer .cmd on Windows so PATH resolution does not depend on aicortex.exe
|
|
57
|
+
(pip console_scripts launcher / Go installer binary detection).
|
|
58
|
+
"""
|
|
55
59
|
shim_dir = state_dir / "py-cli-shim"
|
|
56
60
|
try:
|
|
57
61
|
shim_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -63,12 +67,19 @@ def ensure_cli_shim_dir(state_dir: Path) -> Path | None:
|
|
|
63
67
|
if sys.platform == "win32":
|
|
64
68
|
cmd = shim_dir / "aicortex.cmd"
|
|
65
69
|
cmd.write_text(
|
|
66
|
-
f'@echo off\r\n"{py}" -m
|
|
70
|
+
f'@echo off\r\n"{py}" -m aicortex %*\r\n',
|
|
67
71
|
encoding="utf-8",
|
|
68
72
|
)
|
|
73
|
+
# Drop a stale pip console_scripts aicortex.exe in this shim dir if any.
|
|
74
|
+
stale = shim_dir / "aicortex.exe"
|
|
75
|
+
if stale.is_file():
|
|
76
|
+
try:
|
|
77
|
+
stale.unlink()
|
|
78
|
+
except OSError:
|
|
79
|
+
pass
|
|
69
80
|
else:
|
|
70
81
|
sh = shim_dir / "aicortex"
|
|
71
|
-
sh.write_text(f'#!/bin/sh\nexec "{py}" -m
|
|
82
|
+
sh.write_text(f'#!/bin/sh\nexec "{py}" -m aicortex "$@"\n', encoding="utf-8")
|
|
72
83
|
try:
|
|
73
84
|
sh.chmod(0o755)
|
|
74
85
|
except OSError:
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Pinned app template / instance workdirs (port of Go daemon/app_workdir.go)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .client import Task
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger("aicortexpy.app_workdir")
|
|
12
|
+
|
|
13
|
+
_DEFAULT_SEED = b"<!DOCTYPE html><html><body><h1>App</h1></body></html>\n"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def app_uses_pinned_workdir(task: Task) -> bool:
|
|
17
|
+
return bool(task.app_id.strip()) and task.app_mode in ("edit", "run")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def prepare_app_workdir(workspaces_root: Path, task: Task) -> Path:
|
|
21
|
+
"""Create/reuse app template or instance dir; seed index.html from task.app_html."""
|
|
22
|
+
ws = (task.workspace_id or "").strip()
|
|
23
|
+
app_id = task.app_id.strip()
|
|
24
|
+
if not ws or not app_id:
|
|
25
|
+
raise ValueError("missing app workdir identity")
|
|
26
|
+
|
|
27
|
+
base = workspaces_root / ws / "apps" / app_id
|
|
28
|
+
mode = task.app_mode
|
|
29
|
+
if mode == "edit":
|
|
30
|
+
directory = base / "template"
|
|
31
|
+
elif mode == "run":
|
|
32
|
+
if task.app_workdir_mode == "shared":
|
|
33
|
+
directory = base / "instances" / "shared"
|
|
34
|
+
else:
|
|
35
|
+
user_id = task.app_user_id.strip() or "anonymous"
|
|
36
|
+
directory = base / "instances" / "users" / user_id
|
|
37
|
+
else:
|
|
38
|
+
raise ValueError(f"unknown app mode {mode!r}")
|
|
39
|
+
|
|
40
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
index_path = directory / "index.html"
|
|
42
|
+
seed_text = task.app_html.strip()
|
|
43
|
+
seed = seed_text.encode("utf-8") if seed_text else _DEFAULT_SEED
|
|
44
|
+
need_write = (not index_path.is_file()) or index_path.stat().st_size == 0
|
|
45
|
+
if not need_write and seed_text:
|
|
46
|
+
# Server AppHTML is authoritative at task start; refresh stale disk
|
|
47
|
+
# copies (e.g. first-run default seed) so complete_task won't clobber DB.
|
|
48
|
+
try:
|
|
49
|
+
existing = index_path.read_text(encoding="utf-8")
|
|
50
|
+
except OSError:
|
|
51
|
+
existing = ""
|
|
52
|
+
if existing.strip() != seed_text:
|
|
53
|
+
need_write = True
|
|
54
|
+
if need_write:
|
|
55
|
+
index_path.write_bytes(seed)
|
|
56
|
+
log.info("seeded app index.html at %s mode=%s", index_path, mode)
|
|
57
|
+
return directory
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def read_app_index_html(work_dir: str | Path) -> str:
|
|
61
|
+
path = Path(work_dir) / "index.html"
|
|
62
|
+
return path.read_text(encoding="utf-8")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def is_app_workdir_path(path: Path) -> bool:
|
|
66
|
+
"""True when path sits under .../apps/<id>/(template|instances)/..."""
|
|
67
|
+
parts = path.resolve().parts
|
|
68
|
+
try:
|
|
69
|
+
i = parts.index("apps")
|
|
70
|
+
except ValueError:
|
|
71
|
+
return False
|
|
72
|
+
return i + 1 < len(parts)
|
|
@@ -110,10 +110,14 @@ class ArtifactBrowser:
|
|
|
110
110
|
st = child.stat()
|
|
111
111
|
except OSError:
|
|
112
112
|
continue
|
|
113
|
+
child_rel = (
|
|
114
|
+
f"{rel.rstrip('/')}/{child.name}".lstrip("/") if rel else child.name
|
|
115
|
+
)
|
|
116
|
+
# Protocol (Go ArtifactListEntry) expects `path`, not `rel_path`.
|
|
113
117
|
entries.append(
|
|
114
118
|
{
|
|
115
119
|
"name": child.name,
|
|
116
|
-
"
|
|
120
|
+
"path": child_rel,
|
|
117
121
|
"is_dir": child.is_dir(),
|
|
118
122
|
"size": st.st_size if child.is_file() else 0,
|
|
119
123
|
"mtime": int(st.st_mtime),
|
|
@@ -64,4 +64,10 @@ def report_issue_artifacts(
|
|
|
64
64
|
try:
|
|
65
65
|
report(items)
|
|
66
66
|
except Exception as e:
|
|
67
|
+
# Issue preview is feature-flagged off by default (404 "not found").
|
|
68
|
+
# Chat/app workdirs often have index.html that is not an issue artifact.
|
|
69
|
+
status = getattr(e, "status", None)
|
|
70
|
+
if status in (400, 404):
|
|
71
|
+
log.debug("report artifacts skipped: %s", e)
|
|
72
|
+
return
|
|
67
73
|
log.warning("report artifacts failed: %s", e)
|