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.
Files changed (102) hide show
  1. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/PKG-INFO +10 -7
  2. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/README.md +9 -6
  3. aicortexpy-0.2.3/aicortex/__init__.py +9 -0
  4. aicortexpy-0.2.3/aicortex/__main__.py +5 -0
  5. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/__init__.py +1 -1
  6. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/acp_client.py +92 -3
  7. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/acp_conn.py +62 -13
  8. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/acp_errors.py +23 -0
  9. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/agent_env.py +14 -3
  10. aicortexpy-0.2.3/aicortexpy/app_workdir.py +72 -0
  11. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/artifact_ws.py +5 -1
  12. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/artifacts.py +6 -0
  13. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/auto_commit.py +3 -1
  14. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/chat_acp_pool.py +10 -0
  15. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/cli.py +1 -1
  16. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/client.py +116 -1
  17. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/agent_cmd.py +5 -0
  18. aicortexpy-0.2.3/aicortexpy/commands/graph_cmd.py +204 -0
  19. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/memory_cmd.py +1 -1
  20. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/config.py +4 -2
  21. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/daemon.py +46 -10
  22. aicortexpy-0.2.3/aicortexpy/data/default_skills/aicortex-graph/SKILL.md +46 -0
  23. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/execenv.py +13 -1
  24. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/models.py +12 -3
  25. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/openclaw_config.py +3 -1
  26. aicortexpy-0.2.3/aicortexpy/permission.py +107 -0
  27. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/process.py +35 -16
  28. aicortexpy-0.2.3/aicortexpy/procutil.py +113 -0
  29. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/repo.py +13 -12
  30. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/runner.py +76 -9
  31. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/runtime_config.py +73 -1
  32. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/sandbox.py +2 -2
  33. aicortexpy-0.2.3/aicortexpy/security.py +168 -0
  34. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/terminal.py +3 -0
  35. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/wakeup.py +15 -0
  36. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy.egg-info/PKG-INFO +10 -7
  37. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy.egg-info/SOURCES.txt +9 -1
  38. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy.egg-info/entry_points.txt +0 -1
  39. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy.egg-info/top_level.txt +1 -0
  40. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/pyproject.toml +9 -4
  41. aicortexpy-0.2.3/scripts/aicortex +3 -0
  42. aicortexpy-0.2.3/scripts/aicortex.cmd +3 -0
  43. aicortexpy-0.2.3/tests/test_app_workdir.py +59 -0
  44. aicortexpy-0.2.1/aicortexpy/commands/graph_cmd.py +0 -69
  45. aicortexpy-0.2.1/aicortexpy/data/default_skills/aicortex-graph/SKILL.md +0 -30
  46. aicortexpy-0.2.1/aicortexpy/procutil.py +0 -61
  47. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/MANIFEST.in +0 -0
  48. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/__main__.py +0 -0
  49. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/attachments.py +0 -0
  50. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/capabilities.py +0 -0
  51. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/cli_context.py +0 -0
  52. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/codex_config.py +0 -0
  53. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/__init__.py +0 -0
  54. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/attachment_cmd.py +0 -0
  55. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/auth_cmd.py +0 -0
  56. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/autopilot_cmd.py +0 -0
  57. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/config_cmd.py +0 -0
  58. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/daemon_cmd.py +0 -0
  59. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/issue_cmd.py +0 -0
  60. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/label_cmd.py +0 -0
  61. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/meeting_cmd.py +0 -0
  62. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/project_cmd.py +0 -0
  63. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/repo_cmd.py +0 -0
  64. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/runtime_cmd.py +0 -0
  65. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/setup_cmd.py +0 -0
  66. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/skill_cmd.py +0 -0
  67. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/squad_cmd.py +0 -0
  68. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/update_cmd.py +0 -0
  69. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/version_cmd.py +0 -0
  70. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/commands/workspace_cmd.py +0 -0
  71. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/agent-memory/SKILL.md +0 -0
  72. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/aicorkit-atlassian/SKILL.md +0 -0
  73. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/aicorkit-graph/SKILL.md +0 -0
  74. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/aicorkit-outlook/SKILL.md +0 -0
  75. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/aicorkit-wxmsg/SKILL.md +0 -0
  76. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/aicortex-basics/SKILL.md +0 -0
  77. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/aicortex-bridge-cdp/SKILL.md +0 -0
  78. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/aicortex-cli-ops/SKILL.md +0 -0
  79. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/graphify/SKILL.md +0 -0
  80. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/question-form/SKILL.md +0 -0
  81. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/test-report-generator/SKILL.md +0 -0
  82. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/test-report-generator/scripts/generate_cases.py +0 -0
  83. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/test-report-generator/scripts/generate_report.py +0 -0
  84. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/design_seed.py +0 -0
  85. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/diskusage.py +0 -0
  86. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/gc.py +0 -0
  87. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/handoff_bundle.py +0 -0
  88. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/health.py +0 -0
  89. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/id_resolve.py +0 -0
  90. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/local_skills.py +0 -0
  91. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/oauth_login.py +0 -0
  92. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/output.py +0 -0
  93. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/pinned_workdir.py +0 -0
  94. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/platform_client.py +0 -0
  95. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/platform_info.py +0 -0
  96. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/poisoned.py +0 -0
  97. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/prompt.py +0 -0
  98. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/stream.py +0 -0
  99. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy/tunnel.py +0 -0
  100. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy.egg-info/dependency_links.txt +0 -0
  101. {aicortexpy-0.2.1 → aicortexpy-0.2.3}/aicortexpy.egg-info/requires.txt +0 -0
  102. {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.1
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
- 也可用 `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.1"
3
+ __version__ = "0.2.3"
@@ -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"):
@@ -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 ACPProviderErrorSniffer, promote_acp_result_on_provider_error
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 = subprocess.run(
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
- if sys.platform != "win32":
92
- return {}
93
- startup = subprocess.STARTUPINFO()
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(self._stdin_text, tool_name_fn=tool_name_fn)
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 = ["--yolo", "--trust", "--sandbox", "disabled", "acp", *filtered]
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
- conn._start_process(argv, work, env, tool_name_fn=hermes_tool_name_from_title)
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", "--trust-all-tools", *filtered]
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
- conn._start_process(argv, work, env, tool_name_fn=kiro_tool_name)
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 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:
@@ -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
- "rel_path": f"{rel.rstrip('/')}/{child.name}".lstrip("/") if rel else child.name,
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)
@@ -27,7 +27,9 @@ EXCLUDE = [
27
27
 
28
28
 
29
29
  def _git(cwd: Path, *args: str) -> tuple[int, str, str]:
30
- proc = subprocess.run(
30
+ from .procutil import run_no_window
31
+
32
+ proc = run_no_window(
31
33
  ["git", *args],
32
34
  cwd=str(cwd),
33
35
  capture_output=True,