@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.
- package/CHANGELOG.md +418 -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 +29 -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 +584 -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,639 @@
|
|
|
1
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { constants as fsConstants } from "node:fs";
|
|
4
|
+
import { access } from "node:fs/promises";
|
|
5
|
+
import { isAbsolute } from "node:path";
|
|
6
|
+
import { PassThrough } from "node:stream";
|
|
7
|
+
import { assertAbsoluteExecutable, createSecretRedactor } from "./docker-cli.js";
|
|
8
|
+
import { assertPathInsideRoots } from "./path-containment.js";
|
|
9
|
+
import type {
|
|
10
|
+
DisposableSandbox,
|
|
11
|
+
SandboxCapabilities,
|
|
12
|
+
SandboxCloseOptions,
|
|
13
|
+
SandboxExecFileRequest,
|
|
14
|
+
SandboxExecRequest,
|
|
15
|
+
SandboxExportMetadata,
|
|
16
|
+
SandboxStatus,
|
|
17
|
+
SandboxStatusState,
|
|
18
|
+
} from "./sandbox.js";
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_CLEANUP_DEADLINE_MS,
|
|
21
|
+
DEFAULT_IDLE_TIMEOUT_MS,
|
|
22
|
+
DEFAULT_MAX_COMMANDS,
|
|
23
|
+
DEFAULT_MAX_CONCURRENT_EXECS,
|
|
24
|
+
DEFAULT_MAX_ENV_BYTES,
|
|
25
|
+
DEFAULT_MAX_ENV_NAMES,
|
|
26
|
+
DEFAULT_MAX_EXPORT_BYTES,
|
|
27
|
+
DEFAULT_MAX_EXPORT_ENTRIES,
|
|
28
|
+
DEFAULT_MAX_FDS,
|
|
29
|
+
DEFAULT_MAX_OUTPUT_BYTES,
|
|
30
|
+
DEFAULT_MAX_RETAINED_ARTIFACTS,
|
|
31
|
+
DEFAULT_MEMORY_BYTES,
|
|
32
|
+
DEFAULT_STOP_GRACE_MS,
|
|
33
|
+
DEFAULT_WALL_TIME_MS,
|
|
34
|
+
HARD_CLEANUP_DEADLINE_MS,
|
|
35
|
+
HARD_IDLE_TIMEOUT_MS,
|
|
36
|
+
HARD_MAX_COMMANDS,
|
|
37
|
+
HARD_MAX_CONCURRENT_EXECS,
|
|
38
|
+
HARD_MAX_ENV_BYTES,
|
|
39
|
+
HARD_MAX_ENV_NAMES,
|
|
40
|
+
HARD_MAX_EXPORT_BYTES,
|
|
41
|
+
HARD_MAX_EXPORT_ENTRIES,
|
|
42
|
+
HARD_MAX_FDS,
|
|
43
|
+
HARD_MAX_OUTPUT_BYTES,
|
|
44
|
+
HARD_MAX_RETAINED_ARTIFACTS,
|
|
45
|
+
HARD_MEMORY_BYTES,
|
|
46
|
+
HARD_STOP_GRACE_MS,
|
|
47
|
+
HARD_WALL_TIME_MS,
|
|
48
|
+
validateSandboxLimit,
|
|
49
|
+
} from "./sandbox-limits.js";
|
|
50
|
+
import { createImportTarStream, summarizeTarStream } from "./sandbox-tar.js";
|
|
51
|
+
import { Semaphore } from "./semaphore.js";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Network-free native sandbox backend (plan 018 Task 3).
|
|
55
|
+
*
|
|
56
|
+
* Linux-only, created via {@link createNativeSandbox}. Egress denial is a
|
|
57
|
+
* fresh network namespace per command via the OS `unshare` binary; resource
|
|
58
|
+
* limits are POSIX `ulimit` hard caps; path containment is cwd-in-root. No
|
|
59
|
+
* container runtime, no shipped netns tooling, zero runtime dependencies.
|
|
60
|
+
* Platforms/privileges that cannot create a netns fail closed at creation.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
export class NativeSandboxError extends Error {
|
|
64
|
+
readonly code = "ERR_PRISM_NATIVE_SANDBOX";
|
|
65
|
+
constructor(message: string) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.name = "NativeSandboxError";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
72
|
+
const SH = "/bin/sh";
|
|
73
|
+
const WRAPPER_ARGV0 = "prism-native-sh";
|
|
74
|
+
|
|
75
|
+
export interface NativeSandboxLimitOptions {
|
|
76
|
+
readonly wallTimeMs?: number;
|
|
77
|
+
readonly idleTimeoutMs?: number;
|
|
78
|
+
readonly memoryBytes?: number;
|
|
79
|
+
readonly maxFds?: number;
|
|
80
|
+
readonly maxCommands?: number;
|
|
81
|
+
readonly maxConcurrentExecs?: number;
|
|
82
|
+
readonly maxEnvNames?: number;
|
|
83
|
+
readonly maxEnvBytes?: number;
|
|
84
|
+
readonly maxOutputBytes?: number;
|
|
85
|
+
readonly maxExportEntries?: number;
|
|
86
|
+
readonly maxExportBytes?: number;
|
|
87
|
+
readonly maxRetainedArtifacts?: number;
|
|
88
|
+
readonly stopGraceMs?: number;
|
|
89
|
+
readonly cleanupDeadlineMs?: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ResolvedNativeSandboxLimits {
|
|
93
|
+
readonly wallTimeMs: number;
|
|
94
|
+
readonly idleTimeoutMs: number;
|
|
95
|
+
readonly memoryBytes: number;
|
|
96
|
+
readonly maxFds: number;
|
|
97
|
+
readonly maxCommands: number;
|
|
98
|
+
readonly maxConcurrentExecs: number;
|
|
99
|
+
readonly maxEnvNames: number;
|
|
100
|
+
readonly maxEnvBytes: number;
|
|
101
|
+
readonly maxOutputBytes: number;
|
|
102
|
+
readonly maxExportEntries: number;
|
|
103
|
+
readonly maxExportBytes: number;
|
|
104
|
+
readonly maxRetainedArtifacts: number;
|
|
105
|
+
readonly stopGraceMs: number;
|
|
106
|
+
readonly cleanupDeadlineMs: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const SPECS: Record<keyof NativeSandboxLimitOptions, [number, number]> = {
|
|
110
|
+
wallTimeMs: [DEFAULT_WALL_TIME_MS, HARD_WALL_TIME_MS],
|
|
111
|
+
idleTimeoutMs: [DEFAULT_IDLE_TIMEOUT_MS, HARD_IDLE_TIMEOUT_MS],
|
|
112
|
+
memoryBytes: [DEFAULT_MEMORY_BYTES, HARD_MEMORY_BYTES],
|
|
113
|
+
maxFds: [DEFAULT_MAX_FDS, HARD_MAX_FDS],
|
|
114
|
+
maxCommands: [DEFAULT_MAX_COMMANDS, HARD_MAX_COMMANDS],
|
|
115
|
+
maxConcurrentExecs: [DEFAULT_MAX_CONCURRENT_EXECS, HARD_MAX_CONCURRENT_EXECS],
|
|
116
|
+
maxEnvNames: [DEFAULT_MAX_ENV_NAMES, HARD_MAX_ENV_NAMES],
|
|
117
|
+
maxEnvBytes: [DEFAULT_MAX_ENV_BYTES, HARD_MAX_ENV_BYTES],
|
|
118
|
+
maxOutputBytes: [DEFAULT_MAX_OUTPUT_BYTES, HARD_MAX_OUTPUT_BYTES],
|
|
119
|
+
maxExportEntries: [DEFAULT_MAX_EXPORT_ENTRIES, HARD_MAX_EXPORT_ENTRIES],
|
|
120
|
+
maxExportBytes: [DEFAULT_MAX_EXPORT_BYTES, HARD_MAX_EXPORT_BYTES],
|
|
121
|
+
maxRetainedArtifacts: [DEFAULT_MAX_RETAINED_ARTIFACTS, HARD_MAX_RETAINED_ARTIFACTS],
|
|
122
|
+
stopGraceMs: [DEFAULT_STOP_GRACE_MS, HARD_STOP_GRACE_MS],
|
|
123
|
+
cleanupDeadlineMs: [DEFAULT_CLEANUP_DEADLINE_MS, HARD_CLEANUP_DEADLINE_MS],
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export function resolveNativeSandboxLimits(input: NativeSandboxLimitOptions = {}): ResolvedNativeSandboxLimits {
|
|
127
|
+
const resolved = {} as Record<keyof ResolvedNativeSandboxLimits, number>;
|
|
128
|
+
for (const [name, [fallback, hardCap]] of Object.entries(SPECS)) {
|
|
129
|
+
const value = input[name as keyof NativeSandboxLimitOptions] ?? fallback;
|
|
130
|
+
resolved[name as keyof ResolvedNativeSandboxLimits] = validateSandboxLimit(name, value as number, hardCap);
|
|
131
|
+
}
|
|
132
|
+
return resolved as ResolvedNativeSandboxLimits;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface CreateNativeSandboxOptions {
|
|
136
|
+
/** Absolute host directory that IS the sandbox workspace; cwd of every command defaults here. */
|
|
137
|
+
readonly root: string;
|
|
138
|
+
/** Absolute path to the `unshare` executable (default: resolved from PATH at creation). */
|
|
139
|
+
readonly unshare?: string;
|
|
140
|
+
readonly limits?: NativeSandboxLimitOptions;
|
|
141
|
+
/** Exact environment allow-list for every command (host env is never inherited). */
|
|
142
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
143
|
+
/** Secret canaries redacted from error text. */
|
|
144
|
+
readonly secrets?: readonly string[];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Which `unshare` invocation granted the netns at creation (recorded once). */
|
|
148
|
+
export type NativeUnshareMode = "plain" | "maproot";
|
|
149
|
+
|
|
150
|
+
export interface NativeSpawnCommand {
|
|
151
|
+
readonly file: string;
|
|
152
|
+
readonly args: readonly string[];
|
|
153
|
+
readonly env: Record<string, string>;
|
|
154
|
+
readonly cwd: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Build the unshare + sh wrapper argv. @internal test helper (mirrors buildDockerCreateArgsForTest). */
|
|
158
|
+
export function buildNativeSpawnCommand(
|
|
159
|
+
request: SandboxExecRequest | SandboxExecFileRequest,
|
|
160
|
+
options: {
|
|
161
|
+
readonly root: string;
|
|
162
|
+
readonly unshare: string;
|
|
163
|
+
readonly mode: NativeUnshareMode;
|
|
164
|
+
readonly limits: ResolvedNativeSandboxLimits;
|
|
165
|
+
readonly env: Readonly<Record<string, string>>;
|
|
166
|
+
},
|
|
167
|
+
): NativeSpawnCommand {
|
|
168
|
+
const modeFlags = options.mode === "plain" ? ["--net"] : ["--net", "--map-root-user"];
|
|
169
|
+
// one ulimit per call: dash rejects multiple options in a single invocation
|
|
170
|
+
const ulimit = [
|
|
171
|
+
`ulimit -v ${Math.ceil(options.limits.memoryBytes / 1024)} || exit 126`,
|
|
172
|
+
`ulimit -t ${Math.ceil(options.limits.wallTimeMs / 1000)} || exit 126`,
|
|
173
|
+
`ulimit -n ${options.limits.maxFds} || exit 126`,
|
|
174
|
+
].join("; ");
|
|
175
|
+
if ("command" in request) {
|
|
176
|
+
return {
|
|
177
|
+
file: options.unshare,
|
|
178
|
+
args: [...modeFlags, SH, "-c", `${ulimit}; ${request.command}`, WRAPPER_ARGV0],
|
|
179
|
+
env: { ...options.env },
|
|
180
|
+
cwd: request.cwd,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
// execFile: file/args are argv to `exec "$@"` — never shell-interpolated.
|
|
184
|
+
return {
|
|
185
|
+
file: options.unshare,
|
|
186
|
+
args: [...modeFlags, SH, "-c", `${ulimit}; exec "$@"`, WRAPPER_ARGV0, request.file, ...request.args],
|
|
187
|
+
env: { ...options.env },
|
|
188
|
+
cwd: request.cwd ?? options.root,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function validateEnv(env: Readonly<Record<string, string>> | undefined, limits: ResolvedNativeSandboxLimits): Record<string, string> {
|
|
193
|
+
const entries = Object.entries(env ?? {});
|
|
194
|
+
if (entries.length > limits.maxEnvNames) {
|
|
195
|
+
throw new NativeSandboxError(`env exceeds maxEnvNames (${limits.maxEnvNames})`);
|
|
196
|
+
}
|
|
197
|
+
let bytes = 0;
|
|
198
|
+
const out: Record<string, string> = {};
|
|
199
|
+
for (const [name, value] of entries) {
|
|
200
|
+
if (!ENV_NAME_RE.test(name)) {
|
|
201
|
+
throw new NativeSandboxError(`invalid env name: ${name}`);
|
|
202
|
+
}
|
|
203
|
+
bytes += Buffer.byteLength(name, "utf8") + Buffer.byteLength(value, "utf8");
|
|
204
|
+
if (bytes > limits.maxEnvBytes) {
|
|
205
|
+
throw new NativeSandboxError(`env exceeds maxEnvBytes (${limits.maxEnvBytes})`);
|
|
206
|
+
}
|
|
207
|
+
out[name] = value;
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Pure run-request validation: NUL bytes are rejected in commands and execFile paths/args. */
|
|
213
|
+
export function validateRunArgs(request: NativeRunRequest): void {
|
|
214
|
+
if (request.command !== undefined) {
|
|
215
|
+
if (typeof request.command !== "string" || request.command.includes("\0")) {
|
|
216
|
+
throw new NativeSandboxError("exec requires a command string without NUL");
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
if (!request.file || request.file.includes("\0")) {
|
|
220
|
+
throw new NativeSandboxError("execFile requires a non-empty file path without NUL");
|
|
221
|
+
}
|
|
222
|
+
if (!Array.isArray(request.args) || request.args.some((a) => typeof a !== "string" || a.includes("\0"))) {
|
|
223
|
+
throw new NativeSandboxError("execFile args must be a string array without NUL");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Validates the cwd (absolute, inside the root, symlink-safe) and the env
|
|
230
|
+
* allow-list; returns the resolved cwd and validated env. Pure against the
|
|
231
|
+
* filesystem, so it is testable without a network namespace.
|
|
232
|
+
*/
|
|
233
|
+
export async function validateCwdAndEnv(
|
|
234
|
+
request: NativeRunRequest,
|
|
235
|
+
root: string,
|
|
236
|
+
limits: ResolvedNativeSandboxLimits,
|
|
237
|
+
): Promise<{ cwd: string; env: Record<string, string> }> {
|
|
238
|
+
const cwd = request.cwd ?? root;
|
|
239
|
+
if (!isAbsolute(cwd) || !(await assertPathInsideRoots([root], cwd))) {
|
|
240
|
+
throw new NativeSandboxError("cwd must be an absolute path inside the sandbox root");
|
|
241
|
+
}
|
|
242
|
+
return { cwd, env: validateEnv(request.env as Readonly<Record<string, string>> | undefined, limits) };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function preflightUnshare(unshare: string): Promise<NativeUnshareMode> {
|
|
246
|
+
for (const mode of ["plain", "maproot"] as const) {
|
|
247
|
+
const args = mode === "plain" ? ["--net", "true"] : ["--net", "--map-root-user", "true"];
|
|
248
|
+
try {
|
|
249
|
+
const exitCode = await new Promise<number | null>((resolve) => {
|
|
250
|
+
const child = spawn(unshare, args, { stdio: "ignore", env: { PATH: "/usr/bin:/bin", LANG: "C" } });
|
|
251
|
+
child.on("error", () => resolve(null));
|
|
252
|
+
child.on("close", (code) => resolve(code));
|
|
253
|
+
});
|
|
254
|
+
if (exitCode === 0) return mode;
|
|
255
|
+
} catch {
|
|
256
|
+
// try next mode
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
throw new NativeSandboxError(
|
|
260
|
+
`network-free native sandbox unavailable: 'unshare' could not create a network namespace ` +
|
|
261
|
+
`(need root/CAP_SYS_ADMIN or unprivileged user namespaces enabled). ` +
|
|
262
|
+
`Refusing to create a network-enabled sandbox; use the Docker backend or grant netns privileges.`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
interface NativeRunRequest {
|
|
267
|
+
readonly command?: string;
|
|
268
|
+
readonly file?: string;
|
|
269
|
+
readonly args?: readonly string[];
|
|
270
|
+
readonly cwd?: string;
|
|
271
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
272
|
+
readonly onData?: (data: Buffer) => void;
|
|
273
|
+
readonly signal?: AbortSignal;
|
|
274
|
+
readonly timeout?: number;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Truthful native capability metadata: commands run in a fresh netns with
|
|
279
|
+
* egress denied (networkIsolated, egressRestricted), but there is NO
|
|
280
|
+
* filesystem, process, or privilege isolation — commands run as the invoking
|
|
281
|
+
* OS user with full host-tree access. Never a security boundary for untrusted
|
|
282
|
+
* code; pair with the Docker backend.
|
|
283
|
+
*/
|
|
284
|
+
const NATIVE_SANDBOX_CAPABILITIES: SandboxCapabilities = Object.freeze({
|
|
285
|
+
workspaceCoherent: true,
|
|
286
|
+
filesystemIsolated: false,
|
|
287
|
+
networkIsolated: true,
|
|
288
|
+
processIsolated: false,
|
|
289
|
+
privilegeIsolated: false,
|
|
290
|
+
egressRestricted: true,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
class NativeSandboxSession implements DisposableSandbox {
|
|
294
|
+
readonly id: string;
|
|
295
|
+
readonly capabilities: SandboxCapabilities = NATIVE_SANDBOX_CAPABILITIES;
|
|
296
|
+
private _lastExportIdentity: SandboxExportMetadata | undefined;
|
|
297
|
+
private state: SandboxStatusState = "running";
|
|
298
|
+
private commandCount = 0;
|
|
299
|
+
private retainedArtifacts = 0;
|
|
300
|
+
private readonly startedAt = Date.now();
|
|
301
|
+
private lastActivityAt = Date.now();
|
|
302
|
+
private readonly active = new Set<ChildProcess>();
|
|
303
|
+
private readonly execLock: Semaphore;
|
|
304
|
+
private readonly redact: (text: string) => string;
|
|
305
|
+
private readonly closing: Promise<SandboxExportMetadata | undefined> | undefined;
|
|
306
|
+
|
|
307
|
+
constructor(
|
|
308
|
+
private readonly opts: {
|
|
309
|
+
readonly root: string;
|
|
310
|
+
readonly unshare: string;
|
|
311
|
+
readonly mode: NativeUnshareMode;
|
|
312
|
+
readonly limits: ResolvedNativeSandboxLimits;
|
|
313
|
+
readonly env: Record<string, string>;
|
|
314
|
+
readonly secrets: readonly string[];
|
|
315
|
+
},
|
|
316
|
+
) {
|
|
317
|
+
this.id = randomUUID();
|
|
318
|
+
this.execLock = new Semaphore(opts.limits.maxConcurrentExecs, NativeSandboxError);
|
|
319
|
+
this.redact = createSecretRedactor(opts.secrets);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
get lastExportIdentity(): SandboxExportMetadata | undefined {
|
|
323
|
+
return this._lastExportIdentity;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private touch(): void {
|
|
327
|
+
this.lastActivityAt = Date.now();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private remainingWallMs(): number {
|
|
331
|
+
return Math.max(1, this.opts.limits.wallTimeMs - (Date.now() - this.startedAt));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private assertActive(): void {
|
|
335
|
+
if (this.state !== "running") throw new NativeSandboxError(`sandbox is ${this.state}`);
|
|
336
|
+
const now = Date.now();
|
|
337
|
+
if (now - this.startedAt > this.opts.limits.wallTimeMs) {
|
|
338
|
+
this.state = "failed";
|
|
339
|
+
throw new NativeSandboxError("sandbox wall time exceeded");
|
|
340
|
+
}
|
|
341
|
+
if (now - this.lastActivityAt > this.opts.limits.idleTimeoutMs) {
|
|
342
|
+
this.state = "failed";
|
|
343
|
+
throw new NativeSandboxError("sandbox idle timeout exceeded");
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** SIGKILL the whole process group (sh + command + grandchildren). */
|
|
348
|
+
private groupKill(signal: NodeJS.Signals = "SIGKILL"): void {
|
|
349
|
+
for (const child of this.active) {
|
|
350
|
+
try {
|
|
351
|
+
process.kill(-child.pid!, signal);
|
|
352
|
+
} catch {
|
|
353
|
+
// already gone
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private async waitActive(graceMs: number): Promise<void> {
|
|
359
|
+
const deadline = Date.now() + graceMs;
|
|
360
|
+
while (this.active.size > 0 && Date.now() < deadline) {
|
|
361
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
362
|
+
}
|
|
363
|
+
if (this.active.size > 0) this.groupKill("SIGKILL");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async status(): Promise<SandboxStatus> {
|
|
367
|
+
return {
|
|
368
|
+
id: this.id,
|
|
369
|
+
state: this.state,
|
|
370
|
+
image: "native:linux",
|
|
371
|
+
startedAt: this.startedAt,
|
|
372
|
+
commandCount: this.commandCount,
|
|
373
|
+
lastActivityAt: this.lastActivityAt,
|
|
374
|
+
...(this._lastExportIdentity ? { lastExportIdentity: this._lastExportIdentity } : {}),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async exec(request: SandboxExecRequest): Promise<{ exitCode: number | null }> {
|
|
379
|
+
return this.run({
|
|
380
|
+
command: request.command,
|
|
381
|
+
cwd: request.cwd,
|
|
382
|
+
env: request.env,
|
|
383
|
+
onData: request.onData,
|
|
384
|
+
signal: request.signal,
|
|
385
|
+
timeout: request.timeout,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async execFile(request: SandboxExecFileRequest): Promise<{ exitCode: number | null }> {
|
|
390
|
+
return this.run({
|
|
391
|
+
file: request.file,
|
|
392
|
+
args: request.args,
|
|
393
|
+
cwd: request.cwd,
|
|
394
|
+
env: request.env as NodeJS.ProcessEnv | undefined,
|
|
395
|
+
onData: request.onData,
|
|
396
|
+
signal: request.signal,
|
|
397
|
+
timeout: request.timeout,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private async run(request: NativeRunRequest): Promise<{ exitCode: number | null }> {
|
|
402
|
+
this.assertActive();
|
|
403
|
+
validateRunArgs(request);
|
|
404
|
+
if (this.commandCount >= this.opts.limits.maxCommands) {
|
|
405
|
+
throw new NativeSandboxError(`sandbox exceeded maxCommands (${this.opts.limits.maxCommands})`);
|
|
406
|
+
}
|
|
407
|
+
const release = await this.execLock.acquire(request.signal);
|
|
408
|
+
this.commandCount += 1;
|
|
409
|
+
this.touch();
|
|
410
|
+
try {
|
|
411
|
+
this.assertActive();
|
|
412
|
+
const { cwd, env: extraEnv } = await validateCwdAndEnv(request, this.opts.root, this.opts.limits);
|
|
413
|
+
const baseEnv = Object.keys(this.opts.env).length > 0 ? this.opts.env : { PATH: process.env.PATH ?? "/usr/bin:/bin" };
|
|
414
|
+
const command = buildNativeSpawnCommand(
|
|
415
|
+
request.command !== undefined ? { command: request.command, cwd } : { file: request.file!, args: request.args!, cwd },
|
|
416
|
+
{
|
|
417
|
+
root: this.opts.root,
|
|
418
|
+
unshare: this.opts.unshare,
|
|
419
|
+
mode: this.opts.mode,
|
|
420
|
+
limits: this.opts.limits,
|
|
421
|
+
env: { ...baseEnv, ...extraEnv },
|
|
422
|
+
},
|
|
423
|
+
);
|
|
424
|
+
const timeoutMs = request.timeout !== undefined ? Math.min(request.timeout, this.remainingWallMs()) : this.remainingWallMs();
|
|
425
|
+
const result = await this.spawnWithCaps(command, request, timeoutMs);
|
|
426
|
+
this.touch();
|
|
427
|
+
return { exitCode: result };
|
|
428
|
+
} catch (error) {
|
|
429
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
430
|
+
throw new NativeSandboxError(this.redact(message));
|
|
431
|
+
} finally {
|
|
432
|
+
release();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private spawnWithCaps(command: NativeSpawnCommand, request: NativeRunRequest, timeoutMs: number): Promise<number | null> {
|
|
437
|
+
return new Promise((resolve, reject) => {
|
|
438
|
+
let settled = false;
|
|
439
|
+
let child: ChildProcess;
|
|
440
|
+
try {
|
|
441
|
+
// detached: true -> own process group; group kill covers sh + command + grandchildren
|
|
442
|
+
child = spawn(command.file, [...command.args], {
|
|
443
|
+
cwd: command.cwd,
|
|
444
|
+
env: command.env,
|
|
445
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
446
|
+
detached: true,
|
|
447
|
+
windowsHide: true,
|
|
448
|
+
});
|
|
449
|
+
} catch (error) {
|
|
450
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
451
|
+
reject(new NativeSandboxError(message));
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
this.active.add(child);
|
|
455
|
+
|
|
456
|
+
const finalize = (exitCode: number | null) => {
|
|
457
|
+
if (settled) return;
|
|
458
|
+
settled = true;
|
|
459
|
+
cleanup();
|
|
460
|
+
this.active.delete(child);
|
|
461
|
+
resolve(exitCode);
|
|
462
|
+
};
|
|
463
|
+
const fail = (error: Error) => {
|
|
464
|
+
if (settled) return;
|
|
465
|
+
settled = true;
|
|
466
|
+
cleanup();
|
|
467
|
+
this.active.delete(child);
|
|
468
|
+
this.groupKill("SIGKILL");
|
|
469
|
+
reject(error);
|
|
470
|
+
};
|
|
471
|
+
const noteBytes = (chunk: Buffer) => {
|
|
472
|
+
outputBytes += chunk.byteLength;
|
|
473
|
+
if (outputBytes > this.opts.limits.maxOutputBytes) {
|
|
474
|
+
fail(new NativeSandboxError(`command output exceeded maxOutputBytes (${this.opts.limits.maxOutputBytes})`));
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
const onAbort = () => {
|
|
478
|
+
this.groupKill("SIGKILL");
|
|
479
|
+
};
|
|
480
|
+
const onTimeout = () => {
|
|
481
|
+
this.groupKill("SIGTERM");
|
|
482
|
+
setTimeout(() => this.groupKill("SIGKILL"), Math.min(this.opts.limits.stopGraceMs, 1000));
|
|
483
|
+
};
|
|
484
|
+
const timer = setTimeout(onTimeout, timeoutMs);
|
|
485
|
+
const cleanup = () => {
|
|
486
|
+
clearTimeout(timer);
|
|
487
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
488
|
+
child.stdout?.removeAllListeners();
|
|
489
|
+
child.stderr?.removeAllListeners();
|
|
490
|
+
child.removeAllListeners();
|
|
491
|
+
};
|
|
492
|
+
let outputBytes = 0;
|
|
493
|
+
|
|
494
|
+
child.on("error", (error) => fail(error));
|
|
495
|
+
child.on("close", (code) => finalize(code));
|
|
496
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
497
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
498
|
+
noteBytes(chunk);
|
|
499
|
+
request.onData?.(chunk);
|
|
500
|
+
});
|
|
501
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
502
|
+
noteBytes(chunk);
|
|
503
|
+
request.onData?.(chunk);
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async stop(options?: { graceMs?: number; signal?: AbortSignal }): Promise<void> {
|
|
509
|
+
if (this.state === "removed" || this.state === "stopped") return;
|
|
510
|
+
const graceMs = Math.min(options?.graceMs ?? this.opts.limits.stopGraceMs, this.opts.limits.stopGraceMs);
|
|
511
|
+
this.groupKill("SIGTERM");
|
|
512
|
+
await this.waitActive(graceMs);
|
|
513
|
+
if (this.active.size > 0) this.groupKill("SIGKILL");
|
|
514
|
+
this.state = "stopped";
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async kill(_options?: { signal?: AbortSignal }): Promise<void> {
|
|
518
|
+
if (this.state === "removed") return;
|
|
519
|
+
this.groupKill("SIGKILL");
|
|
520
|
+
this.state = "stopped";
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private async exportWorkspace(write: NonNullable<SandboxCloseOptions["export"]>, signal?: AbortSignal): Promise<SandboxExportMetadata> {
|
|
524
|
+
if (this.retainedArtifacts >= this.opts.limits.maxRetainedArtifacts) {
|
|
525
|
+
throw new NativeSandboxError(`sandbox exceeded maxRetainedArtifacts (${this.opts.limits.maxRetainedArtifacts})`);
|
|
526
|
+
}
|
|
527
|
+
const bounds = {
|
|
528
|
+
maxEntries: this.opts.limits.maxExportEntries,
|
|
529
|
+
maxBytes: this.opts.limits.maxExportBytes,
|
|
530
|
+
};
|
|
531
|
+
const tee = new PassThrough();
|
|
532
|
+
const summaryPromise = summarizeTarStream(tee, bounds);
|
|
533
|
+
const hostStream = new PassThrough();
|
|
534
|
+
tee.on("data", (chunk: Buffer) => hostStream.write(chunk));
|
|
535
|
+
tee.on("end", () => hostStream.end());
|
|
536
|
+
tee.on("error", (error) => hostStream.destroy(error));
|
|
537
|
+
|
|
538
|
+
const run = (async () => {
|
|
539
|
+
for await (const chunk of createImportTarStream(this.opts.root, bounds)) {
|
|
540
|
+
if (signal?.aborted) throw new NativeSandboxError("sandbox export aborted");
|
|
541
|
+
tee.write(chunk);
|
|
542
|
+
}
|
|
543
|
+
tee.end();
|
|
544
|
+
})();
|
|
545
|
+
try {
|
|
546
|
+
const summary = await summaryPromise;
|
|
547
|
+
await run;
|
|
548
|
+
const metadata: SandboxExportMetadata = {
|
|
549
|
+
sha256: summary.sha256,
|
|
550
|
+
entryCount: summary.entryCount,
|
|
551
|
+
byteCount: summary.byteCount,
|
|
552
|
+
format: "tar",
|
|
553
|
+
};
|
|
554
|
+
await write(hostStream, metadata);
|
|
555
|
+
this.retainedArtifacts += 1;
|
|
556
|
+
this._lastExportIdentity = metadata;
|
|
557
|
+
this.touch();
|
|
558
|
+
return metadata;
|
|
559
|
+
} catch (error) {
|
|
560
|
+
hostStream.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
561
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
562
|
+
throw new NativeSandboxError(this.redact(message));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
close(options?: SandboxCloseOptions): Promise<SandboxExportMetadata | undefined> {
|
|
567
|
+
if (this.closing) return this.closing;
|
|
568
|
+
return (async () => {
|
|
569
|
+
let metadata: SandboxExportMetadata | undefined;
|
|
570
|
+
try {
|
|
571
|
+
if (options?.export && this.state === "running") {
|
|
572
|
+
metadata = await this.exportWorkspace(options.export, options.signal);
|
|
573
|
+
}
|
|
574
|
+
} catch (error) {
|
|
575
|
+
this.groupKill("SIGKILL");
|
|
576
|
+
this.state = "removed";
|
|
577
|
+
throw error;
|
|
578
|
+
}
|
|
579
|
+
this.groupKill("SIGTERM");
|
|
580
|
+
await this.waitActive(this.opts.limits.stopGraceMs);
|
|
581
|
+
if (this.active.size > 0) this.groupKill("SIGKILL");
|
|
582
|
+
this.state = "removed";
|
|
583
|
+
return metadata;
|
|
584
|
+
})();
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async function resolveUnsharePath(unshare: string | undefined): Promise<string> {
|
|
589
|
+
if (unshare) {
|
|
590
|
+
return await assertAbsoluteExecutable(unshare, "unshare");
|
|
591
|
+
}
|
|
592
|
+
const candidates = (process.env.PATH ?? "/usr/bin:/bin").split(":").filter(Boolean);
|
|
593
|
+
for (const dir of candidates) {
|
|
594
|
+
const candidate = `${dir}/unshare`;
|
|
595
|
+
try {
|
|
596
|
+
await access(candidate, fsConstants.X_OK);
|
|
597
|
+
return candidate;
|
|
598
|
+
} catch {
|
|
599
|
+
// keep probing
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
throw new NativeSandboxError(
|
|
603
|
+
"network-free native sandbox unavailable: 'unshare' executable not found on PATH. " +
|
|
604
|
+
"Refusing to create a network-enabled sandbox; use the Docker backend or install util-linux.",
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export async function createNativeSandbox(options: CreateNativeSandboxOptions): Promise<DisposableSandbox> {
|
|
609
|
+
if (process.platform !== "linux") {
|
|
610
|
+
throw new NativeSandboxError(
|
|
611
|
+
`network-free native sandbox is supported on linux only (this host: ${process.platform}). ` +
|
|
612
|
+
`Egress denial cannot be enforced by construction on this platform; refusing to create a network-enabled sandbox. ` +
|
|
613
|
+
`Use the Docker backend.`,
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
if (!isAbsolute(options.root)) {
|
|
617
|
+
throw new NativeSandboxError("root must be an absolute path");
|
|
618
|
+
}
|
|
619
|
+
try {
|
|
620
|
+
await access(options.root, fsConstants.R_OK);
|
|
621
|
+
} catch {
|
|
622
|
+
throw new NativeSandboxError(`root is missing or unreadable: ${options.root}`);
|
|
623
|
+
}
|
|
624
|
+
const unshare = await resolveUnsharePath(options.unshare).catch((error: unknown) => {
|
|
625
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
626
|
+
throw new NativeSandboxError(message);
|
|
627
|
+
});
|
|
628
|
+
const mode = await preflightUnshare(unshare);
|
|
629
|
+
const limits = resolveNativeSandboxLimits(options.limits);
|
|
630
|
+
const env = validateEnv(options.env, limits);
|
|
631
|
+
return new NativeSandboxSession({
|
|
632
|
+
root: options.root,
|
|
633
|
+
unshare,
|
|
634
|
+
mode,
|
|
635
|
+
limits,
|
|
636
|
+
env,
|
|
637
|
+
secrets: options.secrets ?? [],
|
|
638
|
+
});
|
|
639
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function isNodeErrorCode(error: unknown, code: string): boolean {
|
|
5
|
+
return error instanceof Error && "code" in error && (error as { code?: unknown }).code === code;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function isPathInside(root: string, target: string): boolean {
|
|
9
|
+
const from = resolve(root);
|
|
10
|
+
const to = resolve(target);
|
|
11
|
+
const rel = relative(from, to);
|
|
12
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `realpath` of `target`, or, for a path that does not exist yet, the `realpath` of its nearest
|
|
17
|
+
* existing ancestor joined with the missing tail, so a create under new directories is still checked
|
|
18
|
+
* against the real location. The tail does not exist, so it holds no symlink. Any error other than
|
|
19
|
+
* `ENOENT` (or reaching the filesystem root) returns `undefined`, which callers treat as outside.
|
|
20
|
+
*/
|
|
21
|
+
export async function realpathNearest(target: string): Promise<string | undefined> {
|
|
22
|
+
const absolute = resolve(target);
|
|
23
|
+
const tail: string[] = [];
|
|
24
|
+
let current = absolute;
|
|
25
|
+
for (;;) {
|
|
26
|
+
try {
|
|
27
|
+
return join(await realpath(current), ...tail.reverse());
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (!isNodeErrorCode(error, "ENOENT")) return undefined;
|
|
30
|
+
const parent = dirname(current);
|
|
31
|
+
if (parent === current) return undefined;
|
|
32
|
+
tail.push(basename(current));
|
|
33
|
+
current = parent;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Resolve symlinks on root and target; fail closed when containment cannot be verified. */
|
|
39
|
+
export async function isPathInsideReal(root: string, target: string): Promise<boolean> {
|
|
40
|
+
let from: string;
|
|
41
|
+
try {
|
|
42
|
+
from = await realpath(root);
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const to = await realpathNearest(target);
|
|
47
|
+
return to !== undefined && isPathInside(from, to);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function assertPathInsideRoots(roots: readonly string[], target: string): Promise<boolean> {
|
|
51
|
+
for (const root of roots) {
|
|
52
|
+
if (await isPathInsideReal(root, target)) return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|