@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,222 @@
1
+ /**
2
+ * Shared mutable registry for managed process sessions.
3
+ * Lifted out of the createProcessSessions closure so phase modules take it explicitly.
4
+ */
5
+ import type { ChildProcessWithoutNullStreams } from "node:child_process";
6
+ import { createHash } from "node:crypto";
7
+ import { isAbsolute, relative, resolve } from "node:path";
8
+ import { DEFAULT_MAX_TERMINAL_COLUMNS, DEFAULT_MAX_TERMINAL_ROWS } from "../limits.js";
9
+ import type { OutputAccumulator } from "../output-accumulator.js";
10
+ import { type ProcessRecoveryBackend, ProcessRecoveryError, resolveProcessRecoveryLimits } from "./recovery.js";
11
+ import {
12
+ type CodingProcessEvent,
13
+ type CreateProcessSessionsOptions,
14
+ type ProcessExitResult,
15
+ type ProcessPtyHandle,
16
+ type ProcessSandboxHandle,
17
+ type ProcessSession,
18
+ ProcessSessionError,
19
+ type ProcessSessionState,
20
+ type ProcessTerminalRequest,
21
+ type ResolvedProcessSessionLimits,
22
+ resolveProcessSessionLimits,
23
+ } from "./types.js";
24
+
25
+ /** Default TERM for PTY sessions (validated <= maxTerminalTermBytes). */
26
+ export const DEFAULT_TERM = "xterm-256color";
27
+
28
+ export function ownershipKey(
29
+ ownership: CreateProcessSessionsOptions["ownership"],
30
+ identity: CreateProcessSessionsOptions["identity"],
31
+ ): string {
32
+ if (ownership) {
33
+ return `${ownership.tenantId ?? ""}:${ownership.accountId ?? ""}:${ownership.userId ?? ""}`;
34
+ }
35
+ if (identity) {
36
+ return `${identity.tenantId}:${identity.accountId ?? ""}:${identity.userId ?? ""}`;
37
+ }
38
+ return "default";
39
+ }
40
+
41
+ export function commandFingerprint(command: string, args: readonly string[]): string {
42
+ return createHash("sha256")
43
+ .update(JSON.stringify([command, ...args]))
44
+ .digest("hex");
45
+ }
46
+
47
+ export function isInsideRoot(root: string, target: string): boolean {
48
+ const from = resolve(root);
49
+ const to = resolve(target);
50
+ if (to === from) return true;
51
+ const rel = relative(from, to);
52
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
53
+ }
54
+
55
+ export function nowIso(): string {
56
+ return new Date().toISOString();
57
+ }
58
+
59
+ export interface SessionRecord {
60
+ id: string;
61
+ owner: string;
62
+ workspace: string;
63
+ command: string;
64
+ args: readonly string[];
65
+ commandFingerprint: string;
66
+ policyDecision: string;
67
+ startedAt: string;
68
+ exitedAt?: string;
69
+ releaseOnCancel: boolean;
70
+ expiresAt: number;
71
+ state: ProcessSessionState;
72
+ exitCode: number | null;
73
+ child?: ChildProcessWithoutNullStreams;
74
+ backend?: ProcessSandboxHandle;
75
+ pty?: ProcessPtyHandle;
76
+ ptyTerminal?: { columns: number; rows: number; term: string };
77
+ ptyResizeAt: number[];
78
+ pid?: number;
79
+ accumulator: OutputAccumulator;
80
+ waiters: Array<(result: ProcessExitResult) => void>;
81
+ /** Readers waiting for the next publication (`ProcessSession.onOutput`). */
82
+ outputListeners: Set<() => void>;
83
+ stdinClosed: boolean;
84
+ handle: ProcessSession;
85
+ /** Durable recovery bookkeeping (plan 026 Task 5); absent when durability is not configured. */
86
+ backendRef?: string;
87
+ recoveryFencingToken: number;
88
+ recoveryVersion: number;
89
+ /** Opaque lease claim token for renewal/release; never serialized, memory only. */
90
+ recoveryLeaseToken?: string;
91
+ /** Serializes durable transition writes per record so CAS order never inverts on slow stores. */
92
+ recoveryWriteChain?: Promise<void>;
93
+ }
94
+
95
+ export interface SessionsHost {
96
+ readonly workspace: string;
97
+ readonly limits: ResolvedProcessSessionLimits;
98
+ readonly defaultOwner: string;
99
+ readonly policy: CreateProcessSessionsOptions["policy"];
100
+ readonly onEvent: CreateProcessSessionsOptions["onEvent"];
101
+ readonly sandbox: CreateProcessSessionsOptions["sandbox"];
102
+ readonly ptyBackend: CreateProcessSessionsOptions["ptyBackend"];
103
+ readonly ptyResizeCapable: boolean;
104
+ readonly sessions: Map<string, SessionRecord>;
105
+ disposed: boolean;
106
+ sandboxLost: boolean;
107
+ readonly checkpoints: CreateProcessSessionsOptions["checkpoints"];
108
+ readonly leases: CreateProcessSessionsOptions["leases"];
109
+ readonly ownerId: CreateProcessSessionsOptions["ownerId"];
110
+ readonly recoveryBackend: ProcessRecoveryBackend | undefined;
111
+ readonly recoveryLimits: ReturnType<typeof resolveProcessRecoveryLimits>;
112
+ readonly durable: boolean;
113
+ readonly ownership: CreateProcessSessionsOptions["ownership"];
114
+ }
115
+
116
+ export function createSessionsHost(options: CreateProcessSessionsOptions): SessionsHost {
117
+ const checkpoints = options.checkpoints;
118
+ const leases = options.leases;
119
+ const ownerId = options.ownerId;
120
+ const recoveryBackend =
121
+ options.recoveryBackend ??
122
+ (options.sandbox && typeof options.sandbox.attachProcess === "function"
123
+ ? { attach: (ref: string) => options.sandbox!.attachProcess!(ref) }
124
+ : undefined);
125
+ const durable = checkpoints !== undefined || leases !== undefined || ownerId !== undefined || recoveryBackend !== undefined;
126
+ if (durable && !(checkpoints && leases && ownerId)) {
127
+ throw new ProcessRecoveryError(
128
+ "ERR_PRISM_RECOVERY_UNSUPPORTED",
129
+ "durable process recovery requires checkpoints, leases, and ownerId together",
130
+ );
131
+ }
132
+ return {
133
+ workspace: resolve(options.cwd),
134
+ limits: resolveProcessSessionLimits(options.limits),
135
+ defaultOwner: ownershipKey(options.ownership, options.identity),
136
+ policy: options.policy,
137
+ onEvent: options.onEvent,
138
+ sandbox: options.sandbox,
139
+ ptyBackend: options.ptyBackend,
140
+ ptyResizeCapable: options.ptyBackend?.capabilities?.resize === true,
141
+ sessions: new Map(),
142
+ disposed: false,
143
+ sandboxLost: false,
144
+ checkpoints,
145
+ leases,
146
+ ownerId,
147
+ recoveryBackend,
148
+ recoveryLimits: resolveProcessRecoveryLimits(options.recoveryLimits),
149
+ durable,
150
+ ownership: options.ownership,
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Durable recovery seams. `createSessionsHost` already fails closed when a durable host
156
+ * is missing any of the three, so callers under a `host.durable` guard can take them here
157
+ * without non-null assertions — and a host that somehow lost one fails closed again.
158
+ */
159
+ export function durableSeams(host: SessionsHost): {
160
+ readonly checkpoints: NonNullable<SessionsHost["checkpoints"]>;
161
+ readonly leases: NonNullable<SessionsHost["leases"]>;
162
+ readonly ownerId: NonNullable<SessionsHost["ownerId"]>;
163
+ } {
164
+ const { checkpoints, leases, ownerId } = host;
165
+ if (!checkpoints || !leases || !ownerId) {
166
+ throw new ProcessRecoveryError(
167
+ "ERR_PRISM_RECOVERY_UNSUPPORTED",
168
+ "durable process recovery requires checkpoints, leases, and ownerId together",
169
+ );
170
+ }
171
+ return { checkpoints, leases, ownerId };
172
+ }
173
+
174
+ export function emit(host: SessionsHost, event: CodingProcessEvent): void {
175
+ host.onEvent?.(event);
176
+ }
177
+
178
+ export function assertNotDisposed(host: SessionsHost): void {
179
+ if (host.disposed) throw new ProcessSessionError("ERR_PRISM_PROCESS_STATE", "process sessions disposed");
180
+ }
181
+
182
+ export function isTerminalState(state: ProcessSessionState): boolean {
183
+ return state === "exited" || state === "killed" || state === "released" || state === "expired" || state === "unknown";
184
+ }
185
+
186
+ /** Wakes every reader waiting on this record: a chunk landed, or the session went terminal. */
187
+ export function notifyOutput(record: SessionRecord): void {
188
+ for (const listener of [...record.outputListeners]) {
189
+ try {
190
+ listener();
191
+ } catch {
192
+ // swallowed on purpose: a reader must never fail the process that produced the output
193
+ }
194
+ }
195
+ }
196
+
197
+ export function settleWaiters(record: SessionRecord): void {
198
+ const result: ProcessExitResult = { exitCode: record.exitCode, state: record.state };
199
+ const waiters = record.waiters;
200
+ record.waiters = [];
201
+ for (const resolveWait of waiters) resolveWait(result);
202
+ notifyOutput(record);
203
+ }
204
+
205
+ export function resolveTerminal(
206
+ host: SessionsHost,
207
+ terminal: ProcessTerminalRequest | undefined,
208
+ ): { columns: number; rows: number; term: string } {
209
+ const columns = terminal?.columns ?? Math.min(DEFAULT_MAX_TERMINAL_COLUMNS, host.limits.maxTerminalColumns);
210
+ const rows = terminal?.rows ?? Math.min(DEFAULT_MAX_TERMINAL_ROWS, host.limits.maxTerminalRows);
211
+ const term = terminal?.term ?? DEFAULT_TERM;
212
+ if (!Number.isSafeInteger(columns) || columns < 1 || columns > host.limits.maxTerminalColumns) {
213
+ throw new ProcessSessionError("ERR_PRISM_PROCESS_PTY_LIMIT", `columns must be 1..${host.limits.maxTerminalColumns}`);
214
+ }
215
+ if (!Number.isSafeInteger(rows) || rows < 1 || rows > host.limits.maxTerminalRows) {
216
+ throw new ProcessSessionError("ERR_PRISM_PROCESS_PTY_LIMIT", `rows must be 1..${host.limits.maxTerminalRows}`);
217
+ }
218
+ if (Buffer.byteLength(term, "utf8") > host.limits.maxTerminalTermBytes) {
219
+ throw new ProcessSessionError("ERR_PRISM_PROCESS_PTY_LIMIT", `term exceeds maxTerminalTermBytes (${host.limits.maxTerminalTermBytes})`);
220
+ }
221
+ return { columns, rows, term };
222
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Durable persist / CAS transitions for managed process sessions.
3
+ */
4
+
5
+ import type { ProcessRecoveryRecord } from "./recovery.js";
6
+ import {
7
+ buildProcessRecoveryRecord,
8
+ deleteProcessRecoveryRecord,
9
+ loadProcessRecoveryRecords,
10
+ PROCESS_RECOVERY_LEASE_NAMESPACE,
11
+ releaseRecordLease,
12
+ saveProcessRecoveryRecord,
13
+ } from "./recovery.js";
14
+ import { durableSeams, isTerminalState, nowIso, type SessionRecord, type SessionsHost } from "./sessions-host.js";
15
+
16
+ // Durable transition write: fire-and-forget CAS (fence/version conflicts mean
17
+ // another replica moved the record first — the newer state wins). The crash
18
+ // window between a terminal transition and its durable write converges on
19
+ // recovery to attach/terminal/unknown, never a duplicate spawn.
20
+ export function persistTransition(host: SessionsHost, record: SessionRecord): void {
21
+ if (!host.durable || record.recoveryVersion === 0) return;
22
+ const seams = durableSeams(host);
23
+ const next = buildProcessRecoveryRecord({
24
+ id: record.id,
25
+ owner: record.owner,
26
+ workspace: record.workspace,
27
+ command: record.command,
28
+ args: record.args,
29
+ commandFingerprint: record.commandFingerprint,
30
+ policyDecision: record.policyDecision,
31
+ startedAt: record.startedAt,
32
+ state: record.state,
33
+ exitCode: record.exitCode,
34
+ releaseOnCancel: record.releaseOnCancel,
35
+ expiresAt: record.expiresAt,
36
+ ...(record.backendRef !== undefined ? { backendRef: record.backendRef } : {}),
37
+ ...(record.ptyTerminal !== undefined ? { pty: record.ptyTerminal } : {}),
38
+ fencingToken: record.recoveryFencingToken,
39
+ });
40
+ const expectedVersion = record.recoveryVersion;
41
+ record.recoveryVersion += 1;
42
+ const write = async (): Promise<void> => {
43
+ try {
44
+ await saveProcessRecoveryRecord({
45
+ checkpoints: seams.checkpoints,
46
+ record: next,
47
+ expectedVersion,
48
+ version: record.recoveryVersion,
49
+ ownership: host.ownership,
50
+ });
51
+ } catch {
52
+ // stale fence or store failure: the durable record keeps its last state
53
+ return;
54
+ }
55
+ void evictRecoveryOverflow(host);
56
+ // Terminal transition: release the record lease (clean shutdown makes
57
+ // recovery immediate). Live running/starting transitions renew it so a
58
+ // crashed replica's lease lapses within TTL while a live one stays
59
+ // fenced. Best effort on both.
60
+ if (isTerminalState(record.state)) {
61
+ if (record.recoveryLeaseToken) {
62
+ void releaseRecordLease({
63
+ leases: seams.leases,
64
+ id: record.id,
65
+ ownerId: seams.ownerId,
66
+ token: record.recoveryLeaseToken,
67
+ ownership: host.ownership,
68
+ });
69
+ record.recoveryLeaseToken = undefined;
70
+ }
71
+ } else if (record.recoveryLeaseToken) {
72
+ void seams.leases
73
+ .renewLease({
74
+ namespace: PROCESS_RECOVERY_LEASE_NAMESPACE,
75
+ key: `recover:${record.id}`,
76
+ ownerId: seams.ownerId,
77
+ token: record.recoveryLeaseToken,
78
+ ttlMs: host.recoveryLimits.leaseTtlMs,
79
+ ...host.ownership,
80
+ })
81
+ .catch(() => {
82
+ // lease lapsed or fenced elsewhere; recovery is attestation-gated
83
+ });
84
+ }
85
+ };
86
+ // Per-record chain: transition CAS writes must never invert order on slow
87
+ // stores (a later terminal write landing before the running write would
88
+ // fail its CAS and leave the record running forever).
89
+ record.recoveryWriteChain = (record.recoveryWriteChain ?? Promise.resolve()).then(write, write);
90
+ }
91
+
92
+ // Bound durable record growth: after a terminal transition, drop the oldest
93
+ // terminal records until at most maxRecords remain (running/starting records
94
+ // are never evicted). Best effort, bounded work.
95
+ export async function evictRecoveryOverflow(host: SessionsHost): Promise<void> {
96
+ if (!host.durable) return;
97
+ const seams = durableSeams(host);
98
+ try {
99
+ const page = await loadProcessRecoveryRecords({
100
+ checkpoints: seams.checkpoints,
101
+ limits: { ...host.recoveryLimits, maxRecords: host.recoveryLimits.maxRecords + 1 },
102
+ ownership: host.ownership,
103
+ });
104
+ if (page.records.length <= host.recoveryLimits.maxRecords) return;
105
+ const overflow = page.records.length - host.recoveryLimits.maxRecords;
106
+ let evicted = 0;
107
+ for (const { record } of [...page.records].reverse()) {
108
+ if (evicted >= overflow) break;
109
+ if (record.state === "running" || record.state === "starting") continue;
110
+ await deleteProcessRecoveryRecord({ checkpoints: seams.checkpoints, id: record.id, ownership: host.ownership });
111
+ evicted += 1;
112
+ }
113
+ } catch {
114
+ // best effort: caps are enforced again on the next transition
115
+ }
116
+ }
117
+
118
+ // Atomic starting|running -> unknown (never an exit code). Returns false when
119
+ // a fence/version conflict means another replica moved the record first.
120
+ export async function persistRecoveryUnknown(
121
+ host: SessionsHost,
122
+ current: ProcessRecoveryRecord,
123
+ version: number,
124
+ signal?: AbortSignal,
125
+ ): Promise<boolean> {
126
+ const next = buildProcessRecoveryRecord({ ...current, state: "unknown", exitCode: null, updatedAt: nowIso() });
127
+ const seams = durableSeams(host);
128
+ try {
129
+ await saveProcessRecoveryRecord({
130
+ checkpoints: seams.checkpoints,
131
+ record: next,
132
+ expectedVersion: version,
133
+ version: version + 1,
134
+ ownership: host.ownership,
135
+ signal,
136
+ });
137
+ return true;
138
+ } catch {
139
+ return false;
140
+ }
141
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Durable recover / reattach phase for managed process sessions.
3
+ */
4
+ import { OutputAccumulator } from "../output-accumulator.js";
5
+ import {
6
+ acquireRecordLease,
7
+ attachWithTimeout,
8
+ buildProcessRecoveryRecord,
9
+ loadProcessRecoveryRecord,
10
+ loadProcessRecoveryRecords,
11
+ ProcessRecoveryError,
12
+ type ProcessRecoveryRecord,
13
+ type ProcessRecoveryRecordReport,
14
+ type ProcessRecoveryReport,
15
+ releaseRecordLease,
16
+ saveProcessRecoveryRecord,
17
+ } from "./recovery.js";
18
+ import { assertNotDisposed, durableSeams, isTerminalState, nowIso, type SessionRecord, type SessionsHost } from "./sessions-host.js";
19
+ import { persistRecoveryUnknown } from "./sessions-monitor.js";
20
+ import { makeHandle } from "./sessions-spawn.js";
21
+ import { sweepExpired, terminateRecord } from "./sessions-teardown.js";
22
+ import type { ProcessPtyHandle, ProcessSandboxHandle, ProcessSession } from "./types.js";
23
+
24
+ // Reattach an attested handle into the live registry. Recovered sessions
25
+ // expose control (input/signal/kill/release/resize/wait) through the handle;
26
+ // output streaming is not re-established after a restart — the host backend
27
+ // owns any buffered output behind its opaque ref.
28
+ export function attachRecoveredHandle(
29
+ host: SessionsHost,
30
+ current: ProcessRecoveryRecord,
31
+ handle: ProcessPtyHandle | ProcessSandboxHandle,
32
+ version: number,
33
+ ): void {
34
+ const accumulator = new OutputAccumulator({
35
+ maxBytes: host.limits.maxOutputChunkBytes,
36
+ maxLines: 100_000,
37
+ maxTotalOutputBytes: host.limits.maxTotalOutputBytes,
38
+ tempFilePrefix: "prism-proc",
39
+ });
40
+ const record: SessionRecord = {
41
+ id: current.id,
42
+ owner: current.owner,
43
+ workspace: current.workspace,
44
+ command: current.command,
45
+ args: current.args,
46
+ commandFingerprint: current.commandFingerprint,
47
+ policyDecision: current.policyDecision,
48
+ startedAt: current.startedAt,
49
+ releaseOnCancel: current.releaseOnCancel,
50
+ expiresAt: current.expiresAt,
51
+ state: "running",
52
+ exitCode: null,
53
+ ptyTerminal: current.pty,
54
+ ptyResizeAt: [],
55
+ accumulator,
56
+ waiters: [],
57
+ outputListeners: new Set(),
58
+ stdinClosed: false,
59
+ handle: null as unknown as ProcessSession,
60
+ backendRef: current.backendRef,
61
+ recoveryFencingToken: current.fencingToken,
62
+ recoveryVersion: version,
63
+ };
64
+ if (current.pty !== undefined) {
65
+ record.pty = handle as ProcessPtyHandle;
66
+ } else {
67
+ record.backend = handle as ProcessSandboxHandle;
68
+ }
69
+ record.handle = makeHandle(host, record);
70
+ host.sessions.set(record.id, record);
71
+ void handle
72
+ .wait()
73
+ .then((result) => {
74
+ if (record.state !== "running" && record.state !== "starting") return;
75
+ terminateRecord(host, record, "exited", result.exitCode);
76
+ })
77
+ .catch(() => {
78
+ if (record.state !== "running" && record.state !== "starting") return;
79
+ terminateRecord(host, record, "unknown", null);
80
+ });
81
+ }
82
+
83
+ export async function recoverSessions(host: SessionsHost, recoverOptions?: { signal?: AbortSignal }): Promise<ProcessRecoveryReport> {
84
+ assertNotDisposed(host);
85
+ if (!host.durable) {
86
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNSUPPORTED", "durable process recovery is not configured on this host");
87
+ }
88
+ const seams = durableSeams(host);
89
+ sweepExpired(host);
90
+ const { records } = await loadProcessRecoveryRecords({
91
+ checkpoints: seams.checkpoints,
92
+ limits: host.recoveryLimits,
93
+ ownership: host.ownership,
94
+ signal: recoverOptions?.signal,
95
+ });
96
+ const report: ProcessRecoveryRecordReport[] = [];
97
+ let attached = 0;
98
+ let terminal = 0;
99
+ let unknown = 0;
100
+ for (const { record } of records) {
101
+ const live = host.sessions.get(record.id);
102
+ if (live) {
103
+ // Already live in this registry: nothing to do, report the truth.
104
+ report.push({ id: record.id, outcome: "attached", state: live.state, exitCode: live.exitCode });
105
+ if (live.state === "running" || live.state === "starting") attached += 1;
106
+ else terminal += 1;
107
+ continue;
108
+ }
109
+ if (
110
+ record.state === "exited" ||
111
+ record.state === "killed" ||
112
+ record.state === "released" ||
113
+ record.state === "expired" ||
114
+ record.state === "unknown"
115
+ ) {
116
+ report.push({ id: record.id, outcome: "terminal", state: record.state, exitCode: record.exitCode });
117
+ terminal += 1;
118
+ continue;
119
+ }
120
+ // starting | running durable record without a live handle: attach-if-
121
+ // attested, else atomic unknown. Never fabricate an exit code.
122
+ const lease = await acquireRecordLease({
123
+ leases: seams.leases,
124
+ id: record.id,
125
+ ownerId: seams.ownerId,
126
+ ttlMs: host.recoveryLimits.leaseTtlMs,
127
+ ownership: host.ownership,
128
+ signal: recoverOptions?.signal,
129
+ });
130
+ if (!lease) {
131
+ // Another replica owns or is recovering this record.
132
+ report.push({ id: record.id, outcome: "unknown", state: record.state, exitCode: null });
133
+ unknown += 1;
134
+ continue;
135
+ }
136
+ try {
137
+ // Fresh CAS read under the lease: another replica may have moved the record.
138
+ const fresh = await loadProcessRecoveryRecord({
139
+ checkpoints: seams.checkpoints,
140
+ id: record.id,
141
+ limits: host.recoveryLimits,
142
+ ownership: host.ownership,
143
+ signal: recoverOptions?.signal,
144
+ });
145
+ if (!fresh) {
146
+ report.push({ id: record.id, outcome: "terminal", state: "unknown", exitCode: null, error: "ERR_PRISM_RECOVERY_UNKNOWN" });
147
+ terminal += 1;
148
+ continue;
149
+ }
150
+ const current = fresh.record;
151
+ if (
152
+ current.state === "exited" ||
153
+ current.state === "killed" ||
154
+ current.state === "released" ||
155
+ current.state === "expired" ||
156
+ current.state === "unknown"
157
+ ) {
158
+ report.push({ id: current.id, outcome: "terminal", state: current.state, exitCode: current.exitCode });
159
+ terminal += 1;
160
+ continue;
161
+ }
162
+ if (Date.now() >= current.expiresAt) {
163
+ // Lifetime lapsed while unrecovered: persist the expiry.
164
+ const expired = buildProcessRecoveryRecord({
165
+ ...current,
166
+ state: "expired",
167
+ exitCode: null,
168
+ updatedAt: nowIso(),
169
+ });
170
+ await saveProcessRecoveryRecord({
171
+ checkpoints: seams.checkpoints,
172
+ record: expired,
173
+ expectedVersion: fresh.version,
174
+ version: fresh.version + 1,
175
+ ownership: host.ownership,
176
+ signal: recoverOptions?.signal,
177
+ });
178
+ report.push({ id: current.id, outcome: "terminal", state: "expired", exitCode: null });
179
+ terminal += 1;
180
+ continue;
181
+ }
182
+ const active = [...host.sessions.values()].filter((s) => s.state === "running" || s.state === "starting").length;
183
+ if (active >= host.limits.maxSessions) {
184
+ // Cannot admit another live session: record unknown (fails closed).
185
+ await persistRecoveryUnknown(host, current, fresh.version, recoverOptions?.signal);
186
+ report.push({ id: current.id, outcome: "unknown", state: "unknown", exitCode: null });
187
+ unknown += 1;
188
+ continue;
189
+ }
190
+ let attachErrorCode: ProcessRecoveryRecordReport["error"];
191
+ if (current.backendRef !== undefined && host.recoveryBackend) {
192
+ try {
193
+ const handle = await attachWithTimeout(host.recoveryBackend, current.backendRef, host.recoveryLimits.attachTimeoutMs);
194
+ if (handle) {
195
+ attachRecoveredHandle(host, current, handle, fresh.version);
196
+ attached += 1;
197
+ report.push({ id: current.id, outcome: "attached", state: "running", exitCode: null });
198
+ continue;
199
+ }
200
+ } catch (attachError) {
201
+ attachErrorCode = attachError instanceof ProcessRecoveryError ? attachError.code : ("ERR_PRISM_RECOVERY_UNKNOWN" as const);
202
+ }
203
+ }
204
+ // No ref, no backend, unattested attach, or attach failure: atomic unknown.
205
+ const saved = await persistRecoveryUnknown(host, current, fresh.version, recoverOptions?.signal);
206
+ if (!saved) {
207
+ // CAS/fence conflict: another replica moved the record; re-report its state.
208
+ const again = await loadProcessRecoveryRecord({
209
+ checkpoints: seams.checkpoints,
210
+ id: current.id,
211
+ limits: host.recoveryLimits,
212
+ ownership: host.ownership,
213
+ signal: recoverOptions?.signal,
214
+ });
215
+ const reported = again?.record;
216
+ report.push({
217
+ id: current.id,
218
+ outcome: reported && isTerminalState(reported.state) ? "terminal" : "unknown",
219
+ state: reported?.state ?? "unknown",
220
+ exitCode: reported?.exitCode ?? null,
221
+ error: attachErrorCode,
222
+ });
223
+ if (reported && isTerminalState(reported.state)) terminal += 1;
224
+ else unknown += 1;
225
+ continue;
226
+ }
227
+ report.push({ id: current.id, outcome: "unknown", state: "unknown", exitCode: null, error: attachErrorCode });
228
+ unknown += 1;
229
+ } finally {
230
+ await releaseRecordLease({
231
+ leases: seams.leases,
232
+ id: record.id,
233
+ ownerId: seams.ownerId,
234
+ token: lease.token,
235
+ ownership: host.ownership,
236
+ signal: recoverOptions?.signal,
237
+ });
238
+ }
239
+ }
240
+ return { records: report, attached, terminal, unknown };
241
+ }