aicortexpy 0.2.0__tar.gz → 0.2.2__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.
Files changed (100) hide show
  1. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/PKG-INFO +10 -7
  2. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/README.md +9 -6
  3. aicortexpy-0.2.2/aicortex/__init__.py +9 -0
  4. aicortexpy-0.2.2/aicortex/__main__.py +5 -0
  5. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/__init__.py +1 -1
  6. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/acp_client.py +112 -6
  7. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/acp_conn.py +51 -7
  8. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/agent_env.py +14 -3
  9. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/attachments.py +3 -1
  10. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/chat_acp_pool.py +10 -0
  11. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/cli.py +35 -3
  12. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/client.py +9 -2
  13. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/commands/agent_cmd.py +37 -0
  14. aicortexpy-0.2.2/aicortexpy/commands/auth_cmd.py +167 -0
  15. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/commands/autopilot_cmd.py +86 -2
  16. aicortexpy-0.2.2/aicortexpy/commands/daemon_cmd.py +228 -0
  17. aicortexpy-0.2.2/aicortexpy/commands/setup_cmd.py +159 -0
  18. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/commands/skill_cmd.py +28 -6
  19. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/commands/squad_cmd.py +22 -1
  20. aicortexpy-0.2.2/aicortexpy/commands/update_cmd.py +46 -0
  21. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/config.py +82 -8
  22. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/daemon.py +20 -1
  23. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/id_resolve.py +17 -0
  24. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/models.py +8 -0
  25. aicortexpy-0.2.2/aicortexpy/oauth_login.py +183 -0
  26. aicortexpy-0.2.2/aicortexpy/output.py +77 -0
  27. aicortexpy-0.2.2/aicortexpy/permission.py +107 -0
  28. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/platform_client.py +8 -1
  29. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/process.py +21 -14
  30. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/runner.py +118 -6
  31. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/sandbox.py +2 -2
  32. aicortexpy-0.2.2/aicortexpy/security.py +168 -0
  33. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/wakeup.py +15 -0
  34. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy.egg-info/PKG-INFO +10 -7
  35. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy.egg-info/SOURCES.txt +10 -1
  36. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy.egg-info/entry_points.txt +0 -1
  37. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy.egg-info/top_level.txt +1 -0
  38. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/pyproject.toml +9 -4
  39. aicortexpy-0.2.2/scripts/aicortex +3 -0
  40. aicortexpy-0.2.2/scripts/aicortex.cmd +3 -0
  41. aicortexpy-0.2.0/aicortexpy/commands/auth_cmd.py +0 -87
  42. aicortexpy-0.2.0/aicortexpy/commands/daemon_cmd.py +0 -97
  43. aicortexpy-0.2.0/aicortexpy/output.py +0 -41
  44. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/MANIFEST.in +0 -0
  45. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/__main__.py +0 -0
  46. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/acp_errors.py +0 -0
  47. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/artifact_ws.py +0 -0
  48. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/artifacts.py +0 -0
  49. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/auto_commit.py +0 -0
  50. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/capabilities.py +0 -0
  51. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/cli_context.py +0 -0
  52. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/codex_config.py +0 -0
  53. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/commands/__init__.py +0 -0
  54. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/commands/attachment_cmd.py +0 -0
  55. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/commands/config_cmd.py +0 -0
  56. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/commands/graph_cmd.py +0 -0
  57. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/commands/issue_cmd.py +0 -0
  58. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/commands/label_cmd.py +0 -0
  59. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/commands/meeting_cmd.py +0 -0
  60. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/commands/memory_cmd.py +0 -0
  61. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/commands/project_cmd.py +0 -0
  62. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/commands/repo_cmd.py +0 -0
  63. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/commands/runtime_cmd.py +0 -0
  64. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/commands/version_cmd.py +0 -0
  65. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/commands/workspace_cmd.py +0 -0
  66. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/data/default_skills/agent-memory/SKILL.md +0 -0
  67. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/data/default_skills/aicorkit-atlassian/SKILL.md +0 -0
  68. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/data/default_skills/aicorkit-graph/SKILL.md +0 -0
  69. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/data/default_skills/aicorkit-outlook/SKILL.md +0 -0
  70. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/data/default_skills/aicorkit-wxmsg/SKILL.md +0 -0
  71. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/data/default_skills/aicortex-basics/SKILL.md +0 -0
  72. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/data/default_skills/aicortex-bridge-cdp/SKILL.md +0 -0
  73. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/data/default_skills/aicortex-cli-ops/SKILL.md +0 -0
  74. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/data/default_skills/aicortex-graph/SKILL.md +0 -0
  75. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/data/default_skills/graphify/SKILL.md +0 -0
  76. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/data/default_skills/question-form/SKILL.md +0 -0
  77. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/data/default_skills/test-report-generator/SKILL.md +0 -0
  78. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/data/default_skills/test-report-generator/scripts/generate_cases.py +0 -0
  79. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/data/default_skills/test-report-generator/scripts/generate_report.py +0 -0
  80. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/design_seed.py +0 -0
  81. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/diskusage.py +0 -0
  82. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/execenv.py +0 -0
  83. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/gc.py +0 -0
  84. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/handoff_bundle.py +0 -0
  85. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/health.py +0 -0
  86. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/local_skills.py +0 -0
  87. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/openclaw_config.py +0 -0
  88. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/pinned_workdir.py +0 -0
  89. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/platform_info.py +0 -0
  90. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/poisoned.py +0 -0
  91. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/procutil.py +0 -0
  92. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/prompt.py +0 -0
  93. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/repo.py +0 -0
  94. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/runtime_config.py +0 -0
  95. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/stream.py +0 -0
  96. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/terminal.py +0 -0
  97. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy/tunnel.py +0 -0
  98. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy.egg-info/dependency_links.txt +0 -0
  99. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/aicortexpy.egg-info/requires.txt +0 -0
  100. {aicortexpy-0.2.0 → aicortexpy-0.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: aicortexpy
3
- Version: 0.2.0
3
+ Version: 0.2.2
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
- 也可用 `python -m aicortexpy`(与 `aicortex` / `aicortexpy` 入口等价)。
29
+ 等价入口:`python -m aicortex`(推荐)/ `python -m aicortexpy` / `aicortexpy`。
30
30
 
31
31
  | | |
32
32
  |--|--|
33
- | PyPI / 模块 | `aicortexpy` |
34
- | Console scripts | `aicortex`、`aicortexpy`(`aicortex` 刻意同名,供 Agent 提示词零改动) |
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-daemon
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-daemon
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
- 尚未:浏览器 OAuth login、`update` 二进制自更新(请用 `pip install -U aicortexpy`)。
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
- 也可用 `python -m aicortexpy`(与 `aicortex` / `aicortexpy` 入口等价)。
12
+ 等价入口:`python -m aicortex`(推荐)/ `python -m aicortexpy` / `aicortexpy`。
13
13
 
14
14
  | | |
15
15
  |--|--|
16
- | PyPI / 模块 | `aicortexpy` |
17
- | Console scripts | `aicortex`、`aicortexpy`(`aicortex` 刻意同名,供 Agent 提示词零改动) |
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-daemon
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-daemon
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
- 尚未:浏览器 OAuth login、`update` 二进制自更新(请用 `pip install -U aicortexpy`)。
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__"]
@@ -0,0 +1,5 @@
1
+ """Allow `python -m aicortex` (same as `python -m aicortexpy`)."""
2
+
3
+ from aicortexpy.cli import main
4
+
5
+ raise SystemExit(main())
@@ -1,3 +1,3 @@
1
1
  """AICortex Python CLI + local agent runtime daemon."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.2"
@@ -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__(self, stdin: TextIO, *, tool_name_fn: Callable[[str, str], str] | None = None) -> None:
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
- option_id = _select_permission_option_id(raw.get("params"))
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"):
@@ -236,7 +324,9 @@ class ACPClient:
236
324
  if self.on_message:
237
325
  self.on_message(
238
326
  {
239
- "type": "tool-use",
327
+ # Server/UI wire format uses underscores (Go daemon maps
328
+ # internal "tool-use" → "tool_use" before ReportTaskMessages).
329
+ "type": "tool_use",
240
330
  "tool": tool_name,
241
331
  "call_id": call_id,
242
332
  "input": raw_input,
@@ -271,8 +361,18 @@ class ACPClient:
271
361
  output = str(data.get("rawOutput") or data.get("output") or "")
272
362
  if not output:
273
363
  output = _extract_tool_call_text(content)
364
+ tool_name = ""
365
+ if pending is not None:
366
+ tool_name = pending.tool_name or ""
274
367
  if self.on_message:
275
- self.on_message({"type": "tool-result", "call_id": call_id, "output": output})
368
+ self.on_message(
369
+ {
370
+ "type": "tool_result",
371
+ "tool": tool_name or "tool",
372
+ "call_id": call_id,
373
+ "output": output,
374
+ }
375
+ )
276
376
 
277
377
  def _emit_deferred_tool_use(self, pending: _PendingTool | None, call_id: str, data: dict) -> None:
278
378
  if pending is not None and pending.emitted:
@@ -300,7 +400,12 @@ class ACPClient:
300
400
  inp = {}
301
401
  if self.on_message:
302
402
  self.on_message(
303
- {"type": "tool-use", "tool": tool_name, "call_id": call_id, "input": inp}
403
+ {
404
+ "type": "tool_use",
405
+ "tool": tool_name,
406
+ "call_id": call_id,
407
+ "input": inp,
408
+ }
304
409
  )
305
410
 
306
411
  def _track_tool(self, call_id: str, p: _PendingTool) -> None:
@@ -397,6 +502,7 @@ def _select_permission_option_id(params: Any) -> str:
397
502
  return fallback
398
503
 
399
504
 
505
+
400
506
  def _normalize_update(data: Any) -> tuple[str, Any]:
401
507
  if not isinstance(data, dict):
402
508
  return "", data
@@ -25,6 +25,7 @@ from .acp_client import (
25
25
  )
26
26
  from .acp_errors import ACPProviderErrorSniffer, promote_acp_result_on_provider_error
27
27
  from .procutil import kill_process_tree, popen_kwargs_for_killable_group
28
+ from .security import SecurityPolicy, build_cursor_acp_global_flags, default_security_policy
28
29
 
29
30
  log = logging.getLogger("aicortexpy.acp")
30
31
 
@@ -203,6 +204,8 @@ class _BaseACPConn:
203
204
  on_message: MessageCB | None = None,
204
205
  idle_watchdog: float = 0,
205
206
  cancel_event: threading.Event | None = None,
207
+ permissions: Any = None,
208
+ task_id: str = "",
206
209
  ) -> ACPPromptResult:
207
210
  if self._closed or not self._client or not self._session_id:
208
211
  return ACPPromptResult(
@@ -211,6 +214,10 @@ class _BaseACPConn:
211
214
  error=f"{self.provider} acp connection closed",
212
215
  )
213
216
  client = self._client
217
+ if permissions is not None:
218
+ client.permissions = permissions
219
+ if task_id:
220
+ client.task_id = task_id
214
221
  output_parts: list[str] = []
215
222
  messages: list[dict[str, Any]] = []
216
223
  prompt_done = threading.Event()
@@ -226,9 +233,9 @@ class _BaseACPConn:
226
233
  typ = msg.get("type")
227
234
  if typ == "text":
228
235
  output_parts.append(str(msg.get("content") or ""))
229
- elif typ == "tool-use":
236
+ elif typ in ("tool_use", "tool-use"):
230
237
  in_flight_tools["n"] += 1
231
- elif typ == "tool-result":
238
+ elif typ in ("tool_result", "tool-result"):
232
239
  in_flight_tools["n"] = max(0, in_flight_tools["n"] - 1)
233
240
  messages.append(msg)
234
241
  if on_message:
@@ -368,6 +375,7 @@ class _BaseACPConn:
368
375
  env: dict[str, str] | None,
369
376
  *,
370
377
  tool_name_fn: Callable[[str, str], str],
378
+ security_policy: SecurityPolicy | None = None,
371
379
  ) -> None:
372
380
  kwargs = popen_kwargs_for_killable_group()
373
381
  kwargs.update(_hide_window())
@@ -393,7 +401,11 @@ class _BaseACPConn:
393
401
  write_through=True,
394
402
  line_buffering=True,
395
403
  )
396
- self._client = ACPClient(self._stdin_text, tool_name_fn=tool_name_fn)
404
+ self._client = ACPClient(
405
+ self._stdin_text,
406
+ tool_name_fn=tool_name_fn,
407
+ security_policy=security_policy or default_security_policy(),
408
+ )
397
409
  threading.Thread(target=self._read_stdout, name=f"{self.provider}-acp-out", daemon=True).start()
398
410
  threading.Thread(target=self._read_stderr, name=f"{self.provider}-acp-err", daemon=True).start()
399
411
  log.info("%s acp started pid=%s cwd=%s", self.provider, self._proc.pid, cwd)
@@ -557,16 +569,26 @@ class CursorACPConn(_BaseACPConn):
557
569
  timeout: float = 0,
558
570
  env: dict[str, str] | None = None,
559
571
  allow_resume: bool = False,
572
+ security_policy: SecurityPolicy | None = None,
560
573
  ) -> CursorACPConn:
561
574
  path = shutil.which(executable) or executable
575
+ policy = security_policy or default_security_policy()
562
576
  blocked = {"acp", "--yolo", "--force", "--sandbox", "--trust"}
563
577
  filtered = _filter_blocked(list(custom_args or []), blocked, {"--sandbox"})
564
- acp_args = ["--yolo", "--trust", "--sandbox", "disabled", "acp", *filtered]
578
+ acp_args = [*build_cursor_acp_global_flags(policy), "acp", *filtered]
565
579
  argv0, cmd_args = choose_cursor_invocation(path, acp_args)
566
580
  argv = [argv0, *cmd_args]
567
581
  conn = cls()
568
582
  work = cwd or str(Path.cwd())
569
- conn._start_process(argv, work, env, tool_name_fn=hermes_tool_name_from_title)
583
+ if policy.allowed_roots_enabled and work and work not in policy.allowed_roots:
584
+ policy.allowed_roots = [*policy.allowed_roots, work]
585
+ conn._start_process(
586
+ argv,
587
+ work,
588
+ env,
589
+ tool_name_fn=hermes_tool_name_from_title,
590
+ security_policy=policy,
591
+ )
570
592
  hs = HANDSHAKE_TIMEOUT
571
593
  if timeout > 0:
572
594
  hs = min(hs, timeout)
@@ -600,16 +622,29 @@ class KiroACPConn(_BaseACPConn):
600
622
  custom_args: list[str] | None = None,
601
623
  timeout: float = 0,
602
624
  env: dict[str, str] | None = None,
625
+ security_policy: SecurityPolicy | None = None,
603
626
  ) -> KiroACPConn:
604
627
  path = shutil.which(executable) or executable
628
+ policy = security_policy or default_security_policy()
605
629
  blocked = {"acp", "-a", "--trust-all-tools", "--trust-tools"}
606
630
  filtered = _filter_blocked(
607
631
  list(custom_args or []), blocked, {"--trust-tools"}
608
632
  )
609
- argv = [path, "acp", "--trust-all-tools", *filtered]
633
+ argv = [path, "acp"]
634
+ if policy.permit_all:
635
+ argv.append("--trust-all-tools")
636
+ argv.extend(filtered)
610
637
  conn = cls()
611
638
  work = cwd or str(Path.cwd())
612
- conn._start_process(argv, work, env, tool_name_fn=kiro_tool_name)
639
+ if policy.allowed_roots_enabled and work and work not in policy.allowed_roots:
640
+ policy.allowed_roots = [*policy.allowed_roots, work]
641
+ conn._start_process(
642
+ argv,
643
+ work,
644
+ env,
645
+ tool_name_fn=kiro_tool_name,
646
+ security_policy=policy,
647
+ )
613
648
  hs = timeout if timeout > 0 else HANDSHAKE_TIMEOUT
614
649
  try:
615
650
  conn._handshake(
@@ -754,6 +789,7 @@ def open_acp_conn(
754
789
  custom_args: list[str] | None = None,
755
790
  timeout: float = 0,
756
791
  env: dict[str, str] | None = None,
792
+ security_policy: SecurityPolicy | None = None,
757
793
  ) -> _BaseACPConn:
758
794
  if provider == "cursor":
759
795
  return CursorACPConn.open(
@@ -765,6 +801,7 @@ def open_acp_conn(
765
801
  timeout=timeout,
766
802
  env=env,
767
803
  allow_resume=False,
804
+ security_policy=security_policy,
768
805
  )
769
806
  if provider == "kiro":
770
807
  return KiroACPConn.open(
@@ -775,6 +812,7 @@ def open_acp_conn(
775
812
  custom_args=custom_args,
776
813
  timeout=timeout,
777
814
  env=env,
815
+ security_policy=security_policy,
778
816
  )
779
817
  if provider == "hermes":
780
818
  return HermesACPConn.open(
@@ -824,6 +862,9 @@ def run_acp_oneshot(
824
862
  on_message: MessageCB | None = None,
825
863
  cancel_event: threading.Event | None = None,
826
864
  idle_watchdog: float = 0,
865
+ security_policy: SecurityPolicy | None = None,
866
+ permissions: Any = None,
867
+ task_id: str = "",
827
868
  ) -> ACPPromptResult:
828
869
  """One-shot ACP turn (open → prompt → close). Used for non-chat kiro/hermes/kimi/dsh."""
829
870
  t = timeout if timeout > 0 else PROMPT_TIMEOUT
@@ -837,6 +878,7 @@ def run_acp_oneshot(
837
878
  custom_args=custom_args,
838
879
  timeout=t,
839
880
  env=env,
881
+ security_policy=security_policy,
840
882
  )
841
883
  except Exception as e:
842
884
  return ACPPromptResult(status="failed", output="", error=f"{provider} acp open failed: {e}")
@@ -848,6 +890,8 @@ def run_acp_oneshot(
848
890
  on_message=on_message,
849
891
  idle_watchdog=idle_watchdog,
850
892
  cancel_event=cancel_event,
893
+ permissions=permissions,
894
+ task_id=task_id,
851
895
  )
852
896
  finally:
853
897
  conn.close()
@@ -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 aicortexpy."""
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 aicortexpy %*\r\n',
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 aicortexpy "$@"\n', encoding="utf-8")
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:
@@ -61,7 +61,9 @@ def materialize_attachments(
61
61
  if not att_id:
62
62
  continue
63
63
  try:
64
- body, header_name = client.fetch_attachment_bytes(att_id)
64
+ body, header_name = client.fetch_attachment_bytes(
65
+ att_id, task.workspace_id
66
+ )
65
67
  except Exception as e:
66
68
  log.warning("fetch attachment failed id=%s: %s", att_id, e)
67
69
  continue
@@ -17,6 +17,7 @@ from .acp_conn import (
17
17
  kiro_acp_supported,
18
18
  )
19
19
  from .config import AgentEntry, Config
20
+ from .security import SecurityPolicy
20
21
 
21
22
  log = logging.getLogger("aicortexpy.acp_pool")
22
23
 
@@ -101,6 +102,9 @@ class ChatACPPool:
101
102
  on_message: Any | None = None,
102
103
  idle_watchdog: float = 0,
103
104
  system_prompt: str = "",
105
+ security_policy: SecurityPolicy | None = None,
106
+ permissions: Any = None,
107
+ task_id: str = "",
104
108
  ) -> ACPPromptResult:
105
109
  key = pool_key(provider, chat_session_id, agent_id, work_dir)
106
110
  entry = self._get_or_create(key, provider, chat_session_id, agent_id, work_dir, model)
@@ -121,6 +125,7 @@ class ChatACPPool:
121
125
  custom_args=custom_args,
122
126
  timeout=timeout,
123
127
  env=env,
128
+ security_policy=security_policy,
124
129
  )
125
130
  entry.model = model
126
131
  log.info("%s chat acp pool miss chat=%s", provider, chat_session_id[:8])
@@ -144,6 +149,8 @@ class ChatACPPool:
144
149
  on_message=on_message,
145
150
  idle_watchdog=idle_watchdog,
146
151
  cancel_event=cancel_event,
152
+ permissions=permissions,
153
+ task_id=task_id,
147
154
  )
148
155
 
149
156
  if not result.session_id and entry.conn.session_id():
@@ -173,6 +180,7 @@ class ChatACPPool:
173
180
  custom_args: list[str],
174
181
  timeout: float,
175
182
  env: dict[str, str] | None,
183
+ security_policy: SecurityPolicy | None = None,
176
184
  ) -> ACPChatConn:
177
185
  entry = self._agents.get(provider)
178
186
  if not entry:
@@ -186,6 +194,7 @@ class ChatACPPool:
186
194
  custom_args=custom_args,
187
195
  timeout=timeout,
188
196
  env=env,
197
+ security_policy=security_policy,
189
198
  )
190
199
  if provider == "kiro":
191
200
  return KiroACPConn.open(
@@ -196,6 +205,7 @@ class ChatACPPool:
196
205
  custom_args=custom_args,
197
206
  timeout=timeout,
198
207
  env=env,
208
+ security_policy=security_policy,
199
209
  )
200
210
  raise RuntimeError(f"chat acp pool: unsupported provider {provider!r}")
201
211
 
@@ -22,8 +22,10 @@ from .commands import (
22
22
  project_cmd,
23
23
  repo_cmd,
24
24
  runtime_cmd,
25
+ setup_cmd,
25
26
  skill_cmd,
26
27
  squad_cmd,
28
+ update_cmd,
27
29
  version_cmd,
28
30
  workspace_cmd,
29
31
  )
@@ -41,12 +43,31 @@ _LEGACY_DAEMON = {
41
43
  }
42
44
 
43
45
 
44
- def _setup_logging(verbose: bool = False) -> None:
45
- level = logging.DEBUG if verbose else logging.WARNING
46
+ def _setup_logging(
47
+ verbose: bool = False,
48
+ *,
49
+ log_level: str = "",
50
+ daemon_mode: bool = False,
51
+ ) -> None:
52
+ """Configure root logging.
53
+
54
+ - CLI one-shots default to WARNING (quiet).
55
+ - Daemon run defaults to INFO (useful in python-aicortex.log).
56
+ - `-v` or `--log-level DEBUG` enables DEBUG.
57
+ """
58
+ if log_level:
59
+ level = getattr(logging, log_level.upper(), logging.INFO)
60
+ elif verbose:
61
+ level = logging.DEBUG
62
+ elif daemon_mode:
63
+ level = logging.INFO
64
+ else:
65
+ level = logging.WARNING
46
66
  logging.basicConfig(
47
67
  level=level,
48
68
  format="%(asctime)s %(levelname)s %(name)s: %(message)s",
49
69
  datefmt="%H:%M:%S",
70
+ force=True,
50
71
  )
51
72
 
52
73
 
@@ -80,6 +101,8 @@ def build_parser() -> argparse.ArgumentParser:
80
101
  config_cmd.add_parser(sub)
81
102
  daemon_cmd.add_parser(sub)
82
103
  runtime_cmd.add_parser(sub)
104
+ setup_cmd.add_parser(sub)
105
+ update_cmd.add_parser(sub)
83
106
  version_cmd.add_parser(sub)
84
107
  return p
85
108
 
@@ -100,8 +123,15 @@ def main(argv: list[str] | None = None) -> int:
100
123
  except SystemExit as e:
101
124
  return int(e.code) if isinstance(e.code, int) else 1
102
125
 
103
- _setup_logging(getattr(ns, "verbose", False))
104
126
  cmd = ns.command
127
+ _setup_logging(
128
+ getattr(ns, "verbose", False),
129
+ log_level=str(getattr(ns, "log_level", "") or ""),
130
+ daemon_mode=(
131
+ cmd == "daemon"
132
+ and getattr(ns, "daemon_command", "") in {"start", "run", "restart"}
133
+ ),
134
+ )
105
135
 
106
136
  handlers = {
107
137
  "issue": issue_cmd.run,
@@ -123,6 +153,8 @@ def main(argv: list[str] | None = None) -> int:
123
153
  "version": version_cmd.run,
124
154
  "login": auth_cmd.run_login,
125
155
  "auth": auth_cmd.run_auth,
156
+ "setup": setup_cmd.run,
157
+ "update": update_cmd.run,
126
158
  }
127
159
  handler = handlers.get(cmd)
128
160
  if not handler:
@@ -422,10 +422,17 @@ class DaemonClient:
422
422
  data = self.get(f"/api/daemon/workspaces/{workspace_id}/repos")
423
423
  return data if isinstance(data, dict) else {}
424
424
 
425
- def fetch_attachment_bytes(self, attachment_id: str) -> tuple[bytes, str]:
425
+ def fetch_attachment_bytes(
426
+ self, attachment_id: str, workspace_id: str = ""
427
+ ) -> tuple[bytes, str]:
426
428
  path = f"/api/daemon/attachments/{attachment_id}/bytes"
427
429
  url = self.base_url + path
428
- req = urllib.request.Request(url, headers=self._headers(), method="GET")
430
+ headers = self._headers()
431
+ wid = (workspace_id or "").strip()
432
+ if wid:
433
+ # PAT/JWT daemon auth has no workspace in context; server requires it.
434
+ headers["X-Workspace-ID"] = wid
435
+ req = urllib.request.Request(url, headers=headers, method="GET")
429
436
  try:
430
437
  with urllib.request.urlopen(req, timeout=self.timeout, context=self._ctx) as resp:
431
438
  body = resp.read(MAX_ATTACHMENT_BYTES + 1)