@arnilo/prism-tools 0.0.0-stage → 0.13.0
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.
- package/CHANGELOG.md +410 -0
- package/LICENSE +21 -0
- package/README.md +44 -2
- package/browser/README.md +98 -0
- package/browser/skills/browser-automation/SKILL.md +14 -0
- package/browser/src/actions/cdp.ts +131 -0
- package/browser/src/actions/dialog.ts +25 -0
- package/browser/src/actions/download_release.ts +22 -0
- package/browser/src/actions/index.ts +37 -0
- package/browser/src/actions/locator.ts +70 -0
- package/browser/src/actions/navigate.ts +23 -0
- package/browser/src/actions/screenshot.ts +27 -0
- package/browser/src/actions/scroll.ts +24 -0
- package/browser/src/actions/select_page.ts +21 -0
- package/browser/src/actions/types.ts +114 -0
- package/browser/src/actions/upload.ts +44 -0
- package/browser/src/actions/wait.ts +45 -0
- package/browser/src/browser.ts +197 -0
- package/browser/src/cdp.ts +178 -0
- package/browser/src/checkpoint.ts +189 -0
- package/browser/src/config.ts +72 -0
- package/browser/src/downloads.ts +253 -0
- package/browser/src/errors.ts +31 -0
- package/browser/src/evaluate.ts +101 -0
- package/browser/src/index.ts +129 -0
- package/browser/src/limits.ts +271 -0
- package/browser/src/manager.ts +730 -0
- package/browser/src/network.ts +263 -0
- package/browser/src/observe.ts +178 -0
- package/browser/src/policy.ts +133 -0
- package/browser/src/screenshot.ts +107 -0
- package/browser/src/shared-sandbox.ts +53 -0
- package/browser/src/snapshot.ts +99 -0
- package/browser/src/targets.ts +165 -0
- package/browser/src/tools.ts +480 -0
- package/browser/src/types.ts +316 -0
- package/browser/src/uploads.ts +174 -0
- package/coding/README.md +337 -0
- package/coding/src/agent/acp-operations.ts +146 -0
- package/coding/src/agent/artifacts.ts +38 -0
- package/coding/src/agent/ask-user-decision.ts +623 -0
- package/coding/src/agent/atomic-write.ts +23 -0
- package/coding/src/agent/bounded-file.ts +21 -0
- package/coding/src/agent/checks.ts +301 -0
- package/coding/src/agent/coding-checkpoint.ts +828 -0
- package/coding/src/agent/delete.ts +237 -0
- package/coding/src/agent/diagnostics-tool.ts +163 -0
- package/coding/src/agent/diagnostics.ts +262 -0
- package/coding/src/agent/edit-diff.ts +382 -0
- package/coding/src/agent/edit.ts +322 -0
- package/coding/src/agent/effects.ts +102 -0
- package/coding/src/agent/env.ts +28 -0
- package/coding/src/agent/execution-policy.ts +89 -0
- package/coding/src/agent/file-mutation-queue.ts +72 -0
- package/coding/src/agent/forge/github.ts +603 -0
- package/coding/src/agent/forge/index.ts +16 -0
- package/coding/src/agent/forge/types.ts +185 -0
- package/coding/src/agent/git-aware-repository.ts +305 -0
- package/coding/src/agent/git-exec.ts +326 -0
- package/coding/src/agent/git-status.ts +177 -0
- package/coding/src/agent/git-tools.ts +360 -0
- package/coding/src/agent/git.ts +1074 -0
- package/coding/src/agent/glob-match.ts +146 -0
- package/coding/src/agent/glob.ts +182 -0
- package/coding/src/agent/goal-verify.ts +385 -0
- package/coding/src/agent/grep-worker.ts +74 -0
- package/coding/src/agent/grep.ts +459 -0
- package/coding/src/agent/identity.ts +48 -0
- package/coding/src/agent/index.ts +676 -0
- package/coding/src/agent/language/client.ts +429 -0
- package/coding/src/agent/language/framing.ts +190 -0
- package/coding/src/agent/language/index.ts +23 -0
- package/coding/src/agent/language/intelligence.ts +682 -0
- package/coding/src/agent/language/types.ts +187 -0
- package/coding/src/agent/lifecycle-events.ts +121 -0
- package/coding/src/agent/lifecycle.ts +166 -0
- package/coding/src/agent/limits.ts +214 -0
- package/coding/src/agent/move.ts +178 -0
- package/coding/src/agent/mutation-path.ts +52 -0
- package/coding/src/agent/output-accumulator.ts +304 -0
- package/coding/src/agent/path-utils.ts +120 -0
- package/coding/src/agent/process/index.ts +58 -0
- package/coding/src/agent/process/recovery.ts +508 -0
- package/coding/src/agent/process/sessions-host.ts +222 -0
- package/coding/src/agent/process/sessions-monitor.ts +141 -0
- package/coding/src/agent/process/sessions-recovery.ts +241 -0
- package/coding/src/agent/process/sessions-spawn.ts +613 -0
- package/coding/src/agent/process/sessions-teardown.ts +281 -0
- package/coding/src/agent/process/sessions.ts +30 -0
- package/coding/src/agent/process/types.ts +358 -0
- package/coding/src/agent/read-path-set.ts +126 -0
- package/coding/src/agent/read.ts +713 -0
- package/coding/src/agent/repository/glob.ts +147 -0
- package/coding/src/agent/repository/indexed-search.ts +480 -0
- package/coding/src/agent/repository/list.ts +131 -0
- package/coding/src/agent/repository/operations.ts +22 -0
- package/coding/src/agent/repository/path.ts +98 -0
- package/coding/src/agent/repository/search.ts +277 -0
- package/coding/src/agent/repository/types.ts +243 -0
- package/coding/src/agent/repository/walk.ts +126 -0
- package/coding/src/agent/repository.ts +12 -0
- package/coding/src/agent/shell-background.ts +264 -0
- package/coding/src/agent/shell-output.ts +75 -0
- package/coding/src/agent/shell.ts +526 -0
- package/coding/src/agent/todo.ts +399 -0
- package/coding/src/agent/truncate.ts +251 -0
- package/coding/src/agent/write.ts +170 -0
- package/coding/src/ask-user-host.ts +48 -0
- package/coding/src/config.ts +278 -0
- package/coding/src/hard-deny.ts +97 -0
- package/coding/src/index.ts +318 -0
- package/coding/src/principal-guard.ts +84 -0
- package/coding/src/sandbox.ts +92 -0
- package/coding/src/security/approval.ts +144 -0
- package/coding/src/security/command-rules.ts +57 -0
- package/coding/src/security/docker-cli.ts +222 -0
- package/coding/src/security/docker-sandbox.ts +1205 -0
- package/coding/src/security/e2b-sandbox.ts +853 -0
- package/coding/src/security/egress/dns-pin.ts +88 -0
- package/coding/src/security/egress/index.ts +32 -0
- package/coding/src/security/egress/limits.ts +57 -0
- package/coding/src/security/egress/policy.ts +137 -0
- package/coding/src/security/egress/proxy.ts +599 -0
- package/coding/src/security/egress/types.ts +30 -0
- package/coding/src/security/index.ts +199 -0
- package/coding/src/security/native-sandbox.ts +639 -0
- package/coding/src/security/path-containment.ts +55 -0
- package/coding/src/security/sandbox-coding-operations.ts +392 -0
- package/coding/src/security/sandbox-fs-operations.ts +644 -0
- package/coding/src/security/sandbox-limits.ts +147 -0
- package/coding/src/security/sandbox-tar.ts +308 -0
- package/coding/src/security/sandbox.ts +179 -0
- package/coding/src/security/semaphore.ts +55 -0
- package/coding/src/targets.ts +110 -0
- package/coding/src/workspace/diff.ts +78 -0
- package/coding/src/workspace/errors.ts +28 -0
- package/coding/src/workspace/index.ts +5 -0
- package/coding/src/workspace/merge.ts +151 -0
- package/coding/src/workspace/routing.ts +76 -0
- package/coding/src/workspace/service.ts +545 -0
- package/computer-use/README.md +105 -0
- package/computer-use/skills/computer-use-linux/SKILL.md +17 -0
- package/computer-use/src/classify.ts +53 -0
- package/computer-use/src/command.ts +29 -0
- package/computer-use/src/config.ts +69 -0
- package/computer-use/src/create.ts +170 -0
- package/computer-use/src/devices.ts +247 -0
- package/computer-use/src/env.ts +28 -0
- package/computer-use/src/index.ts +157 -0
- package/computer-use/src/skill.ts +21 -0
- package/openapi/README.md +97 -0
- package/openapi/src/compile.ts +386 -0
- package/openapi/src/config.ts +73 -0
- package/openapi/src/errors.ts +16 -0
- package/openapi/src/execute.ts +241 -0
- package/openapi/src/index.ts +124 -0
- package/openapi/src/limits.ts +63 -0
- package/openapi/src/specs.ts +86 -0
- package/openapi/src/tools.ts +60 -0
- package/openapi/src/types.ts +61 -0
- package/package.json +171 -4
- package/research/README.md +72 -0
- package/research/skills/research-report/SKILL.md +12 -0
- package/research/skills/research-scoping/SKILL.md +10 -0
- package/research/skills/research-synthesis/SKILL.md +10 -0
- package/research/skills/source-triage/SKILL.md +10 -0
- package/research/src/check.ts +71 -0
- package/research/src/config.ts +41 -0
- package/research/src/index.ts +147 -0
- package/research/src/ledger.ts +157 -0
- package/research/src/urls.ts +21 -0
- package/web/README.md +287 -0
- package/web/skills/web-research/SKILL.md +17 -0
- package/web/src/brave.ts +66 -0
- package/web/src/cache.ts +151 -0
- package/web/src/config.ts +210 -0
- package/web/src/controls.ts +128 -0
- package/web/src/domains.ts +114 -0
- package/web/src/exa.ts +60 -0
- package/web/src/firecrawl.ts +160 -0
- package/web/src/index.ts +387 -0
- package/web/src/kagi.ts +73 -0
- package/web/src/limits.ts +55 -0
- package/web/src/native/convert.ts +84 -0
- package/web/src/native/fetch.ts +226 -0
- package/web/src/normalize.ts +131 -0
- package/web/src/obscura/cdp.ts +201 -0
- package/web/src/obscura/classify.ts +35 -0
- package/web/src/obscura/cli.ts +254 -0
- package/web/src/obscura/errors.ts +8 -0
- package/web/src/obscura/index.ts +59 -0
- package/web/src/obscura/limits.ts +92 -0
- package/web/src/obscura/mcp.ts +104 -0
- package/web/src/obscura/playwright.ts +133 -0
- package/web/src/obscura/process.ts +235 -0
- package/web/src/obscura/search-profile.ts +73 -0
- package/web/src/obscura/types.ts +48 -0
- package/web/src/obscura/web.ts +419 -0
- package/web/src/prompt-fetch.ts +88 -0
- package/web/src/render.ts +75 -0
- package/web/src/searxng.ts +78 -0
- package/web/src/slice.ts +139 -0
- package/web/src/tavily.ts +77 -0
- package/web/src/tools.ts +182 -0
- package/web/src/transport.ts +310 -0
- package/web/src/types.ts +199 -0
- package/work/README.md +77 -0
- package/work/THIRD_PARTY_NOTICES +24 -0
- package/work/docling/ocr.py +150 -0
- package/work/sandbox/Dockerfile +36 -0
- package/work/sandbox/soffice.sh +12 -0
- package/work/src/command.ts +29 -0
- package/work/src/config.ts +138 -0
- package/work/src/connectors/cli.ts +343 -0
- package/work/src/connectors/drafts.ts +647 -0
- package/work/src/connectors/errors.ts +8 -0
- package/work/src/connectors/file-bytes.ts +149 -0
- package/work/src/connectors/google-workspace-http.ts +399 -0
- package/work/src/connectors/google-workspace.ts +465 -0
- package/work/src/connectors/gws-tools.ts +378 -0
- package/work/src/connectors/http.ts +123 -0
- package/work/src/connectors/idempotency.ts +235 -0
- package/work/src/connectors/identity.ts +34 -0
- package/work/src/connectors/index.ts +93 -0
- package/work/src/connectors/limits.ts +50 -0
- package/work/src/connectors/m365-tools.ts +308 -0
- package/work/src/connectors/microsoft365-http.ts +428 -0
- package/work/src/connectors/microsoft365.ts +402 -0
- package/work/src/connectors/normalize.ts +120 -0
- package/work/src/connectors/tool-helpers.ts +398 -0
- package/work/src/connectors/tools.ts +42 -0
- package/work/src/connectors/types.ts +399 -0
- package/work/src/diagrams/canonicalize.ts +166 -0
- package/work/src/diagrams/caps.ts +52 -0
- package/work/src/diagrams/embed.ts +340 -0
- package/work/src/diagrams/errors.ts +76 -0
- package/work/src/diagrams/index.ts +81 -0
- package/work/src/diagrams/messages.ts +150 -0
- package/work/src/diagrams/origin.ts +52 -0
- package/work/src/diagrams/telemetry.ts +34 -0
- package/work/src/diagrams/xml.ts +267 -0
- package/work/src/document-extraction/index.ts +563 -0
- package/work/src/document-reader/ambient.d.ts +2 -0
- package/work/src/document-reader/errors.ts +7 -0
- package/work/src/document-reader/index.ts +181 -0
- package/work/src/document-reader/mistral-ocr.ts +318 -0
- package/work/src/document-reader/parsers.ts +171 -0
- package/work/src/documents/caps.ts +135 -0
- package/work/src/documents/diff.ts +269 -0
- package/work/src/documents/errors.ts +44 -0
- package/work/src/documents/fidelity.ts +98 -0
- package/work/src/documents/generate.ts +96 -0
- package/work/src/documents/hash.ts +8 -0
- package/work/src/documents/index.ts +14 -0
- package/work/src/documents/model-schema.ts +639 -0
- package/work/src/documents/parse.ts +252 -0
- package/work/src/documents/patch-history.ts +81 -0
- package/work/src/documents/patch.ts +476 -0
- package/work/src/documents/preview-html.ts +245 -0
- package/work/src/documents/preview.ts +227 -0
- package/work/src/documents/telemetry.ts +36 -0
- package/work/src/documents/translate/docx.ts +292 -0
- package/work/src/documents/translate/pptx.ts +304 -0
- package/work/src/documents/translate/xlsx.ts +137 -0
- package/work/src/documents/types.ts +188 -0
- package/work/src/index.ts +311 -0
- package/work/src/runtime/worker-pool-entry.ts +102 -0
- package/work/src/runtime/worker-pool.ts +267 -0
- package/work/src/sandbox/composition.ts +212 -0
- package/work/src/sandbox/image.ts +6 -0
- package/work/src/sandbox/index.ts +14 -0
- package/work/src/sheets/caps.ts +68 -0
- package/work/src/sheets/csv.ts +351 -0
- package/work/src/sheets/decimal.ts +388 -0
- package/work/src/sheets/errors.ts +52 -0
- package/work/src/sheets/index.ts +62 -0
- package/work/src/sheets/inference.ts +378 -0
- package/work/src/sheets/telemetry.ts +34 -0
- package/work/src/sheets/types.ts +162 -0
- package/work/src/sheets/xlsx.ts +388 -0
- package/work/src/skills/index.ts +8 -0
- package/work/src/skills/load.ts +43 -0
- package/work/src/tools/filesystem.ts +21 -0
- package/work/src/tools/index.ts +1 -0
- package/work/src/tools/local-filesystem.ts +33 -0
- package/work/src/tools/office.ts +311 -0
- package/work/vendor/hermes-agent/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/SKILL.md +196 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_comments.py +289 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_common.py +94 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_create.py +177 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_edit.py +250 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_read.py +149 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_revisions.py +147 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_template.py +70 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_validate.py +156 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/SKILL.md +125 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/_raster.py +76 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_marker.py +88 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_pymupdf.py +101 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_create.py +130 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_fill_form.py +97 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_form_layout.py +168 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_make_form.py +145 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_merge.py +50 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_meta.py +115 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_page_image.py +99 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_read.py +153 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_secure.py +71 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_split.py +84 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_stamp.py +143 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_watermark.py +51 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/SKILL.md +220 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_create.py +214 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_edit.py +436 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_from_template.py +88 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_read.py +131 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_render.py +93 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/SKILL.md +196 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/csv_to_xlsx.py +104 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_create.py +259 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_edit.py +263 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_read.py +160 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_recalc.py +111 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_restructure.py +337 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_to_csv.py +69 -0
- package/work/vendor-lock.json +11 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/** Finite defaults and hard caps for the disposable Docker sandbox reference. */
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_STARTUP_TIMEOUT_MS = 30_000;
|
|
4
|
+
export const HARD_STARTUP_TIMEOUT_MS = 120_000;
|
|
5
|
+
export const DEFAULT_WALL_TIME_MS = 20 * 60_000;
|
|
6
|
+
export const HARD_WALL_TIME_MS = 30 * 60_000;
|
|
7
|
+
export const DEFAULT_IDLE_TIMEOUT_MS = 5 * 60_000;
|
|
8
|
+
export const HARD_IDLE_TIMEOUT_MS = 15 * 60_000;
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_CPUS = 2;
|
|
11
|
+
export const HARD_CPUS = 8;
|
|
12
|
+
export const DEFAULT_MEMORY_BYTES = 2 * 1024 ** 3;
|
|
13
|
+
export const HARD_MEMORY_BYTES = 16 * 1024 ** 3;
|
|
14
|
+
export const DEFAULT_MAX_PIDS = 256;
|
|
15
|
+
export const HARD_MAX_PIDS = 1_024;
|
|
16
|
+
export const DEFAULT_MAX_FDS = 1_024;
|
|
17
|
+
export const HARD_MAX_FDS = 8_192;
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_WORKSPACE_BYTES = 1024 ** 3;
|
|
20
|
+
export const HARD_WORKSPACE_BYTES = 8 * 1024 ** 3;
|
|
21
|
+
export const DEFAULT_TMP_BYTES = 256 * 1024 ** 2;
|
|
22
|
+
export const HARD_TMP_BYTES = 2 * 1024 ** 3;
|
|
23
|
+
export const DEFAULT_DOWNLOAD_BYTES = 64 * 1024 ** 2;
|
|
24
|
+
export const HARD_DOWNLOAD_BYTES = 512 * 1024 ** 2;
|
|
25
|
+
|
|
26
|
+
export const DEFAULT_MAX_COMMANDS = 100;
|
|
27
|
+
export const HARD_MAX_COMMANDS = 256;
|
|
28
|
+
export const DEFAULT_MAX_CONCURRENT_EXECS = 1;
|
|
29
|
+
export const HARD_MAX_CONCURRENT_EXECS = 8;
|
|
30
|
+
|
|
31
|
+
export const DEFAULT_MAX_ENV_NAMES = 64;
|
|
32
|
+
export const HARD_MAX_ENV_NAMES = 256;
|
|
33
|
+
export const DEFAULT_MAX_ENV_BYTES = 64 * 1024;
|
|
34
|
+
export const HARD_MAX_ENV_BYTES = 256 * 1024;
|
|
35
|
+
|
|
36
|
+
export const DEFAULT_MAX_EXPORT_ENTRIES = 50_000;
|
|
37
|
+
export const HARD_MAX_EXPORT_ENTRIES = 250_000;
|
|
38
|
+
export const DEFAULT_MAX_EXPORT_BYTES = 256 * 1024 ** 2;
|
|
39
|
+
export const HARD_MAX_EXPORT_BYTES = 2 * 1024 ** 3;
|
|
40
|
+
export const DEFAULT_MAX_RETAINED_ARTIFACTS = 16;
|
|
41
|
+
export const HARD_MAX_RETAINED_ARTIFACTS = 64;
|
|
42
|
+
|
|
43
|
+
export const DEFAULT_STOP_GRACE_MS = 5_000;
|
|
44
|
+
export const HARD_STOP_GRACE_MS = 30_000;
|
|
45
|
+
export const DEFAULT_CLEANUP_DEADLINE_MS = 30_000;
|
|
46
|
+
export const HARD_CLEANUP_DEADLINE_MS = 120_000;
|
|
47
|
+
|
|
48
|
+
export const DEFAULT_MAX_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
49
|
+
export const HARD_MAX_OUTPUT_BYTES = 1024 * 1024 * 1024;
|
|
50
|
+
|
|
51
|
+
export interface DockerSandboxLimitOptions {
|
|
52
|
+
readonly startupTimeoutMs?: number;
|
|
53
|
+
readonly wallTimeMs?: number;
|
|
54
|
+
readonly idleTimeoutMs?: number;
|
|
55
|
+
readonly cpus?: number;
|
|
56
|
+
readonly memoryBytes?: number;
|
|
57
|
+
readonly maxPids?: number;
|
|
58
|
+
readonly maxFds?: number;
|
|
59
|
+
readonly workspaceBytes?: number;
|
|
60
|
+
readonly tmpBytes?: number;
|
|
61
|
+
readonly downloadBytes?: number;
|
|
62
|
+
readonly maxCommands?: number;
|
|
63
|
+
readonly maxConcurrentExecs?: number;
|
|
64
|
+
readonly maxEnvNames?: number;
|
|
65
|
+
readonly maxEnvBytes?: number;
|
|
66
|
+
readonly maxExportEntries?: number;
|
|
67
|
+
readonly maxExportBytes?: number;
|
|
68
|
+
readonly maxRetainedArtifacts?: number;
|
|
69
|
+
readonly stopGraceMs?: number;
|
|
70
|
+
readonly cleanupDeadlineMs?: number;
|
|
71
|
+
readonly maxOutputBytes?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ResolvedDockerSandboxLimits {
|
|
75
|
+
readonly startupTimeoutMs: number;
|
|
76
|
+
readonly wallTimeMs: number;
|
|
77
|
+
readonly idleTimeoutMs: number;
|
|
78
|
+
readonly cpus: number;
|
|
79
|
+
readonly memoryBytes: number;
|
|
80
|
+
readonly maxPids: number;
|
|
81
|
+
readonly maxFds: number;
|
|
82
|
+
readonly workspaceBytes: number;
|
|
83
|
+
readonly tmpBytes: number;
|
|
84
|
+
readonly downloadBytes: number;
|
|
85
|
+
readonly maxCommands: number;
|
|
86
|
+
readonly maxConcurrentExecs: number;
|
|
87
|
+
readonly maxEnvNames: number;
|
|
88
|
+
readonly maxEnvBytes: number;
|
|
89
|
+
readonly maxExportEntries: number;
|
|
90
|
+
readonly maxExportEntriesHard: number;
|
|
91
|
+
readonly maxExportBytes: number;
|
|
92
|
+
readonly maxRetainedArtifacts: number;
|
|
93
|
+
readonly stopGraceMs: number;
|
|
94
|
+
readonly cleanupDeadlineMs: number;
|
|
95
|
+
readonly maxOutputBytes: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const SPECS = {
|
|
99
|
+
startupTimeoutMs: [DEFAULT_STARTUP_TIMEOUT_MS, HARD_STARTUP_TIMEOUT_MS],
|
|
100
|
+
wallTimeMs: [DEFAULT_WALL_TIME_MS, HARD_WALL_TIME_MS],
|
|
101
|
+
idleTimeoutMs: [DEFAULT_IDLE_TIMEOUT_MS, HARD_IDLE_TIMEOUT_MS],
|
|
102
|
+
memoryBytes: [DEFAULT_MEMORY_BYTES, HARD_MEMORY_BYTES],
|
|
103
|
+
maxPids: [DEFAULT_MAX_PIDS, HARD_MAX_PIDS],
|
|
104
|
+
maxFds: [DEFAULT_MAX_FDS, HARD_MAX_FDS],
|
|
105
|
+
workspaceBytes: [DEFAULT_WORKSPACE_BYTES, HARD_WORKSPACE_BYTES],
|
|
106
|
+
tmpBytes: [DEFAULT_TMP_BYTES, HARD_TMP_BYTES],
|
|
107
|
+
downloadBytes: [DEFAULT_DOWNLOAD_BYTES, HARD_DOWNLOAD_BYTES],
|
|
108
|
+
maxCommands: [DEFAULT_MAX_COMMANDS, HARD_MAX_COMMANDS],
|
|
109
|
+
maxConcurrentExecs: [DEFAULT_MAX_CONCURRENT_EXECS, HARD_MAX_CONCURRENT_EXECS],
|
|
110
|
+
maxEnvNames: [DEFAULT_MAX_ENV_NAMES, HARD_MAX_ENV_NAMES],
|
|
111
|
+
maxEnvBytes: [DEFAULT_MAX_ENV_BYTES, HARD_MAX_ENV_BYTES],
|
|
112
|
+
maxExportEntries: [DEFAULT_MAX_EXPORT_ENTRIES, HARD_MAX_EXPORT_ENTRIES],
|
|
113
|
+
maxExportBytes: [DEFAULT_MAX_EXPORT_BYTES, HARD_MAX_EXPORT_BYTES],
|
|
114
|
+
maxRetainedArtifacts: [DEFAULT_MAX_RETAINED_ARTIFACTS, HARD_MAX_RETAINED_ARTIFACTS],
|
|
115
|
+
stopGraceMs: [DEFAULT_STOP_GRACE_MS, HARD_STOP_GRACE_MS],
|
|
116
|
+
cleanupDeadlineMs: [DEFAULT_CLEANUP_DEADLINE_MS, HARD_CLEANUP_DEADLINE_MS],
|
|
117
|
+
maxOutputBytes: [DEFAULT_MAX_OUTPUT_BYTES, HARD_MAX_OUTPUT_BYTES],
|
|
118
|
+
} as const;
|
|
119
|
+
|
|
120
|
+
export function validateSandboxLimit(name: string, value: number, hardCap: number): number {
|
|
121
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > hardCap) {
|
|
122
|
+
throw new RangeError(`${name} must be a positive safe integer at most ${hardCap}`);
|
|
123
|
+
}
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function validateCpus(value: number): number {
|
|
128
|
+
if (!Number.isFinite(value) || value <= 0 || value > HARD_CPUS) {
|
|
129
|
+
throw new RangeError(`cpus must be a finite number greater than 0 and at most ${HARD_CPUS}`);
|
|
130
|
+
}
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function resolveDockerSandboxLimits(input: DockerSandboxLimitOptions = {}): ResolvedDockerSandboxLimits {
|
|
135
|
+
const resolved = Object.fromEntries(
|
|
136
|
+
Object.entries(SPECS).map(([name, [fallback, hardCap]]) => {
|
|
137
|
+
const value = input[name as keyof DockerSandboxLimitOptions] ?? fallback;
|
|
138
|
+
return [name, validateSandboxLimit(name, value as number, hardCap)];
|
|
139
|
+
}),
|
|
140
|
+
) as Omit<ResolvedDockerSandboxLimits, "cpus" | "maxExportEntriesHard">;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
...resolved,
|
|
144
|
+
cpus: validateCpus(input.cpus ?? DEFAULT_CPUS),
|
|
145
|
+
maxExportEntriesHard: HARD_MAX_EXPORT_ENTRIES,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { createHash, type Hash } from "node:crypto";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { lstat, opendir, realpath } from "node:fs/promises";
|
|
4
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
|
|
7
|
+
export interface TarBounds {
|
|
8
|
+
readonly maxEntries: number;
|
|
9
|
+
readonly maxBytes: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TarExportSummary {
|
|
13
|
+
readonly sha256: string;
|
|
14
|
+
readonly entryCount: number;
|
|
15
|
+
readonly byteCount: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class SandboxTarError extends Error {
|
|
19
|
+
readonly code = "ERR_PRISM_SANDBOX_TAR";
|
|
20
|
+
constructor(message: string) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "SandboxTarError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const BLOCK = 512;
|
|
27
|
+
|
|
28
|
+
function encodeOctal(value: number, length: number): Buffer {
|
|
29
|
+
const body = value.toString(8).padStart(length - 1, "0");
|
|
30
|
+
return Buffer.from(`${body}\0`, "utf8");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function checksumHeader(header: Buffer): number {
|
|
34
|
+
let sum = 0;
|
|
35
|
+
for (let i = 0; i < header.length; i++) sum += header[i];
|
|
36
|
+
return sum;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writeUstarHeader(opts: { name: string; size: number; mode: number; mtimeSec: number; type: "0" | "5" }): Buffer {
|
|
40
|
+
if (Buffer.byteLength(opts.name, "utf8") > 100) {
|
|
41
|
+
throw new SandboxTarError(`tar path exceeds 100 UTF-8 bytes: ${opts.name}`);
|
|
42
|
+
}
|
|
43
|
+
const header = Buffer.alloc(BLOCK, 0);
|
|
44
|
+
header.write(opts.name, 0, 100, "utf8");
|
|
45
|
+
encodeOctal(opts.mode & 0o7777, 8).copy(header, 100);
|
|
46
|
+
encodeOctal(0, 8).copy(header, 108);
|
|
47
|
+
encodeOctal(0, 8).copy(header, 116);
|
|
48
|
+
encodeOctal(opts.size, 12).copy(header, 124);
|
|
49
|
+
encodeOctal(opts.mtimeSec, 12).copy(header, 136);
|
|
50
|
+
header.write(" ", 148, 8, "utf8");
|
|
51
|
+
header.write(opts.type, 156, 1, "utf8");
|
|
52
|
+
header.write("ustar\0", 257, 6, "utf8");
|
|
53
|
+
header.write("00", 263, 2, "utf8");
|
|
54
|
+
const sum = checksumHeader(header);
|
|
55
|
+
const checksum = Buffer.from(`${sum.toString(8).padStart(6, "0")}\0 `, "utf8");
|
|
56
|
+
checksum.copy(header, 148);
|
|
57
|
+
return header;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function padToBlock(size: number): number {
|
|
61
|
+
const rem = size % BLOCK;
|
|
62
|
+
return rem === 0 ? 0 : BLOCK - rem;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface WalkEntry {
|
|
66
|
+
readonly abs: string;
|
|
67
|
+
readonly rel: string;
|
|
68
|
+
readonly size: number;
|
|
69
|
+
readonly mode: number;
|
|
70
|
+
readonly dir: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function* walkRegularFiles(root: string, bounds: TarBounds): AsyncGenerator<WalkEntry> {
|
|
74
|
+
const rootReal = await realpath(root);
|
|
75
|
+
let entries = 0;
|
|
76
|
+
let bytes = 0;
|
|
77
|
+
|
|
78
|
+
async function* visit(dirReal: string): AsyncGenerator<WalkEntry> {
|
|
79
|
+
const handle = await opendir(dirReal);
|
|
80
|
+
try {
|
|
81
|
+
for await (const dirent of handle) {
|
|
82
|
+
const abs = join(dirReal, dirent.name);
|
|
83
|
+
const st = await lstat(abs);
|
|
84
|
+
if (st.isSymbolicLink()) {
|
|
85
|
+
throw new SandboxTarError(`symlink rejected during sandbox import: ${abs}`);
|
|
86
|
+
}
|
|
87
|
+
if (st.isFIFO() || st.isSocket() || st.isCharacterDevice() || st.isBlockDevice()) {
|
|
88
|
+
throw new SandboxTarError(`special file rejected during sandbox import: ${abs}`);
|
|
89
|
+
}
|
|
90
|
+
const relToRoot = relative(rootReal, abs);
|
|
91
|
+
if (relToRoot.startsWith("..") || relToRoot.includes(`..${sep}`)) {
|
|
92
|
+
throw new SandboxTarError(`path escape rejected during sandbox import: ${abs}`);
|
|
93
|
+
}
|
|
94
|
+
const rel = relToRoot.split(sep).join("/");
|
|
95
|
+
entries += 1;
|
|
96
|
+
if (entries > bounds.maxEntries) {
|
|
97
|
+
throw new SandboxTarError(`import exceeded max entries (${bounds.maxEntries})`);
|
|
98
|
+
}
|
|
99
|
+
if (st.isDirectory()) {
|
|
100
|
+
if (rel !== "") {
|
|
101
|
+
yield { abs, rel: `${rel}/`, size: 0, mode: st.mode, dir: true };
|
|
102
|
+
}
|
|
103
|
+
const childReal = await realpath(abs);
|
|
104
|
+
if (relative(rootReal, childReal).startsWith("..")) {
|
|
105
|
+
throw new SandboxTarError(`directory escape rejected during sandbox import: ${abs}`);
|
|
106
|
+
}
|
|
107
|
+
yield* visit(childReal);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!st.isFile()) {
|
|
111
|
+
throw new SandboxTarError(`unsupported file type during sandbox import: ${abs}`);
|
|
112
|
+
}
|
|
113
|
+
bytes += st.size;
|
|
114
|
+
if (bytes > bounds.maxBytes) {
|
|
115
|
+
throw new SandboxTarError(`import exceeded max bytes (${bounds.maxBytes})`);
|
|
116
|
+
}
|
|
117
|
+
yield { abs, rel, size: st.size, mode: st.mode, dir: false };
|
|
118
|
+
}
|
|
119
|
+
} finally {
|
|
120
|
+
await handle.close().catch(() => undefined);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
yield* visit(rootReal);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function* importTarBlocks(sourceRoot: string, bounds: TarBounds): AsyncGenerator<Buffer> {
|
|
128
|
+
const mtimeSec = Math.floor(Date.now() / 1000);
|
|
129
|
+
for await (const entry of walkRegularFiles(sourceRoot, bounds)) {
|
|
130
|
+
if (entry.dir) {
|
|
131
|
+
yield writeUstarHeader({
|
|
132
|
+
name: entry.rel.endsWith("/") ? entry.rel : `${entry.rel}/`,
|
|
133
|
+
size: 0,
|
|
134
|
+
mode: entry.mode,
|
|
135
|
+
mtimeSec,
|
|
136
|
+
type: "5",
|
|
137
|
+
});
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
yield writeUstarHeader({
|
|
141
|
+
name: entry.rel,
|
|
142
|
+
size: entry.size,
|
|
143
|
+
mode: entry.mode,
|
|
144
|
+
mtimeSec,
|
|
145
|
+
type: "0",
|
|
146
|
+
});
|
|
147
|
+
const file = createReadStream(entry.abs);
|
|
148
|
+
for await (const chunk of file) {
|
|
149
|
+
yield Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
150
|
+
}
|
|
151
|
+
const padding = padToBlock(entry.size);
|
|
152
|
+
if (padding > 0) yield Buffer.alloc(padding, 0);
|
|
153
|
+
}
|
|
154
|
+
yield Buffer.alloc(BLOCK * 2, 0);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Create a bounded ustar stream from a host directory. Rejects symlinks/devices/escapes. */
|
|
158
|
+
export function createImportTarStream(sourceRoot: string, bounds: TarBounds): Readable {
|
|
159
|
+
return Readable.from(importTarBlocks(sourceRoot, bounds));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function parseOctal(buf: Buffer): number {
|
|
163
|
+
const text = buf.toString("utf8").replace(/\0/g, "").trim();
|
|
164
|
+
if (!text) return 0;
|
|
165
|
+
return Number.parseInt(text, 8);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Consume a ustar stream, enforce entry/byte bounds, and compute SHA-256 of the raw bytes.
|
|
170
|
+
* Rejects absolute paths, `..` segments, and non-file/dir entry types.
|
|
171
|
+
*/
|
|
172
|
+
export async function summarizeTarStream(stream: AsyncIterable<Buffer>, bounds: TarBounds): Promise<TarExportSummary> {
|
|
173
|
+
const hash: Hash = createHash("sha256");
|
|
174
|
+
// ponytail: chunk-array accumulator — O(1) append per stream chunk, no whole-pending
|
|
175
|
+
// re-concat (the 0.2.4 `pending = Buffer.concat([pending, chunk])` was O(input * chunks)).
|
|
176
|
+
// 512B headers copy once each (contiguous); file data is advanced past (drop) without
|
|
177
|
+
// copying since the hash already consumed each chunk via `hash.update`. Total copying O(input).
|
|
178
|
+
const acc = { chunks: [] as Buffer[], offset: 0, retained: 0 };
|
|
179
|
+
let entryCount = 0;
|
|
180
|
+
let byteCount = 0;
|
|
181
|
+
let fileRemaining = 0;
|
|
182
|
+
let skipPadding = 0;
|
|
183
|
+
let sawZeroBlock = false;
|
|
184
|
+
let finished = false;
|
|
185
|
+
|
|
186
|
+
const peekAt = (start: number, n: number): Buffer => {
|
|
187
|
+
const out = Buffer.allocUnsafe(n);
|
|
188
|
+
let written = 0;
|
|
189
|
+
let i = 0;
|
|
190
|
+
let off = acc.offset;
|
|
191
|
+
let skip = start;
|
|
192
|
+
while (skip > 0) {
|
|
193
|
+
const c = acc.chunks[i];
|
|
194
|
+
const avail = c.length - off;
|
|
195
|
+
if (skip >= avail) {
|
|
196
|
+
skip -= avail;
|
|
197
|
+
i++;
|
|
198
|
+
off = 0;
|
|
199
|
+
} else {
|
|
200
|
+
off += skip;
|
|
201
|
+
skip = 0;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
while (written < n) {
|
|
205
|
+
const c = acc.chunks[i];
|
|
206
|
+
const take = Math.min(c.length - off, n - written);
|
|
207
|
+
c.copy(out, written, off, off + take);
|
|
208
|
+
written += take;
|
|
209
|
+
i++;
|
|
210
|
+
off = 0;
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
};
|
|
214
|
+
const drop = (n: number): void => {
|
|
215
|
+
let remaining = n;
|
|
216
|
+
while (remaining > 0 && acc.chunks.length > 0) {
|
|
217
|
+
const first = acc.chunks[0];
|
|
218
|
+
const avail = first.length - acc.offset;
|
|
219
|
+
if (remaining >= avail) {
|
|
220
|
+
remaining -= avail;
|
|
221
|
+
acc.chunks.shift();
|
|
222
|
+
acc.offset = 0;
|
|
223
|
+
} else {
|
|
224
|
+
acc.offset += remaining;
|
|
225
|
+
remaining = 0;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
acc.retained -= n;
|
|
229
|
+
};
|
|
230
|
+
const take = (n: number): Buffer | undefined => {
|
|
231
|
+
if (acc.retained < n) return undefined;
|
|
232
|
+
const out = peekAt(0, n);
|
|
233
|
+
drop(n);
|
|
234
|
+
return out;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
for await (const chunk of stream) {
|
|
238
|
+
hash.update(chunk);
|
|
239
|
+
byteCount += chunk.byteLength;
|
|
240
|
+
if (byteCount > bounds.maxBytes) {
|
|
241
|
+
throw new SandboxTarError(`export exceeded max bytes (${bounds.maxBytes})`);
|
|
242
|
+
}
|
|
243
|
+
if (finished) continue;
|
|
244
|
+
acc.chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
245
|
+
acc.retained += chunk.length;
|
|
246
|
+
|
|
247
|
+
while (true) {
|
|
248
|
+
if (fileRemaining > 0) {
|
|
249
|
+
if (acc.retained === 0) break;
|
|
250
|
+
const n = Math.min(fileRemaining, acc.retained);
|
|
251
|
+
drop(n);
|
|
252
|
+
fileRemaining -= n;
|
|
253
|
+
if (fileRemaining === 0 && skipPadding > 0) {
|
|
254
|
+
if (acc.retained < skipPadding) break;
|
|
255
|
+
drop(skipPadding);
|
|
256
|
+
skipPadding = 0;
|
|
257
|
+
}
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const header = take(BLOCK);
|
|
262
|
+
if (!header) break;
|
|
263
|
+
if (header.every((b) => b === 0)) {
|
|
264
|
+
if (sawZeroBlock) {
|
|
265
|
+
finished = true;
|
|
266
|
+
acc.chunks = [];
|
|
267
|
+
acc.offset = 0;
|
|
268
|
+
acc.retained = 0;
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
sawZeroBlock = true;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
sawZeroBlock = false;
|
|
275
|
+
const name = header.subarray(0, 100).toString("utf8").replace(/\0/g, "");
|
|
276
|
+
const size = parseOctal(header.subarray(124, 136));
|
|
277
|
+
const typeFlag = String.fromCharCode(header[156] ?? 0);
|
|
278
|
+
if (!name || name.startsWith("/") || name.split("/").includes("..")) {
|
|
279
|
+
throw new SandboxTarError(`unsafe tar path rejected: ${name || "<empty>"}`);
|
|
280
|
+
}
|
|
281
|
+
if (typeFlag !== "0" && typeFlag !== "\0" && typeFlag !== "5") {
|
|
282
|
+
throw new SandboxTarError(`unsupported tar entry type '${typeFlag}' for ${name}`);
|
|
283
|
+
}
|
|
284
|
+
entryCount += 1;
|
|
285
|
+
if (entryCount > bounds.maxEntries) {
|
|
286
|
+
throw new SandboxTarError(`export exceeded max entries (${bounds.maxEntries})`);
|
|
287
|
+
}
|
|
288
|
+
if (typeFlag === "5") continue;
|
|
289
|
+
fileRemaining = size;
|
|
290
|
+
skipPadding = padToBlock(size);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
sha256: hash.digest("hex"),
|
|
296
|
+
entryCount,
|
|
297
|
+
byteCount,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function resolveUnderRoot(root: string, candidate: string): string {
|
|
302
|
+
const resolved = resolve(root, candidate);
|
|
303
|
+
const rel = relative(resolve(root), resolved);
|
|
304
|
+
if (rel.startsWith("..") || rel.includes(`..${sep}`)) {
|
|
305
|
+
throw new SandboxTarError(`path escapes root: ${candidate}`);
|
|
306
|
+
}
|
|
307
|
+
return resolved;
|
|
308
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { Readable } from "node:stream";
|
|
2
|
+
import type { BashOperations } from "../agent/index.js";
|
|
3
|
+
|
|
4
|
+
export interface SandboxExecRequest {
|
|
5
|
+
readonly command: string;
|
|
6
|
+
readonly cwd: string;
|
|
7
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
8
|
+
readonly onData?: (data: Buffer) => void;
|
|
9
|
+
readonly signal?: AbortSignal;
|
|
10
|
+
readonly timeout?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SandboxExecFileRequest {
|
|
14
|
+
readonly file: string;
|
|
15
|
+
readonly args: readonly string[];
|
|
16
|
+
readonly cwd?: string;
|
|
17
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
18
|
+
readonly onData?: (data: Buffer) => void;
|
|
19
|
+
readonly signal?: AbortSignal;
|
|
20
|
+
/** Wall timeout in milliseconds. */
|
|
21
|
+
readonly timeout?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type SandboxStatusState = "running" | "stopped" | "removed" | "failed";
|
|
25
|
+
|
|
26
|
+
/** Vendor snapshot kind. `filesystem` drops RAM/processes on resume; `memory` restores them. */
|
|
27
|
+
export type SandboxSnapshotKind = "memory" | "filesystem";
|
|
28
|
+
|
|
29
|
+
export interface SandboxPauseResult {
|
|
30
|
+
readonly kind: SandboxSnapshotKind;
|
|
31
|
+
readonly state: "paused";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SandboxStatus {
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly state: SandboxStatusState;
|
|
37
|
+
readonly image: string;
|
|
38
|
+
readonly startedAt: number;
|
|
39
|
+
readonly lastActivityAt: number;
|
|
40
|
+
readonly commandCount: number;
|
|
41
|
+
/** Content identity of the imported workspace tree (when import ran). */
|
|
42
|
+
readonly importIdentity?: SandboxExportMetadata;
|
|
43
|
+
/** Content identity of the last successful close export (resume check). */
|
|
44
|
+
readonly lastExportIdentity?: SandboxExportMetadata;
|
|
45
|
+
/** Set when `state` is `stopped` because the backend paused rather than exited. */
|
|
46
|
+
readonly snapshot?: { readonly kind: SandboxSnapshotKind };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SandboxExportMetadata {
|
|
50
|
+
readonly sha256: string;
|
|
51
|
+
readonly entryCount: number;
|
|
52
|
+
readonly byteCount: number;
|
|
53
|
+
readonly format: "tar";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface SandboxCloseOptions {
|
|
57
|
+
/**
|
|
58
|
+
* Host-owned artifact writer. Receives a bounded tar stream plus finalized
|
|
59
|
+
* SHA-256/entry/byte metadata. Partial failures must discard the host artifact.
|
|
60
|
+
*/
|
|
61
|
+
readonly export?: (stream: Readable, metadata: SandboxExportMetadata) => Promise<void>;
|
|
62
|
+
readonly signal?: AbortSignal;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Explicit isolation/coherence capabilities of a sandbox backend or composition.
|
|
67
|
+
*
|
|
68
|
+
* Every field is a claim that MUST hold before security-sensitive host policy
|
|
69
|
+
* may rely on it. Omission (or invalid metadata) resolves every field false —
|
|
70
|
+
* a backend can never gain a capability by omission or by interface shape.
|
|
71
|
+
*
|
|
72
|
+
* `workspaceCoherent`: shell, filesystem, and repository tools observe one
|
|
73
|
+
* workspace tree. `filesystemIsolated`: sandbox processes cannot touch the
|
|
74
|
+
* host filesystem. `networkIsolated`: no reachable network (or only an
|
|
75
|
+
* isolated one). `processIsolated`: sandbox processes run in a separate
|
|
76
|
+
* process namespace. `privilegeIsolated`: sandbox processes cannot obtain
|
|
77
|
+
* host privileges (root-in-container without user namespaces is NOT reliable).
|
|
78
|
+
* `egressRestricted`: any network egress is forced through a controlled
|
|
79
|
+
* proxy/firewall (a dedicated field because `networkIsolated` cannot represent
|
|
80
|
+
* custom networks that exist but have proxy-constrained egress).
|
|
81
|
+
*/
|
|
82
|
+
export interface SandboxCapabilities {
|
|
83
|
+
readonly workspaceCoherent: boolean;
|
|
84
|
+
readonly filesystemIsolated: boolean;
|
|
85
|
+
readonly networkIsolated: boolean;
|
|
86
|
+
readonly processIsolated: boolean;
|
|
87
|
+
readonly privilegeIsolated: boolean;
|
|
88
|
+
readonly egressRestricted: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface SandboxAdapter {
|
|
92
|
+
exec(request: SandboxExecRequest): Promise<{ exitCode: number | null }>;
|
|
93
|
+
/**
|
|
94
|
+
* Optional host attestation of isolation controls. Omitted metadata (or
|
|
95
|
+
* malformed/non-boolean/unknown fields) resolves every capability false;
|
|
96
|
+
* this is host attestation, never verified by Prism.
|
|
97
|
+
*/
|
|
98
|
+
readonly capabilities?: Readonly<SandboxCapabilities>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Disposable sandbox lifecycle used by the Docker/OCI reference adapter.
|
|
103
|
+
* Extends the shell-only `SandboxAdapter` with typed exec, status, and cleanup.
|
|
104
|
+
*/
|
|
105
|
+
/**
|
|
106
|
+
* Long-running process handle inside a disposable sandbox.
|
|
107
|
+
* Optional on {@link DisposableSandbox.startProcess}; absence = one-shot-only adapter.
|
|
108
|
+
*/
|
|
109
|
+
export interface SandboxProcessHandle {
|
|
110
|
+
readonly pid?: number;
|
|
111
|
+
write(data: Uint8Array): Promise<void>;
|
|
112
|
+
signal(name: string): Promise<void>;
|
|
113
|
+
kill(): Promise<void>;
|
|
114
|
+
release(): Promise<void>;
|
|
115
|
+
wait(options?: { timeoutMs?: number; signal?: AbortSignal }): Promise<{ exitCode: number | null }>;
|
|
116
|
+
/** Opaque non-secret reattachment ref for durable process recovery. */
|
|
117
|
+
readonly ref?: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface DisposableSandbox extends SandboxAdapter {
|
|
121
|
+
readonly id: string;
|
|
122
|
+
readonly labels?: Readonly<Record<string, string>>;
|
|
123
|
+
/** Present after workspace import; content hash only (no secrets). */
|
|
124
|
+
readonly importIdentity?: SandboxExportMetadata;
|
|
125
|
+
/** Present after a successful `close({ export })`; use for resume hash checks. */
|
|
126
|
+
readonly lastExportIdentity?: SandboxExportMetadata;
|
|
127
|
+
execFile(request: SandboxExecFileRequest): Promise<{ exitCode: number | null }>;
|
|
128
|
+
status(): Promise<SandboxStatus>;
|
|
129
|
+
stop(options?: { graceMs?: number; signal?: AbortSignal }): Promise<void>;
|
|
130
|
+
kill(options?: { signal?: AbortSignal }): Promise<void>;
|
|
131
|
+
close(options?: SandboxCloseOptions): Promise<SandboxExportMetadata | undefined>;
|
|
132
|
+
/**
|
|
133
|
+
* Optional long-running process capability. Detected, never assumed.
|
|
134
|
+
* Absence → ProcessSessions.start over this sandbox fails closed with ERR_PRISM_PROCESS_UNSUPPORTED.
|
|
135
|
+
*/
|
|
136
|
+
startProcess?(request: SandboxExecFileRequest): Promise<SandboxProcessHandle>;
|
|
137
|
+
/**
|
|
138
|
+
* Optional process reattachment capability for durable recovery.
|
|
139
|
+
* Resolves an attested backend ref to an existing live handle; returns null otherwise.
|
|
140
|
+
*/
|
|
141
|
+
attachProcess?(ref: string): Promise<SandboxProcessHandle | null>;
|
|
142
|
+
/**
|
|
143
|
+
* Optional snapshot/pause. Detected, never assumed.
|
|
144
|
+
* `keepMemory: false` is filesystem-only: resume reboots and running processes are gone.
|
|
145
|
+
* A refused pause must leave the sandbox running (or unknown) — never claim paused.
|
|
146
|
+
*/
|
|
147
|
+
pause?(options?: { keepMemory?: boolean; signal?: AbortSignal }): Promise<SandboxPauseResult>;
|
|
148
|
+
/** Explicit resume after pause. Must not run as a side effect of exec/connect. */
|
|
149
|
+
resume?(options?: { signal?: AbortSignal }): Promise<void>;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export class SandboxExecutionError extends Error {
|
|
153
|
+
readonly code = "ERR_PRISM_SANDBOX_EXECUTION";
|
|
154
|
+
constructor(message: string) {
|
|
155
|
+
super(message);
|
|
156
|
+
this.name = "SandboxExecutionError";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Map a host-owned sandbox adapter to coding-agent `BashOperations`. */
|
|
161
|
+
export function createSandboxBashOperations(adapter: SandboxAdapter): BashOperations {
|
|
162
|
+
return {
|
|
163
|
+
exec: async (command, cwd, options) => {
|
|
164
|
+
try {
|
|
165
|
+
return await adapter.exec({
|
|
166
|
+
command,
|
|
167
|
+
cwd,
|
|
168
|
+
env: options.env,
|
|
169
|
+
onData: options.onData,
|
|
170
|
+
signal: options.signal,
|
|
171
|
+
timeout: options.timeout,
|
|
172
|
+
});
|
|
173
|
+
} catch (error) {
|
|
174
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
175
|
+
throw new SandboxExecutionError(message);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FIFO counting semaphore shared by the native/docker sandbox, the egress proxy,
|
|
3
|
+
* and the check runner concurrency caps.
|
|
4
|
+
*
|
|
5
|
+
* Contract:
|
|
6
|
+
* - `acquire` resolves with a release callback; call it exactly once per successful
|
|
7
|
+
* acquire. Releasing an unheld slot is ignored (the active count floors at zero).
|
|
8
|
+
* - Waiters wake in submission order (FIFO).
|
|
9
|
+
* - With an `AbortSignal`, an aborted wait rejects with the caller's sandbox error
|
|
10
|
+
* class and drops its waiter, so an aborted waiter never consumes a slot; the
|
|
11
|
+
* abort listener is detached on both wake and abort.
|
|
12
|
+
*
|
|
13
|
+
* Internal to the coding-tools security surface — intentionally absent from `./security/index.ts`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const ABORT_MESSAGE = "sandbox operation aborted";
|
|
17
|
+
|
|
18
|
+
export class Semaphore {
|
|
19
|
+
private active = 0;
|
|
20
|
+
private readonly waiters: Array<() => void> = [];
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
private readonly max: number,
|
|
24
|
+
private readonly abortError: new (message: string) => Error = Error,
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
async acquire(signal?: AbortSignal): Promise<() => void> {
|
|
28
|
+
if (signal?.aborted) throw new this.abortError(ABORT_MESSAGE);
|
|
29
|
+
if (this.active < this.max) {
|
|
30
|
+
this.active += 1;
|
|
31
|
+
return () => this.release();
|
|
32
|
+
}
|
|
33
|
+
await new Promise<void>((resolve, reject) => {
|
|
34
|
+
const waiter = () => {
|
|
35
|
+
signal?.removeEventListener("abort", onAbort);
|
|
36
|
+
resolve();
|
|
37
|
+
};
|
|
38
|
+
const onAbort = () => {
|
|
39
|
+
const idx = this.waiters.indexOf(waiter);
|
|
40
|
+
if (idx >= 0) this.waiters.splice(idx, 1);
|
|
41
|
+
reject(new this.abortError(ABORT_MESSAGE));
|
|
42
|
+
};
|
|
43
|
+
this.waiters.push(waiter);
|
|
44
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
45
|
+
});
|
|
46
|
+
this.active += 1;
|
|
47
|
+
return () => this.release();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Release a held slot and wake the next FIFO waiter. Safe to call more than once. */
|
|
51
|
+
release(): void {
|
|
52
|
+
this.active = Math.max(0, this.active - 1);
|
|
53
|
+
this.waiters.shift()?.();
|
|
54
|
+
}
|
|
55
|
+
}
|