@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,235 @@
1
+ import { type ChildProcess, spawn } from "node:child_process";
2
+ import { statSync } from "node:fs";
3
+ import { delimiter, isAbsolute, join } from "node:path";
4
+ import { setTimeout as delay } from "node:timers/promises";
5
+ import { ObscuraError } from "./errors.js";
6
+ import { type ObscuraProcessLimits, resolveObscuraProcessLimits } from "./limits.js";
7
+ import type { ObscuraCloseOptions, ObscuraExit, ObscuraProcessOptions, OwnedObscuraProcess } from "./types.js";
8
+
9
+ const INSECURE_FLAGS = new Set(["--allow-private-network", "--allow-file-access"]);
10
+ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "::1", "[::1]", "localhost"]);
11
+ const ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
12
+
13
+ /**
14
+ * Resolve a configured Obscura command to an absolute executable path. An absolute path is used
15
+ * as-is only when it is an executable file; a bare name is resolved against absolute `PATH` entries
16
+ * only (relative entries are never trusted). Returns `undefined` when it cannot be resolved.
17
+ */
18
+ export function resolveObscuraCommand(name: string): string | undefined {
19
+ if (name.length === 0) return undefined;
20
+ const executable = (path: string): boolean => {
21
+ try {
22
+ const stat = statSync(path);
23
+ if (!stat.isFile()) return false;
24
+ return process.platform === "win32" || (stat.mode & 0o111) !== 0;
25
+ } catch {
26
+ return false;
27
+ }
28
+ };
29
+ if (isAbsolute(name)) return executable(name) ? name : undefined;
30
+ if (name.includes("/") || name.includes("\\")) return undefined;
31
+ const pathExt = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [];
32
+ for (const dir of (process.env.PATH ?? "").split(delimiter)) {
33
+ if (!dir || !isAbsolute(dir)) continue;
34
+ const candidates = pathExt.length > 0 ? pathExt.map((ext) => join(dir, `${name}${ext.toLowerCase()}`)) : [join(dir, name)];
35
+ for (const candidate of candidates) if (executable(candidate)) return candidate;
36
+ }
37
+ return undefined;
38
+ }
39
+
40
+ function byteLength(value: string): number {
41
+ return Buffer.byteLength(value, "utf8");
42
+ }
43
+
44
+ /** Fail-closed config validation: no NUL bytes, bounded sizes, no implicit insecure flags. */
45
+ export function validateObscuraCommand(options: ObscuraProcessOptions, limits: ObscuraProcessLimits): void {
46
+ if (!options.command || options.command.includes("\0") || !isAbsolute(options.command) || options.command.length > 4096) {
47
+ throw new ObscuraError("ERR_OBSCURA_INPUT", "Absolute, NUL-free command path required");
48
+ }
49
+ const args = options.args ?? [];
50
+ if (args.length > limits.maxArgvEntries) {
51
+ throw new ObscuraError("ERR_OBSCURA_LIMIT", `argv exceeds ${limits.maxArgvEntries} entries`);
52
+ }
53
+ let argvBytes = 0;
54
+ for (const arg of args) {
55
+ if (typeof arg !== "string" || arg.includes("\0")) {
56
+ throw new ObscuraError("ERR_OBSCURA_INPUT", "argv entries must be NUL-free strings");
57
+ }
58
+ argvBytes += byteLength(arg);
59
+ }
60
+ if (argvBytes > limits.maxArgvBytes) {
61
+ throw new ObscuraError("ERR_OBSCURA_LIMIT", "argv exceeds byte cap");
62
+ }
63
+ if (!options.allowInsecureFlags) {
64
+ for (let i = 0; i < args.length; i += 1) {
65
+ const arg = args[i];
66
+ if (INSECURE_FLAGS.has(arg)) {
67
+ throw new ObscuraError("ERR_OBSCURA_INSECURE_FLAG", `${arg} requires explicit allowInsecureFlags`);
68
+ }
69
+ // `--host VALUE` or `--host=VALUE`; non-loopback bind needs explicit opt-in.
70
+ const host = arg === "--host" ? args[i + 1] : arg.startsWith("--host=") ? arg.slice("--host=".length) : undefined;
71
+ if (host !== undefined && !LOOPBACK_HOSTS.has(host.toLowerCase())) {
72
+ throw new ObscuraError(
73
+ "ERR_OBSCURA_INSECURE_FLAG",
74
+ `non-loopback --host ${JSON.stringify(host)} requires explicit allowInsecureFlags`,
75
+ );
76
+ }
77
+ }
78
+ }
79
+ const env = options.env ?? {};
80
+ const entries = Object.entries(env);
81
+ if (entries.length > limits.maxEnvEntries) {
82
+ throw new ObscuraError("ERR_OBSCURA_LIMIT", `env exceeds ${limits.maxEnvEntries} entries`);
83
+ }
84
+ for (const [key, value] of entries) {
85
+ if (!ENV_KEY_PATTERN.test(key) || key.includes("\0") || value.includes("\0") || byteLength(value) > limits.maxEnvValueBytes) {
86
+ throw new ObscuraError("ERR_OBSCURA_INPUT", `invalid environment entry ${JSON.stringify(key)}`);
87
+ }
88
+ }
89
+ if (options.cwd !== undefined && (!isAbsolute(options.cwd) || options.cwd.includes("\0"))) {
90
+ throw new ObscuraError("ERR_OBSCURA_INPUT", "cwd must be an absolute NUL-free path");
91
+ }
92
+ if (options.stderr !== undefined && options.stderr !== "pipe" && options.stderr !== "ignore") {
93
+ throw new ObscuraError("ERR_OBSCURA_INPUT", "stderr must be 'pipe' or 'ignore'");
94
+ }
95
+ }
96
+
97
+ /** Minimal environment: never inherit the full host environment implicitly. */
98
+ function baseEnv(explicit: Readonly<Record<string, string>> | undefined): Record<string, string> {
99
+ const base: Record<string, string> = {};
100
+ for (const key of ["PATH", "HOME"] as const) {
101
+ const value = process.env[key];
102
+ if (value !== undefined) base[key] = value;
103
+ }
104
+ return { ...base, ...(explicit ? { ...explicit } : {}) };
105
+ }
106
+
107
+ function killTree(child: ChildProcess, sig: NodeJS.Signals, groupKill: boolean): void {
108
+ // obscura serve spawns worker children; on POSIX kill the whole process group.
109
+ if (child.pid !== undefined && groupKill) {
110
+ try {
111
+ process.kill(-child.pid, sig);
112
+ return;
113
+ } catch {
114
+ // group already gone; fall through to direct kill
115
+ }
116
+ }
117
+ try {
118
+ child.kill(sig);
119
+ } catch {
120
+ // already exited
121
+ }
122
+ }
123
+
124
+ class OwnedProcess implements OwnedObscuraProcess {
125
+ readonly child: ChildProcess;
126
+ private readonly groupKill = process.platform !== "win32";
127
+ private stderr = Buffer.alloc(0);
128
+ private stderrTruncated = false;
129
+ private closePromise: Promise<void> | undefined;
130
+ private readonly exitPromise: Promise<ObscuraExit>;
131
+
132
+ constructor(
133
+ options: ObscuraProcessOptions,
134
+ readonly limits: ObscuraProcessLimits,
135
+ ) {
136
+ validateObscuraCommand(options, limits);
137
+ const wantStderr = (options.stderr ?? "pipe") === "pipe";
138
+ this.child = spawn(options.command, [...(options.args ?? [])], {
139
+ env: baseEnv(options.env),
140
+ ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
141
+ stdio: ["ignore", "ignore", wantStderr ? "pipe" : "ignore"],
142
+ // Own process group on POSIX so a tree kill reaches obscura workers.
143
+ detached: process.platform !== "win32",
144
+ windowsHide: true,
145
+ });
146
+ if (wantStderr) {
147
+ let retained = 0;
148
+ this.child.stderr?.on("data", (chunk: Buffer) => {
149
+ const room = this.limits.maxStderrBytes - retained;
150
+ if (room <= 0) return;
151
+ const take = chunk.subarray(0, room);
152
+ retained += take.byteLength;
153
+ this.stderr = Buffer.concat([this.stderr, take], retained);
154
+ if (take.byteLength < chunk.byteLength) this.stderrTruncated = true;
155
+ });
156
+ }
157
+ this.exitPromise = new Promise<ObscuraExit>((resolve, reject) => {
158
+ this.child.once("error", (error) => reject(new ObscuraError("ERR_OBSCURA_SPAWN", `spawn failed: ${error.message}`)));
159
+ this.child.once("exit", (code, signal) => resolve({ code, signal }));
160
+ });
161
+ }
162
+
163
+ get pid(): number | undefined {
164
+ return this.child.pid;
165
+ }
166
+
167
+ get exited(): Promise<ObscuraExit> {
168
+ return this.exitPromise;
169
+ }
170
+
171
+ stderrText(): string {
172
+ const text = this.stderr.toString("utf8");
173
+ return this.stderrTruncated ? `${text}\n[truncated]` : text;
174
+ }
175
+
176
+ async waitReady(probe: () => boolean | Promise<boolean>, options?: { signal?: AbortSignal }): Promise<void> {
177
+ const signal = options?.signal;
178
+ if (signal?.aborted) throw new ObscuraError("ERR_OBSCURA_ABORTED", "startup aborted");
179
+ let exitSeen: ObscuraExit | undefined;
180
+ const onExit = (exit: ObscuraExit) => {
181
+ exitSeen = exit;
182
+ };
183
+ void this.exitPromise.then(onExit, () => {});
184
+ const deadline = Date.now() + this.limits.startupTimeoutMs;
185
+ try {
186
+ while (exitSeen === undefined) {
187
+ if (await probe()) return;
188
+ if (signal?.aborted) throw new ObscuraError("ERR_OBSCURA_ABORTED", "startup aborted");
189
+ if (Date.now() >= deadline) {
190
+ throw new ObscuraError("ERR_OBSCURA_START_TIMEOUT", `readiness probe timed out after ${this.limits.startupTimeoutMs}ms`);
191
+ }
192
+ await delay(25);
193
+ }
194
+ throw new ObscuraError(
195
+ "ERR_OBSCURA_EXITED",
196
+ `process exited before readiness (code=${exitSeen.code}, signal=${exitSeen.signal ?? "none"})`,
197
+ );
198
+ } catch (error) {
199
+ await this.close({ signal });
200
+ throw error;
201
+ }
202
+ }
203
+
204
+ async close(options?: ObscuraCloseOptions): Promise<void> {
205
+ if (this.closePromise) return this.closePromise;
206
+ this.closePromise = this.doClose(options);
207
+ await this.closePromise;
208
+ }
209
+
210
+ private async doClose(options?: ObscuraCloseOptions): Promise<void> {
211
+ const signal = options?.signal;
212
+ const grace = options?.shutdownTimeoutMs ?? this.limits.shutdownTimeoutMs;
213
+ const abort = () => killTree(this.child, "SIGKILL", this.groupKill);
214
+ signal?.addEventListener("abort", abort, { once: true });
215
+ let timer: NodeJS.Timeout | undefined;
216
+ try {
217
+ killTree(this.child, "SIGTERM", this.groupKill);
218
+ if (grace > 0) {
219
+ timer = setTimeout(() => killTree(this.child, "SIGKILL", this.groupKill), grace);
220
+ timer.unref?.();
221
+ }
222
+ await this.exitPromise;
223
+ } catch {
224
+ // spawn already failed; nothing to terminate
225
+ } finally {
226
+ if (timer) clearTimeout(timer);
227
+ signal?.removeEventListener("abort", abort);
228
+ }
229
+ }
230
+ }
231
+
232
+ /** Spawn a shell-free, bounded, ownership-tracked Obscura (or equivalent) process. */
233
+ export function spawnObscuraProcess(options: ObscuraProcessOptions): OwnedObscuraProcess {
234
+ return new OwnedProcess(options, resolveObscuraProcessLimits(options.limits));
235
+ }
@@ -0,0 +1,73 @@
1
+ import { ObscuraError } from "./errors.js";
2
+
3
+ /**
4
+ * One replaceable HTML search profile. The extraction expression is a constant —
5
+ * the query travels only inside the URL-encoded search URL, never inside
6
+ * JavaScript source, so no user text is ever evaluated.
7
+ */
8
+ export interface SearchProfileControls {
9
+ readonly freshness?: "day" | "week" | "month" | "year";
10
+ readonly country?: string;
11
+ readonly language?: string;
12
+ }
13
+ export interface ObscuraSearchProfile {
14
+ readonly id: string;
15
+ /** Build the public-web search URL for an encoded-ready query and the controls `kl`/`df` carry. */
16
+ readonly searchUrl: (query: string, controls?: SearchProfileControls) => string;
17
+ /** Constant JS evaluated in the search results page; must return JSON of `{url,title,snippet}[]`. */
18
+ readonly extractionJs: string;
19
+ }
20
+
21
+ const DEFAULT_EXTRACTION_JS = `(() => {
22
+ const blocked = Boolean(document.querySelector('.anomaly-modal, [data-testid="anomaly-modal"]'));
23
+ const results = [...document.querySelectorAll('#links .result:not(.result--ad):not(.result--sponsored)')].slice(0, 50).map((r) => {
24
+ const a = r.querySelector('a.result__a');
25
+ const href = a?.getAttribute('href') ?? '';
26
+ let url = href;
27
+ try {
28
+ const target = new URL(href, location.href);
29
+ if (target.pathname === '/l/') url = target.searchParams.get('uddg') ?? href;
30
+ } catch {}
31
+ return {
32
+ url,
33
+ title: a?.textContent?.trim() ?? '',
34
+ snippet: r.querySelector('.result__snippet')?.textContent?.trim() ?? '',
35
+ };
36
+ });
37
+ return JSON.stringify({ blocked, results });
38
+ })()`;
39
+
40
+ /** DuckDuckGo `df` windows. */
41
+ const DDG_FRESHNESS: Readonly<Record<string, string>> = { day: "d", week: "w", month: "m", year: "y" };
42
+
43
+ export const DEFAULT_OBSCURA_SEARCH_PROFILE: ObscuraSearchProfile = {
44
+ id: "default",
45
+ // DuckDuckGo HTML endpoint: no JS requirement, stable result markup; duckduckgo.com/html/ only 302s here.
46
+ searchUrl: (query, controls = {}) => {
47
+ const url = new URL("https://html.duckduckgo.com/html/");
48
+ url.searchParams.set("q", query);
49
+ if (controls.freshness !== undefined) url.searchParams.set("df", DDG_FRESHNESS[controls.freshness]!);
50
+ // `kl` is a region-language pair, so both controls travel in it; the wildcard region is DDG's "any".
51
+ if (controls.country !== undefined || controls.language !== undefined)
52
+ url.searchParams.set("kl", `${controls.country ?? "wt"}-${controls.language ?? "en"}`);
53
+ return url.toString();
54
+ },
55
+ extractionJs: DEFAULT_EXTRACTION_JS,
56
+ };
57
+
58
+ export function resolveObscuraSearchProfile(profile: ObscuraSearchProfile | undefined, maxEvalBytes: number): ObscuraSearchProfile {
59
+ const resolved = profile ?? DEFAULT_OBSCURA_SEARCH_PROFILE;
60
+ if (typeof resolved.id !== "string" || resolved.id.length === 0 || resolved.id.length > 64) {
61
+ throw new ObscuraError("ERR_OBSCURA_INPUT", "search profile id must be a 1-64 character string");
62
+ }
63
+ if (typeof resolved.extractionJs !== "string" || resolved.extractionJs.length === 0) {
64
+ throw new ObscuraError("ERR_OBSCURA_INPUT", "search profile extractionJs must be a non-empty string");
65
+ }
66
+ if (Buffer.byteLength(resolved.extractionJs, "utf8") > maxEvalBytes) {
67
+ throw new ObscuraError("ERR_OBSCURA_LIMIT", `search profile extractionJs exceeds ${maxEvalBytes} bytes`);
68
+ }
69
+ if (typeof resolved.searchUrl !== "function") {
70
+ throw new ObscuraError("ERR_OBSCURA_INPUT", "search profile searchUrl must be a function");
71
+ }
72
+ return resolved;
73
+ }
@@ -0,0 +1,48 @@
1
+ import type { ObscuraProcessLimits } from "./limits.js";
2
+
3
+ export interface ObscuraProcessOptions {
4
+ /** Absolute path to the executable (the `obscura` binary, or e.g. `/usr/bin/docker`). Never run through a shell. */
5
+ readonly command: string;
6
+ /** Argument vector passed byte-for-byte, e.g. `["serve", "--host", "127.0.0.1", "--port", "9222"]` or `["run", "--rm", "-i", "h4ckf0r0day/obscura", "mcp"]`. */
7
+ readonly args?: readonly string[];
8
+ /**
9
+ * Explicit environment for the child. When omitted, a minimal allow-list
10
+ * (`PATH`, `HOME`) is inherited; the full host environment is never passed.
11
+ */
12
+ readonly env?: Readonly<Record<string, string>>;
13
+ readonly cwd?: string;
14
+ /** Capture stderr (default, capped) or discard it. */
15
+ readonly stderr?: "pipe" | "ignore";
16
+ /**
17
+ * Explicit host opt-in permitting `--allow-private-network`, `--allow-file-access`,
18
+ * or a non-loopback `--host` binding in `args`. Off by default and rejected otherwise.
19
+ */
20
+ readonly allowInsecureFlags?: boolean;
21
+ readonly limits?: Partial<ObscuraProcessLimits>;
22
+ }
23
+
24
+ export interface ObscuraExit {
25
+ readonly code: number | null;
26
+ readonly signal: NodeJS.Signals | null;
27
+ }
28
+
29
+ export interface ObscuraCloseOptions {
30
+ readonly signal?: AbortSignal;
31
+ /** Override the configured shutdown grace period for this close. */
32
+ readonly shutdownTimeoutMs?: number;
33
+ }
34
+
35
+ export interface OwnedObscuraProcess {
36
+ readonly pid: number | undefined;
37
+ /** Resolves when the process is gone; rejects on spawn failure. */
38
+ readonly exited: Promise<ObscuraExit>;
39
+ /** Capped stderr collected so far (empty when stderr is "ignore"). */
40
+ stderrText(): string;
41
+ /**
42
+ * Poll `probe()` until it returns true, bounded by `startupTimeoutMs`.
43
+ * Kills the owned process on timeout or abort.
44
+ */
45
+ waitReady(probe: () => boolean | Promise<boolean>, options?: { signal?: AbortSignal }): Promise<void>;
46
+ /** SIGTERM (group-wide on POSIX), then SIGKILL after the grace period. Idempotent. */
47
+ close(options?: ObscuraCloseOptions): Promise<void>;
48
+ }