@arnilo/prism-tools 0.0.0-stage → 0.13.1

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 (331) hide show
  1. package/CHANGELOG.md +418 -0
  2. package/LICENSE +21 -0
  3. package/README.md +44 -2
  4. package/browser/README.md +98 -0
  5. package/browser/skills/browser-automation/SKILL.md +14 -0
  6. package/browser/src/actions/cdp.ts +131 -0
  7. package/browser/src/actions/dialog.ts +25 -0
  8. package/browser/src/actions/download_release.ts +22 -0
  9. package/browser/src/actions/index.ts +37 -0
  10. package/browser/src/actions/locator.ts +70 -0
  11. package/browser/src/actions/navigate.ts +23 -0
  12. package/browser/src/actions/screenshot.ts +27 -0
  13. package/browser/src/actions/scroll.ts +24 -0
  14. package/browser/src/actions/select_page.ts +21 -0
  15. package/browser/src/actions/types.ts +114 -0
  16. package/browser/src/actions/upload.ts +44 -0
  17. package/browser/src/actions/wait.ts +45 -0
  18. package/browser/src/browser.ts +197 -0
  19. package/browser/src/cdp.ts +178 -0
  20. package/browser/src/checkpoint.ts +189 -0
  21. package/browser/src/config.ts +72 -0
  22. package/browser/src/downloads.ts +253 -0
  23. package/browser/src/errors.ts +31 -0
  24. package/browser/src/evaluate.ts +101 -0
  25. package/browser/src/index.ts +129 -0
  26. package/browser/src/limits.ts +271 -0
  27. package/browser/src/manager.ts +730 -0
  28. package/browser/src/network.ts +263 -0
  29. package/browser/src/observe.ts +178 -0
  30. package/browser/src/policy.ts +133 -0
  31. package/browser/src/screenshot.ts +107 -0
  32. package/browser/src/shared-sandbox.ts +53 -0
  33. package/browser/src/snapshot.ts +99 -0
  34. package/browser/src/targets.ts +165 -0
  35. package/browser/src/tools.ts +480 -0
  36. package/browser/src/types.ts +316 -0
  37. package/browser/src/uploads.ts +174 -0
  38. package/coding/README.md +337 -0
  39. package/coding/src/agent/acp-operations.ts +146 -0
  40. package/coding/src/agent/artifacts.ts +38 -0
  41. package/coding/src/agent/ask-user-decision.ts +623 -0
  42. package/coding/src/agent/atomic-write.ts +23 -0
  43. package/coding/src/agent/bounded-file.ts +21 -0
  44. package/coding/src/agent/checks.ts +301 -0
  45. package/coding/src/agent/coding-checkpoint.ts +828 -0
  46. package/coding/src/agent/delete.ts +237 -0
  47. package/coding/src/agent/diagnostics-tool.ts +163 -0
  48. package/coding/src/agent/diagnostics.ts +262 -0
  49. package/coding/src/agent/edit-diff.ts +382 -0
  50. package/coding/src/agent/edit.ts +322 -0
  51. package/coding/src/agent/effects.ts +102 -0
  52. package/coding/src/agent/env.ts +28 -0
  53. package/coding/src/agent/execution-policy.ts +89 -0
  54. package/coding/src/agent/file-mutation-queue.ts +72 -0
  55. package/coding/src/agent/forge/github.ts +603 -0
  56. package/coding/src/agent/forge/index.ts +16 -0
  57. package/coding/src/agent/forge/types.ts +185 -0
  58. package/coding/src/agent/git-aware-repository.ts +305 -0
  59. package/coding/src/agent/git-exec.ts +326 -0
  60. package/coding/src/agent/git-status.ts +177 -0
  61. package/coding/src/agent/git-tools.ts +360 -0
  62. package/coding/src/agent/git.ts +1074 -0
  63. package/coding/src/agent/glob-match.ts +146 -0
  64. package/coding/src/agent/glob.ts +182 -0
  65. package/coding/src/agent/goal-verify.ts +385 -0
  66. package/coding/src/agent/grep-worker.ts +74 -0
  67. package/coding/src/agent/grep.ts +459 -0
  68. package/coding/src/agent/identity.ts +48 -0
  69. package/coding/src/agent/index.ts +676 -0
  70. package/coding/src/agent/language/client.ts +429 -0
  71. package/coding/src/agent/language/framing.ts +190 -0
  72. package/coding/src/agent/language/index.ts +23 -0
  73. package/coding/src/agent/language/intelligence.ts +682 -0
  74. package/coding/src/agent/language/types.ts +187 -0
  75. package/coding/src/agent/lifecycle-events.ts +121 -0
  76. package/coding/src/agent/lifecycle.ts +166 -0
  77. package/coding/src/agent/limits.ts +214 -0
  78. package/coding/src/agent/move.ts +178 -0
  79. package/coding/src/agent/mutation-path.ts +52 -0
  80. package/coding/src/agent/output-accumulator.ts +304 -0
  81. package/coding/src/agent/path-utils.ts +120 -0
  82. package/coding/src/agent/process/index.ts +58 -0
  83. package/coding/src/agent/process/recovery.ts +508 -0
  84. package/coding/src/agent/process/sessions-host.ts +222 -0
  85. package/coding/src/agent/process/sessions-monitor.ts +141 -0
  86. package/coding/src/agent/process/sessions-recovery.ts +241 -0
  87. package/coding/src/agent/process/sessions-spawn.ts +613 -0
  88. package/coding/src/agent/process/sessions-teardown.ts +281 -0
  89. package/coding/src/agent/process/sessions.ts +30 -0
  90. package/coding/src/agent/process/types.ts +358 -0
  91. package/coding/src/agent/read-path-set.ts +126 -0
  92. package/coding/src/agent/read.ts +713 -0
  93. package/coding/src/agent/repository/glob.ts +147 -0
  94. package/coding/src/agent/repository/indexed-search.ts +480 -0
  95. package/coding/src/agent/repository/list.ts +131 -0
  96. package/coding/src/agent/repository/operations.ts +22 -0
  97. package/coding/src/agent/repository/path.ts +98 -0
  98. package/coding/src/agent/repository/search.ts +277 -0
  99. package/coding/src/agent/repository/types.ts +243 -0
  100. package/coding/src/agent/repository/walk.ts +126 -0
  101. package/coding/src/agent/repository.ts +12 -0
  102. package/coding/src/agent/shell-background.ts +264 -0
  103. package/coding/src/agent/shell-output.ts +75 -0
  104. package/coding/src/agent/shell.ts +526 -0
  105. package/coding/src/agent/todo.ts +399 -0
  106. package/coding/src/agent/truncate.ts +251 -0
  107. package/coding/src/agent/write.ts +170 -0
  108. package/coding/src/ask-user-host.ts +48 -0
  109. package/coding/src/config.ts +278 -0
  110. package/coding/src/hard-deny.ts +97 -0
  111. package/coding/src/index.ts +318 -0
  112. package/coding/src/principal-guard.ts +84 -0
  113. package/coding/src/sandbox.ts +92 -0
  114. package/coding/src/security/approval.ts +144 -0
  115. package/coding/src/security/command-rules.ts +57 -0
  116. package/coding/src/security/docker-cli.ts +222 -0
  117. package/coding/src/security/docker-sandbox.ts +1205 -0
  118. package/coding/src/security/e2b-sandbox.ts +853 -0
  119. package/coding/src/security/egress/dns-pin.ts +88 -0
  120. package/coding/src/security/egress/index.ts +32 -0
  121. package/coding/src/security/egress/limits.ts +57 -0
  122. package/coding/src/security/egress/policy.ts +137 -0
  123. package/coding/src/security/egress/proxy.ts +599 -0
  124. package/coding/src/security/egress/types.ts +30 -0
  125. package/coding/src/security/index.ts +199 -0
  126. package/coding/src/security/native-sandbox.ts +639 -0
  127. package/coding/src/security/path-containment.ts +55 -0
  128. package/coding/src/security/sandbox-coding-operations.ts +392 -0
  129. package/coding/src/security/sandbox-fs-operations.ts +644 -0
  130. package/coding/src/security/sandbox-limits.ts +147 -0
  131. package/coding/src/security/sandbox-tar.ts +308 -0
  132. package/coding/src/security/sandbox.ts +179 -0
  133. package/coding/src/security/semaphore.ts +55 -0
  134. package/coding/src/targets.ts +110 -0
  135. package/coding/src/workspace/diff.ts +78 -0
  136. package/coding/src/workspace/errors.ts +29 -0
  137. package/coding/src/workspace/index.ts +5 -0
  138. package/coding/src/workspace/merge.ts +151 -0
  139. package/coding/src/workspace/routing.ts +76 -0
  140. package/coding/src/workspace/service.ts +584 -0
  141. package/computer-use/README.md +105 -0
  142. package/computer-use/skills/computer-use-linux/SKILL.md +17 -0
  143. package/computer-use/src/classify.ts +53 -0
  144. package/computer-use/src/command.ts +29 -0
  145. package/computer-use/src/config.ts +69 -0
  146. package/computer-use/src/create.ts +170 -0
  147. package/computer-use/src/devices.ts +247 -0
  148. package/computer-use/src/env.ts +28 -0
  149. package/computer-use/src/index.ts +157 -0
  150. package/computer-use/src/skill.ts +21 -0
  151. package/openapi/README.md +97 -0
  152. package/openapi/src/compile.ts +386 -0
  153. package/openapi/src/config.ts +73 -0
  154. package/openapi/src/errors.ts +16 -0
  155. package/openapi/src/execute.ts +241 -0
  156. package/openapi/src/index.ts +124 -0
  157. package/openapi/src/limits.ts +63 -0
  158. package/openapi/src/specs.ts +86 -0
  159. package/openapi/src/tools.ts +60 -0
  160. package/openapi/src/types.ts +61 -0
  161. package/package.json +171 -4
  162. package/research/README.md +72 -0
  163. package/research/skills/research-report/SKILL.md +12 -0
  164. package/research/skills/research-scoping/SKILL.md +10 -0
  165. package/research/skills/research-synthesis/SKILL.md +10 -0
  166. package/research/skills/source-triage/SKILL.md +10 -0
  167. package/research/src/check.ts +71 -0
  168. package/research/src/config.ts +41 -0
  169. package/research/src/index.ts +147 -0
  170. package/research/src/ledger.ts +157 -0
  171. package/research/src/urls.ts +21 -0
  172. package/web/README.md +287 -0
  173. package/web/skills/web-research/SKILL.md +17 -0
  174. package/web/src/brave.ts +66 -0
  175. package/web/src/cache.ts +151 -0
  176. package/web/src/config.ts +210 -0
  177. package/web/src/controls.ts +128 -0
  178. package/web/src/domains.ts +114 -0
  179. package/web/src/exa.ts +60 -0
  180. package/web/src/firecrawl.ts +160 -0
  181. package/web/src/index.ts +387 -0
  182. package/web/src/kagi.ts +73 -0
  183. package/web/src/limits.ts +55 -0
  184. package/web/src/native/convert.ts +84 -0
  185. package/web/src/native/fetch.ts +226 -0
  186. package/web/src/normalize.ts +131 -0
  187. package/web/src/obscura/cdp.ts +201 -0
  188. package/web/src/obscura/classify.ts +35 -0
  189. package/web/src/obscura/cli.ts +254 -0
  190. package/web/src/obscura/errors.ts +8 -0
  191. package/web/src/obscura/index.ts +59 -0
  192. package/web/src/obscura/limits.ts +92 -0
  193. package/web/src/obscura/mcp.ts +104 -0
  194. package/web/src/obscura/playwright.ts +133 -0
  195. package/web/src/obscura/process.ts +235 -0
  196. package/web/src/obscura/search-profile.ts +73 -0
  197. package/web/src/obscura/types.ts +48 -0
  198. package/web/src/obscura/web.ts +419 -0
  199. package/web/src/prompt-fetch.ts +88 -0
  200. package/web/src/render.ts +75 -0
  201. package/web/src/searxng.ts +78 -0
  202. package/web/src/slice.ts +139 -0
  203. package/web/src/tavily.ts +77 -0
  204. package/web/src/tools.ts +182 -0
  205. package/web/src/transport.ts +310 -0
  206. package/web/src/types.ts +199 -0
  207. package/work/README.md +77 -0
  208. package/work/THIRD_PARTY_NOTICES +24 -0
  209. package/work/docling/ocr.py +150 -0
  210. package/work/sandbox/Dockerfile +36 -0
  211. package/work/sandbox/soffice.sh +12 -0
  212. package/work/src/command.ts +29 -0
  213. package/work/src/config.ts +138 -0
  214. package/work/src/connectors/cli.ts +343 -0
  215. package/work/src/connectors/drafts.ts +647 -0
  216. package/work/src/connectors/errors.ts +8 -0
  217. package/work/src/connectors/file-bytes.ts +149 -0
  218. package/work/src/connectors/google-workspace-http.ts +399 -0
  219. package/work/src/connectors/google-workspace.ts +465 -0
  220. package/work/src/connectors/gws-tools.ts +378 -0
  221. package/work/src/connectors/http.ts +123 -0
  222. package/work/src/connectors/idempotency.ts +235 -0
  223. package/work/src/connectors/identity.ts +34 -0
  224. package/work/src/connectors/index.ts +93 -0
  225. package/work/src/connectors/limits.ts +50 -0
  226. package/work/src/connectors/m365-tools.ts +308 -0
  227. package/work/src/connectors/microsoft365-http.ts +428 -0
  228. package/work/src/connectors/microsoft365.ts +402 -0
  229. package/work/src/connectors/normalize.ts +120 -0
  230. package/work/src/connectors/tool-helpers.ts +398 -0
  231. package/work/src/connectors/tools.ts +42 -0
  232. package/work/src/connectors/types.ts +399 -0
  233. package/work/src/diagrams/canonicalize.ts +166 -0
  234. package/work/src/diagrams/caps.ts +52 -0
  235. package/work/src/diagrams/embed.ts +340 -0
  236. package/work/src/diagrams/errors.ts +76 -0
  237. package/work/src/diagrams/index.ts +81 -0
  238. package/work/src/diagrams/messages.ts +150 -0
  239. package/work/src/diagrams/origin.ts +52 -0
  240. package/work/src/diagrams/telemetry.ts +34 -0
  241. package/work/src/diagrams/xml.ts +267 -0
  242. package/work/src/document-extraction/index.ts +563 -0
  243. package/work/src/document-reader/ambient.d.ts +2 -0
  244. package/work/src/document-reader/errors.ts +7 -0
  245. package/work/src/document-reader/index.ts +181 -0
  246. package/work/src/document-reader/mistral-ocr.ts +318 -0
  247. package/work/src/document-reader/parsers.ts +171 -0
  248. package/work/src/documents/caps.ts +135 -0
  249. package/work/src/documents/diff.ts +269 -0
  250. package/work/src/documents/errors.ts +44 -0
  251. package/work/src/documents/fidelity.ts +98 -0
  252. package/work/src/documents/generate.ts +96 -0
  253. package/work/src/documents/hash.ts +8 -0
  254. package/work/src/documents/index.ts +14 -0
  255. package/work/src/documents/model-schema.ts +639 -0
  256. package/work/src/documents/parse.ts +252 -0
  257. package/work/src/documents/patch-history.ts +81 -0
  258. package/work/src/documents/patch.ts +476 -0
  259. package/work/src/documents/preview-html.ts +245 -0
  260. package/work/src/documents/preview.ts +227 -0
  261. package/work/src/documents/telemetry.ts +36 -0
  262. package/work/src/documents/translate/docx.ts +292 -0
  263. package/work/src/documents/translate/pptx.ts +304 -0
  264. package/work/src/documents/translate/xlsx.ts +137 -0
  265. package/work/src/documents/types.ts +188 -0
  266. package/work/src/index.ts +311 -0
  267. package/work/src/runtime/worker-pool-entry.ts +102 -0
  268. package/work/src/runtime/worker-pool.ts +267 -0
  269. package/work/src/sandbox/composition.ts +212 -0
  270. package/work/src/sandbox/image.ts +6 -0
  271. package/work/src/sandbox/index.ts +14 -0
  272. package/work/src/sheets/caps.ts +68 -0
  273. package/work/src/sheets/csv.ts +351 -0
  274. package/work/src/sheets/decimal.ts +388 -0
  275. package/work/src/sheets/errors.ts +52 -0
  276. package/work/src/sheets/index.ts +62 -0
  277. package/work/src/sheets/inference.ts +378 -0
  278. package/work/src/sheets/telemetry.ts +34 -0
  279. package/work/src/sheets/types.ts +162 -0
  280. package/work/src/sheets/xlsx.ts +388 -0
  281. package/work/src/skills/index.ts +8 -0
  282. package/work/src/skills/load.ts +43 -0
  283. package/work/src/tools/filesystem.ts +21 -0
  284. package/work/src/tools/index.ts +1 -0
  285. package/work/src/tools/local-filesystem.ts +33 -0
  286. package/work/src/tools/office.ts +311 -0
  287. package/work/vendor/hermes-agent/LICENSE +21 -0
  288. package/work/vendor/hermes-agent/skills/productivity/docx/LICENSE +21 -0
  289. package/work/vendor/hermes-agent/skills/productivity/docx/SKILL.md +196 -0
  290. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_comments.py +289 -0
  291. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_common.py +94 -0
  292. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_create.py +177 -0
  293. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_edit.py +250 -0
  294. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_read.py +149 -0
  295. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_revisions.py +147 -0
  296. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_template.py +70 -0
  297. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_validate.py +156 -0
  298. package/work/vendor/hermes-agent/skills/productivity/pdf/LICENSE +21 -0
  299. package/work/vendor/hermes-agent/skills/productivity/pdf/SKILL.md +125 -0
  300. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/_raster.py +76 -0
  301. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_marker.py +88 -0
  302. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_pymupdf.py +101 -0
  303. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_create.py +130 -0
  304. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_fill_form.py +97 -0
  305. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_form_layout.py +168 -0
  306. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_make_form.py +145 -0
  307. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_merge.py +50 -0
  308. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_meta.py +115 -0
  309. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_page_image.py +99 -0
  310. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_read.py +153 -0
  311. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_secure.py +71 -0
  312. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_split.py +84 -0
  313. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_stamp.py +143 -0
  314. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_watermark.py +51 -0
  315. package/work/vendor/hermes-agent/skills/productivity/powerpoint/LICENSE +21 -0
  316. package/work/vendor/hermes-agent/skills/productivity/powerpoint/SKILL.md +220 -0
  317. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_create.py +214 -0
  318. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_edit.py +436 -0
  319. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_from_template.py +88 -0
  320. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_read.py +131 -0
  321. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_render.py +93 -0
  322. package/work/vendor/hermes-agent/skills/productivity/xlsx/LICENSE +21 -0
  323. package/work/vendor/hermes-agent/skills/productivity/xlsx/SKILL.md +196 -0
  324. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/csv_to_xlsx.py +104 -0
  325. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_create.py +259 -0
  326. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_edit.py +263 -0
  327. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_read.py +160 -0
  328. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_recalc.py +111 -0
  329. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_restructure.py +337 -0
  330. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_to_csv.py +69 -0
  331. package/work/vendor-lock.json +11 -0
@@ -0,0 +1,318 @@
1
+ /**
2
+ * `@arnilo/prism-tools/coding` — the first-party coding tools as a package.
3
+ *
4
+ * The default export registers the file, search, and shell tools (`read`, `write`, `edit`, `grep`,
5
+ * `glob`, `shell` with `shell_output`, plus `delete`/`move` when the shell is unavailable, or the
6
+ * read subset with `readOnly: true`), the
7
+ * structured git tools, `coding_check` when checks are configured, `todo_write` with its `run.end`
8
+ * continuation, and `ask_user_decision` over `host.ui`.
9
+ *
10
+ * Root escapes, read-only violations, and configured `commandRules` denies are hard-denied through
11
+ * one `tool.call` decide handler, so they hold under any approval mode or host `allow` hook. A
12
+ * durable-effect tool fails closed without a host principal. The `coding.targets` service reports the
13
+ * paths and roots of each own-tool call for approval.
14
+ *
15
+ * ```ts
16
+ * import toolsCoding from "@arnilo/prism-tools/coding";
17
+ * import { createAgent } from "@arnilo/prism";
18
+ *
19
+ * const agent = await createAgent({ model, packages: [toolsCoding({ root: process.cwd() })] });
20
+ * ```
21
+ */
22
+
23
+ import { resolve } from "node:path";
24
+ import { definePackage, type PackageInstance, type ToolDefinition } from "@arnilo/prism";
25
+ import manifest from "../../package.json" with { type: "json" };
26
+ import { createAskUserDecisionTool } from "./agent/ask-user-decision.js";
27
+ import { createCodingCheckTool } from "./agent/checks.js";
28
+ import { createDiagnosticsTool, type DiagnosticsToolHandle } from "./agent/diagnostics-tool.js";
29
+ import type { ForgeCredentialResolverSource } from "./agent/forge/index.js";
30
+ import { createGitTools } from "./agent/git-tools.js";
31
+ import { createCodingTools, createReadOnlyTools, type ProcessSandboxBackend, type ToolsOptions } from "./agent/index.js";
32
+ import { getShellConfig } from "./agent/shell.js";
33
+ import { createBackgroundProcesses } from "./agent/shell-background.js";
34
+ import { createTodoContinuationDecision, createTodoWriteTool } from "./agent/todo.js";
35
+ import { createAskUserHostHook } from "./ask-user-host.js";
36
+ import { type ToolsCodingCommandRule, type ToolsCodingConfig, toolsCodingConfigSchema } from "./config.js";
37
+ import { createHardDenyHook } from "./hard-deny.js";
38
+ import { withDurablePrincipal } from "./principal-guard.js";
39
+ import { type CodingSandboxKind, selectCodingSandbox } from "./sandbox.js";
40
+ import { type CommandRule } from "./security/command-rules.js";
41
+ import { createEgressPolicy } from "./security/egress/index.js";
42
+ import { createCodingTargets } from "./targets.js";
43
+ import { createWorkspaceService, createWorktreeRouting, type WorkspaceService, WorkspaceServiceError } from "./workspace/index.js";
44
+
45
+ // The moved agent and security surfaces keep their names, now behind one entry point.
46
+ export * from "./agent/index.js";
47
+ export { type AskUserHostHookOptions, createAskUserHostHook } from "./ask-user-host.js";
48
+ export type {
49
+ ToolsCodingCommandRule,
50
+ ToolsCodingConfig,
51
+ ToolsCodingEgressConfig,
52
+ ToolsCodingEgressRule,
53
+ ToolsCodingForgeConfig,
54
+ ToolsCodingGitConfig,
55
+ ToolsCodingLanguageConfig,
56
+ ToolsCodingLanguageServerConfig,
57
+ ToolsCodingPrincipalConfig,
58
+ ToolsCodingRepositoryConfig,
59
+ ToolsCodingShellConfig,
60
+ ToolsCodingTodoConfig,
61
+ ToolsCodingWorkspaceConfig,
62
+ } from "./config.js";
63
+ export { toolsCodingConfigSchema } from "./config.js";
64
+ export { createHardDenyHook, type HardDenyOptions } from "./hard-deny.js";
65
+ export {
66
+ durablePrincipalError,
67
+ ERR_PRISM_CODING_PRINCIPAL_MISMATCH,
68
+ ERR_PRISM_CODING_PRINCIPAL_REQUIRED,
69
+ isDurableCall,
70
+ withDurablePrincipal,
71
+ } from "./principal-guard.js";
72
+ export { type CodingSandboxKind, type SelectedCodingSandbox, selectCodingSandbox } from "./sandbox.js";
73
+ export * from "./security/index.js";
74
+ export { CODING_TOOL_NAMES, type CodingTargets, type CodingTargetsFn, createCodingTargets } from "./targets.js";
75
+ export * from "./workspace/index.js";
76
+
77
+ function toCommandRules(rules: readonly ToolsCodingCommandRule[] | undefined): readonly CommandRule[] {
78
+ return (rules ?? []).map((rule) => ({
79
+ pattern: rule.match,
80
+ action: rule.decision,
81
+ ...(rule.reason !== undefined ? { reason: rule.reason } : {}),
82
+ }));
83
+ }
84
+
85
+ function errorMessage(error: unknown): string {
86
+ return error instanceof Error ? error.message : String(error);
87
+ }
88
+
89
+ /** The `coding.forge` service: a ref-only credential source a forge adapter resolves per call. */
90
+ export interface CodingForgeService {
91
+ readonly name: string;
92
+ readonly resolver: ForgeCredentialResolverSource["resolver"];
93
+ }
94
+
95
+ const definition = definePackage<ToolsCodingConfig>({
96
+ name: "@arnilo/prism-tools/coding",
97
+ version: manifest.version,
98
+ description:
99
+ "File, search, shell, git, check, todo, and ask-user coding tools with declared effects, in-tool containment, and tool.call hard denies.",
100
+ requires: { api: 1 },
101
+ capabilities: ["host-ui", "credentials"],
102
+ config: toolsCodingConfigSchema,
103
+ setup: async (prism) => {
104
+ const root = prism.config.root;
105
+ if (typeof root !== "string" || root.length === 0) {
106
+ throw new Error("prism-tools-coding: config needs root (the workspace root the file tools operate in)");
107
+ }
108
+ const readOnly = prism.config.readOnly === true;
109
+ const commandRules = toCommandRules(prism.config.commandRules);
110
+ const sandbox: CodingSandboxKind = prism.config.sandbox ?? "none";
111
+ const principalId = prism.config.principal?.id;
112
+
113
+ if (prism.config.egress !== undefined) {
114
+ try {
115
+ createEgressPolicy({
116
+ allow: prism.config.egress.allow?.map((rule) => ({
117
+ host: rule.host,
118
+ port: rule.port,
119
+ protocol: rule.protocol ?? "https",
120
+ ...(rule.allowPrivate !== undefined ? { allowPrivate: rule.allowPrivate } : {}),
121
+ })),
122
+ presets: prism.config.egress.presets,
123
+ });
124
+ } catch (error) {
125
+ throw new Error(`prism-tools-coding: invalid egress config: ${errorMessage(error)}`);
126
+ }
127
+ if (sandbox === "none") {
128
+ prism.status.note("warn", "egress config has no effect without a sandbox backend");
129
+ }
130
+ }
131
+
132
+ const options: ToolsOptions = {};
133
+ if (prism.config.repository !== undefined) options.repository = prism.config.repository;
134
+ if (prism.config.shell?.timeoutMs !== undefined) options.shell = { timeout: prism.config.shell.timeoutMs / 1000 };
135
+
136
+ let toolRoot = resolve(root);
137
+ let shellAvailable = prism.config.shell?.enabled !== false;
138
+ let sandboxTools: readonly ToolDefinition[] | undefined;
139
+ let processBackend: ProcessSandboxBackend | undefined;
140
+
141
+ if (sandbox !== "none") {
142
+ const selected = await selectCodingSandbox(sandbox, { root, repository: prism.config.repository, readOnly });
143
+ if (selected.tools !== undefined) {
144
+ sandboxTools = selected.tools;
145
+ toolRoot = selected.toolRoot;
146
+ processBackend = selected.processes;
147
+ } else {
148
+ for (const warning of selected.warnings) prism.status.note("warn", warning);
149
+ shellAvailable = false;
150
+ }
151
+ }
152
+
153
+ // `delete`/`move` are the no-shell substitutes (plan 189 Task 8), so the set depends on this.
154
+ options.shellAvailable = shellAvailable;
155
+
156
+ // Background processes (plan 189 Task 9): one lazy runtime shared by every worktree's tool set.
157
+ // Nothing is constructed in the process library until the first background call.
158
+ const background =
159
+ shellAvailable && !readOnly && (sandbox === "none" || processBackend !== undefined)
160
+ ? createBackgroundProcesses({
161
+ maxProcesses: prism.config.shell?.background?.maxProcesses,
162
+ shell: getShellConfig(),
163
+ ...(processBackend !== undefined ? { sandbox: processBackend } : {}),
164
+ })
165
+ : undefined;
166
+ if (background !== undefined) options.shell = { ...(options.shell ?? {}), background };
167
+
168
+ // `diagnostics` (plan 189 Task 10): opt-in, one language workspace per cwd, servers start on the
169
+ // first call. Handles stay in the map so a disposed tool can start its servers again.
170
+ const languageServers = prism.config.language?.servers;
171
+ const diagnosticsEnabled = languageServers !== undefined && Object.keys(languageServers).length > 0;
172
+ if (diagnosticsEnabled && sandbox !== "none") {
173
+ prism.status.note("warn", "language servers run on the host, outside the sandbox");
174
+ }
175
+ const diagnosticsByCwd = new Map<string, DiagnosticsToolHandle>();
176
+ const diagnosticsTool = (cwd: string): ToolDefinition => {
177
+ let handle = diagnosticsByCwd.get(cwd);
178
+ if (handle === undefined) {
179
+ handle = createDiagnosticsTool(cwd, {
180
+ servers: languageServers ?? {},
181
+ onServerFailure: (server, message) => prism.status.note("warn", `language server ${server}: ${message}`),
182
+ });
183
+ diagnosticsByCwd.set(cwd, handle);
184
+ }
185
+ return handle.tool;
186
+ };
187
+
188
+ /**
189
+ * The cwd-taking tools for one directory: `toolRoot` at setup, and once per worktree for a
190
+ * session bound to one (plan 165 Task 7). A sandbox backend's own set is not per-cwd.
191
+ */
192
+ const cwdTools = (cwd: string): readonly ToolDefinition[] => {
193
+ if (sandboxTools !== undefined) {
194
+ return diagnosticsEnabled ? [...sandboxTools, diagnosticsTool(cwd)] : sandboxTools;
195
+ }
196
+ let set = readOnly ? createReadOnlyTools(cwd, options) : createCodingTools(cwd, options);
197
+ // Git and checks can mutate, so read-only mode keeps exactly the four read tools.
198
+ if (!readOnly) {
199
+ if (prism.config.git?.enabled !== false) set = [...set, ...createGitTools(cwd, options)];
200
+ if (prism.config.checks !== undefined && Object.keys(prism.config.checks).length > 0) {
201
+ set = [...set, createCodingCheckTool(cwd, { checks: prism.config.checks })];
202
+ }
203
+ }
204
+ if (!shellAvailable) set = set.filter((tool) => tool.name !== "shell");
205
+ return diagnosticsEnabled ? [...set, diagnosticsTool(cwd)] : set;
206
+ };
207
+
208
+ let tools = cwdTools(toolRoot);
209
+
210
+ // Todo and ask_user take no cwd, so they are registered once rather than once per worktree.
211
+ if (!readOnly) {
212
+ const todoEnabled = prism.config.todo?.enabled !== false;
213
+ if (todoEnabled) tools = [...tools, createTodoWriteTool()];
214
+ if (todoEnabled && prism.config.todo?.continueOnOpen !== false) {
215
+ prism.hooks.on(
216
+ "run.end",
217
+ createTodoContinuationDecision({
218
+ onNoProgressStop: (info) =>
219
+ prism.events.emit("todo.no-progress-stop", { open: info.openTodos.length, noProgress: info.noProgress }),
220
+ }),
221
+ { id: "todo" },
222
+ );
223
+ }
224
+ const ui = prism.host.ui;
225
+ tools = [...tools, createAskUserDecisionTool({ ui })];
226
+ prism.hooks.on("tool.call", createAskUserHostHook({ ui }), { id: "ask-user" });
227
+ }
228
+
229
+ let workspaceService: WorkspaceService | undefined;
230
+ if (prism.config.workspace?.enabled !== false) {
231
+ try {
232
+ const workspace = await createWorkspaceService({
233
+ root: toolRoot,
234
+ sandboxed: sandbox !== "none",
235
+ ...(prism.config.workspace?.worktreeRoot !== undefined ? { worktreeRoot: prism.config.workspace.worktreeRoot } : {}),
236
+ ...(prism.config.workspace?.maxWorktrees !== undefined ? { maxWorktrees: prism.config.workspace.maxWorktrees } : {}),
237
+ ...(prism.config.workspace?.diff !== undefined ? { diff: prism.config.workspace.diff } : {}),
238
+ });
239
+ workspaceService = workspace;
240
+ prism.services.provide("workspace", workspace);
241
+ } catch (error) {
242
+ // Outside a git repository there is no worktree to hand out; say so and serve no service.
243
+ if (error instanceof WorkspaceServiceError && error.code === "workspace_not_git") {
244
+ prism.status.note("info", `workspace service unavailable: ${error.message}`);
245
+ } else {
246
+ throw error;
247
+ }
248
+ }
249
+ }
250
+
251
+ // Approval and the hard-deny hook follow the session's worktree, when it has one.
252
+ const targets = createCodingTargets(toolRoot, prism.config.roots ?? [], workspaceService, () => prism.skills.list());
253
+ prism.services.provide("coding.targets", targets);
254
+ prism.hooks.on("tool.call", createHardDenyHook({ targets, readOnly, commandRules }), { id: "hard-deny" });
255
+
256
+ // One routing wrapper at registration: a bound session's tools run in its worktree.
257
+ const routing =
258
+ workspaceService !== undefined && sandboxTools === undefined
259
+ ? createWorktreeRouting({ base: tools, create: cwdTools, workspace: workspaceService })
260
+ : undefined;
261
+ if (routing !== undefined) {
262
+ prism.hooks.on("session.closed", (event) => routing.forget(event.sessionId), { id: "workspace-routing" });
263
+ }
264
+
265
+ if (prism.config.forge !== undefined) {
266
+ const ref = prism.config.forge.token;
267
+ const service: CodingForgeService = {
268
+ name: "github",
269
+ resolver: {
270
+ resolve: async () => await prism.credentials.resolve(ref),
271
+ },
272
+ };
273
+ prism.services.provide("coding.forge", service);
274
+ }
275
+
276
+ if (diagnosticsEnabled) {
277
+ const open = new Set<string>();
278
+ prism.hooks.on(
279
+ "session.opened",
280
+ (event) => {
281
+ open.add(event.sessionId);
282
+ },
283
+ { id: "language-open" },
284
+ );
285
+ prism.hooks.on(
286
+ "session.closed",
287
+ (event) => {
288
+ open.delete(event.sessionId);
289
+ if (open.size > 0) return;
290
+ for (const handle of diagnosticsByCwd.values()) void handle.dispose();
291
+ },
292
+ { id: "language-servers" },
293
+ );
294
+ }
295
+
296
+ let shellRegistered = false;
297
+ for (const tool of routing?.tools ?? tools) {
298
+ if (!shellAvailable && tool.name === "shell") continue;
299
+ if (tool.name === "shell") shellRegistered = true;
300
+ prism.tools.register(withDurablePrincipal(tool, principalId));
301
+ }
302
+ if (background !== undefined && shellRegistered) {
303
+ // `shell_output` itself is part of the shell tool set; this is only the kill switch.
304
+ prism.hooks.on("session.closed", (event) => background.killSession(event.sessionId), { id: "shell-background" });
305
+ }
306
+ if (sandbox === "none" && shellRegistered) {
307
+ prism.status.note("warn", "shell runs on the host without a sandbox; pair it with @arnilo/prism-extensions/approval or set sandbox");
308
+ }
309
+ },
310
+ });
311
+
312
+ /** The package definition. Pass config through `packageConfig` or use {@link toolsCoding}. */
313
+ export default definition;
314
+
315
+ /** The package with config baked in, for `packages: [toolsCoding({ root })]`. */
316
+ export function toolsCoding(config: ToolsCodingConfig): PackageInstance {
317
+ return definition(config);
318
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Durable-effect principal guard (plan 154 Task 5, decision (f)).
3
+ *
4
+ * A tool whose declared effect is not `none` mutates local or external state. Without a host
5
+ * principal there is no identity to attribute the mutation to, so the call fails closed with
6
+ * `ERR_PRISM_CODING_PRINCIPAL_REQUIRED`. When the package config pins `principal.id`, a call
7
+ * whose `ctx.principal.id` differs fails with `ERR_PRISM_CODING_PRINCIPAL_MISMATCH`. Read-only
8
+ * calls (and the read-only git actions, whose classifier returns `none`) need no principal.
9
+ *
10
+ * Verification of the principal stays with `governance` (Phase 9); this only refuses an
11
+ * unattributable or misattributed durable mutation.
12
+ */
13
+ import type { JsonObject, ToolDefinition, ToolEffectDeclaration, ToolExecutionContext, ToolResult } from "@arnilo/prism";
14
+
15
+ /** A tool with a durable effect ran on an agent that has no host principal; pass `principal` to `createAgent`. */
16
+ export const ERR_PRISM_CODING_PRINCIPAL_REQUIRED = "ERR_PRISM_CODING_PRINCIPAL_REQUIRED" as const;
17
+ /** The call's principal id differs from the `principal.id` the package config pins. */
18
+ export const ERR_PRISM_CODING_PRINCIPAL_MISMATCH = "ERR_PRISM_CODING_PRINCIPAL_MISMATCH" as const;
19
+
20
+ /** Whether one call's declared effect is durable. A classifier that throws fails closed. */
21
+ export function isDurableCall(tool: ToolDefinition, args: JsonObject, context: ToolExecutionContext): boolean {
22
+ const effect = tool.effect;
23
+ if (effect === undefined) return false;
24
+ if (typeof effect === "function") {
25
+ try {
26
+ return effect(args, context).kind !== "none";
27
+ } catch {
28
+ return true;
29
+ }
30
+ }
31
+ return (effect as ToolEffectDeclaration).kind !== "none";
32
+ }
33
+
34
+ function principalError(toolCallId: string, toolName: string, code: string, message: string): ToolResult {
35
+ return {
36
+ toolCallId,
37
+ name: toolName,
38
+ content: [{ type: "text", text: message }],
39
+ error: { code, message },
40
+ };
41
+ }
42
+
43
+ /**
44
+ * The error result for a durable call without a matching principal, or `undefined` when the call
45
+ * may proceed. `expectedId` is the package-configured `principal.id`, when set.
46
+ */
47
+ export function durablePrincipalError(
48
+ tool: ToolDefinition,
49
+ args: JsonObject,
50
+ context: ToolExecutionContext,
51
+ expectedId?: string,
52
+ ): ToolResult | undefined {
53
+ if (!isDurableCall(tool, args, context)) return undefined;
54
+ const principal = context.principal;
55
+ if (principal === undefined || typeof principal.id !== "string" || principal.id.length === 0) {
56
+ return principalError(
57
+ context.toolCallId,
58
+ tool.name,
59
+ ERR_PRISM_CODING_PRINCIPAL_REQUIRED,
60
+ `${tool.name} has a durable effect and requires a principal`,
61
+ );
62
+ }
63
+ if (expectedId !== undefined && principal.id !== expectedId) {
64
+ return principalError(
65
+ context.toolCallId,
66
+ tool.name,
67
+ ERR_PRISM_CODING_PRINCIPAL_MISMATCH,
68
+ `${tool.name} principal does not match the configured principal`,
69
+ );
70
+ }
71
+ return undefined;
72
+ }
73
+
74
+ /** Wrap a tool so a durable call fails closed before it executes. Non-durable calls pass through. */
75
+ export function withDurablePrincipal(tool: ToolDefinition, expectedId?: string): ToolDefinition {
76
+ return {
77
+ ...tool,
78
+ async execute(args, context): Promise<ToolResult> {
79
+ const error = durablePrincipalError(tool, args, context, expectedId);
80
+ if (error !== undefined) return error;
81
+ return await tool.execute(args, context);
82
+ },
83
+ };
84
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Config-selected sandbox backend for the shell and file operations.
3
+ *
4
+ * `none` (default) runs locally. `native` builds the in-repo `unshare` backend. `e2b` is reached
5
+ * only through `createE2BSandbox`, which imports the optional `e2b` peer dynamically. `docker`
6
+ * needs a host-provided digest-pinned image and runner, which a JSON config cannot carry, so it
7
+ * fails closed. Any backend that cannot be built yields a warning and no `shell` registration —
8
+ * never a throw and never a silent unsandboxed shell.
9
+ */
10
+ import { resolve } from "node:path";
11
+ import type { ToolDefinition } from "@arnilo/prism";
12
+ import type { ProcessSandboxBackend } from "./agent/process/types.js";
13
+ import type { RepositoryLimitOptions } from "./agent/repository/types.js";
14
+ import { createE2BSandbox } from "./security/e2b-sandbox.js";
15
+ import { createNativeSandbox } from "./security/native-sandbox.js";
16
+ import type { SandboxAdapter } from "./security/sandbox.js";
17
+ import { createSandboxCodingComposition, createSandboxReadOnlyComposition } from "./security/sandbox-coding-operations.js";
18
+
19
+ export type CodingSandboxKind = "none" | "native" | "docker" | "e2b";
20
+
21
+ export interface SelectCodingSandboxOptions {
22
+ readonly root: string;
23
+ readonly repository?: RepositoryLimitOptions;
24
+ readonly readOnly: boolean;
25
+ }
26
+
27
+ export interface SelectedCodingSandbox {
28
+ /** Sandbox-backed tools, or absent when the backend could not be built. */
29
+ readonly tools?: readonly ToolDefinition[];
30
+ /** The cwd the tools operate in (`/workspace` for container backends, the host root for native). */
31
+ readonly toolRoot: string;
32
+ /**
33
+ * The same adapter when it can start a long-running process (plan 189 Task 9). Absent means
34
+ * `background` stays out of the `shell` schema and `shell_output` is not registered — the
35
+ * native backend implements one-shot exec only.
36
+ */
37
+ readonly processes?: ProcessSandboxBackend;
38
+ readonly warnings: readonly string[];
39
+ }
40
+
41
+ function message(error: unknown): string {
42
+ return error instanceof Error ? error.message : String(error);
43
+ }
44
+
45
+ async function buildAdapter(kind: Exclude<CodingSandboxKind, "none">, root: string): Promise<SandboxAdapter> {
46
+ if (kind === "native") return createNativeSandbox({ root: resolve(root) });
47
+ if (kind === "e2b") return createE2BSandbox({});
48
+ throw new Error('sandbox "docker" needs a host-provided digest-pinned image and docker runner');
49
+ }
50
+
51
+ /** Build the selected backend. Never throws: a failure becomes a warning and no tools. */
52
+ export async function selectCodingSandbox(
53
+ kind: Exclude<CodingSandboxKind, "none">,
54
+ options: SelectCodingSandboxOptions,
55
+ ): Promise<SelectedCodingSandbox> {
56
+ let adapter: SandboxAdapter;
57
+ try {
58
+ adapter = await buildAdapter(kind, options.root);
59
+ } catch (error) {
60
+ return { toolRoot: resolve(options.root), warnings: [`sandbox "${kind}" unavailable: ${message(error)}; shell is not registered`] };
61
+ }
62
+ // Native's sandbox root IS the host workspace; container backends observe `/workspace`.
63
+ const workspaceRoot = kind === "native" ? resolve(options.root) : undefined;
64
+ try {
65
+ const composition = options.readOnly
66
+ ? createSandboxReadOnlyComposition(options.root, {
67
+ workspaceMode: "sandbox",
68
+ sandbox: adapter,
69
+ ...(workspaceRoot ? { workspaceRoot } : {}),
70
+ repository: options.repository,
71
+ })
72
+ : createSandboxCodingComposition(options.root, {
73
+ workspaceMode: "sandbox",
74
+ sandbox: adapter,
75
+ ...(workspaceRoot ? { workspaceRoot } : {}),
76
+ repository: options.repository,
77
+ });
78
+ const processes =
79
+ typeof (adapter as ProcessSandboxBackend).startProcess === "function" ? (adapter as ProcessSandboxBackend) : undefined;
80
+ return {
81
+ tools: composition.tools,
82
+ toolRoot: workspaceRoot ?? "/workspace",
83
+ ...(processes !== undefined ? { processes } : {}),
84
+ warnings: [...composition.composition.warnings],
85
+ };
86
+ } catch (error) {
87
+ return {
88
+ toolRoot: resolve(options.root),
89
+ warnings: [`sandbox "${kind}" could not be wired: ${message(error)}; shell is not registered`],
90
+ };
91
+ }
92
+ }
@@ -0,0 +1,144 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { ExecutionAction, ExecutionDecision, ExecutionPolicy } from "../agent/execution-policy.js";
3
+ import { type CommandRule, evaluateCommandRules } from "./command-rules.js";
4
+ import { assertPathInsideRoots } from "./path-containment.js";
5
+
6
+ export interface CodingApprovalRequest {
7
+ readonly action: ExecutionAction;
8
+ readonly signal?: AbortSignal;
9
+ }
10
+
11
+ export type CodingApprovalFn = (request: CodingApprovalRequest) => boolean | Promise<boolean>;
12
+
13
+ export type ApprovalCacheScope = "session" | "run" | "none";
14
+
15
+ export interface CodingApprovalPolicyOptions {
16
+ readonly roots: readonly string[];
17
+ readonly approve?: CodingApprovalFn;
18
+ readonly readOnly?: boolean;
19
+ readonly commandRules?: readonly CommandRule[];
20
+ readonly approvalCacheScope?: ApprovalCacheScope;
21
+ readonly approvalTimeoutMs?: number;
22
+ /** When true (default), shell metacharacters require approval. */
23
+ readonly denyMetacharacters?: boolean;
24
+ }
25
+
26
+ const MAX_APPROVAL_CACHE_ENTRIES = 1_000;
27
+
28
+ function isMutatingKind(kind: string): boolean {
29
+ return kind === "shell" || kind === "write" || kind === "edit" || kind === "delete" || kind === "move";
30
+ }
31
+
32
+ function approvalCacheKey(action: ExecutionAction, scope: Exclude<ApprovalCacheScope, "none">): string | undefined {
33
+ const identity = action.metadata?.[scope === "run" ? "runId" : "sessionId"];
34
+ if (typeof identity !== "string" || identity.length === 0) return undefined;
35
+ return createHash("sha256")
36
+ .update(JSON.stringify([scope, identity, action.kind, action.operation, action.paths ?? [], action.command ?? ""]))
37
+ .digest("hex");
38
+ }
39
+
40
+ function cacheApproval(cache: Map<string, boolean>, key: string, approved: boolean): void {
41
+ if (!cache.has(key) && cache.size >= MAX_APPROVAL_CACHE_ENTRIES) {
42
+ cache.delete(cache.keys().next().value!);
43
+ }
44
+ cache.set(key, approved);
45
+ }
46
+
47
+ function readAbortSignal(action: ExecutionAction): AbortSignal | undefined {
48
+ const signal = action.metadata?.signal;
49
+ return signal instanceof AbortSignal ? signal : undefined;
50
+ }
51
+
52
+ async function waitForApproval(approve: CodingApprovalFn, request: CodingApprovalRequest, timeoutMs: number | undefined): Promise<boolean> {
53
+ if (request.signal?.aborted) return false;
54
+
55
+ const approvePromise = Promise.resolve(approve(request));
56
+ if (!timeoutMs || timeoutMs <= 0) return approvePromise;
57
+
58
+ return new Promise<boolean>((resolve) => {
59
+ let settled = false;
60
+ const finish = (value: boolean) => {
61
+ if (settled) return;
62
+ settled = true;
63
+ cleanup();
64
+ resolve(value);
65
+ };
66
+ const onAbort = () => finish(false);
67
+ const timer = setTimeout(() => finish(false), timeoutMs);
68
+ const cleanup = () => {
69
+ clearTimeout(timer);
70
+ request.signal?.removeEventListener("abort", onAbort);
71
+ };
72
+ request.signal?.addEventListener("abort", onAbort, { once: true });
73
+ approvePromise.then(finish, () => finish(false));
74
+ });
75
+ }
76
+
77
+ export function createCodingApprovalPolicy(options: CodingApprovalPolicyOptions): ExecutionPolicy {
78
+ const scope = options.approvalCacheScope ?? "none";
79
+ const cache = scope === "none" ? undefined : new Map<string, boolean>();
80
+
81
+ return {
82
+ async check(action: ExecutionAction): Promise<ExecutionDecision> {
83
+ if (options.roots.length === 0) {
84
+ return { allowed: false, reason: "no trusted roots configured" };
85
+ }
86
+
87
+ if (options.readOnly && action.kind !== "read") {
88
+ return { allowed: false, reason: "read-only mode" };
89
+ }
90
+
91
+ for (const path of action.paths ?? []) {
92
+ if (!(await assertPathInsideRoots(options.roots, path))) {
93
+ return { allowed: false, reason: `path outside trusted roots: ${path}` };
94
+ }
95
+ }
96
+
97
+ let needsApproval = isMutatingKind(action.kind);
98
+
99
+ if (action.kind === "shell" && action.command) {
100
+ const evaluation = evaluateCommandRules(action.command, options.commandRules, {
101
+ denyMetacharacters: options.denyMetacharacters,
102
+ });
103
+ if (evaluation.action === "deny") {
104
+ return {
105
+ allowed: false,
106
+ reason: evaluation.reason ?? "command denied by policy",
107
+ };
108
+ }
109
+ if (evaluation.action === "requireApproval") {
110
+ needsApproval = true;
111
+ }
112
+ }
113
+
114
+ if (needsApproval) {
115
+ if (!options.approve) {
116
+ return { allowed: false, reason: "approval required" };
117
+ }
118
+
119
+ const key = scope === "none" ? undefined : approvalCacheKey(action, scope);
120
+ if (key !== undefined && cache?.has(key)) {
121
+ const cached = cache.get(key);
122
+ if (!cached) {
123
+ return { allowed: false, reason: "approval denied (cached)" };
124
+ }
125
+ } else {
126
+ const approved = await waitForApproval(
127
+ options.approve,
128
+ { action, signal: readAbortSignal(action) },
129
+ options.approvalTimeoutMs ?? 30_000,
130
+ );
131
+ if (key !== undefined && cache) cacheApproval(cache, key, approved);
132
+ if (!approved) {
133
+ return { allowed: false, reason: "approval denied" };
134
+ }
135
+ }
136
+ }
137
+
138
+ return {
139
+ allowed: true,
140
+ exclusive: action.kind === "shell",
141
+ };
142
+ },
143
+ };
144
+ }