@legionworks/facet 1.9.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/LICENSE-APACHE +202 -0
- package/LICENSE-MIT +23 -0
- package/README.md +158 -0
- package/dist/gallery/chunk-4g8rem85.css +656 -0
- package/dist/gallery/chunk-c6s9k9ty.js +5970 -0
- package/dist/gallery/frame/artifact.css +8424 -0
- package/dist/gallery/frame/chunks/abnfDiagram-N423BO3Z-jj74szbm.js +155 -0
- package/dist/gallery/frame/chunks/architecture-TIHT7OUA-cvdjn7dt.js +10 -0
- package/dist/gallery/frame/chunks/architectureDiagram-T3A2C74G-n4z77eg6.js +8675 -0
- package/dist/gallery/frame/chunks/blockDiagram-VBNYF7ZC-7xsqzrd1.js +3529 -0
- package/dist/gallery/frame/chunks/c4Diagram-5PPSVZJV-z2zjs7wx.js +2413 -0
- package/dist/gallery/frame/chunks/classDiagram-JCYQIIEL-ve8rngqp.js +47 -0
- package/dist/gallery/frame/chunks/classDiagram-v2-OCEON4UE-8cfaxb8z.js +47 -0
- package/dist/gallery/frame/chunks/cose-bilkent-JH36ORCC-sn92qexx.js +4822 -0
- package/dist/gallery/frame/chunks/cynefin-VYW2F7L2-tqjv913d.js +10 -0
- package/dist/gallery/frame/chunks/cynefinDiagram-MW4NZA55-c1c0dj12.js +500 -0
- package/dist/gallery/frame/chunks/dagre-VZM6K2ZE-7tyg4yt2.js +476 -0
- package/dist/gallery/frame/chunks/diagram-7IWD3JNH-y4eng9sc.js +526 -0
- package/dist/gallery/frame/chunks/diagram-B4RE2ZJO-rn3p7j5h.js +673 -0
- package/dist/gallery/frame/chunks/diagram-LBJQPF4R-w4a2cm9q.js +255 -0
- package/dist/gallery/frame/chunks/diagram-Q27KOJAE-aqc2v7wy.js +575 -0
- package/dist/gallery/frame/chunks/diagram-UB23O5K3-xncy8nyd.js +355 -0
- package/dist/gallery/frame/chunks/ebnfDiagram-BXEA7PRR-acwgxz16.js +175 -0
- package/dist/gallery/frame/chunks/erDiagram-JOGREHBK-mzp5nswt.js +1332 -0
- package/dist/gallery/frame/chunks/eventmodeling-45OFAUF4-sg07qr0c.js +10 -0
- package/dist/gallery/frame/chunks/flowDiagram-UKHOOZJN-2e5ygs44.js +29 -0
- package/dist/gallery/frame/chunks/ganttDiagram-PKOTCBZU-p2hhh7hr.js +2620 -0
- package/dist/gallery/frame/chunks/gitGraph-TEB2WS4Q-j6kw0naa.js +10 -0
- package/dist/gallery/frame/chunks/gitGraphDiagram-DS77QQ5N-4z9z8wjm.js +1347 -0
- package/dist/gallery/frame/chunks/info-DKCQHKI2-mpt80jd3.js +10 -0
- package/dist/gallery/frame/chunks/infoDiagram-6WML65LV-5rqm3mgg.js +65 -0
- package/dist/gallery/frame/chunks/ishikawaDiagram-WSZJBQD7-b51z96jy.js +975 -0
- package/dist/gallery/frame/chunks/journeyDiagram-NVQOT4AX-9jbfp0pk.js +1272 -0
- package/dist/gallery/frame/chunks/kanban-definition-27J2QSJJ-4x0vx4se.js +1116 -0
- package/dist/gallery/frame/chunks/katex-xfgk95tv.js +14034 -0
- package/dist/gallery/frame/chunks/markdown-1s5vwkf2.js +9 -0
- package/dist/gallery/frame/chunks/markdown-2h6bwsms.js +3239 -0
- package/dist/gallery/frame/chunks/markdown-2x113yh8.js +107 -0
- package/dist/gallery/frame/chunks/markdown-30b7rwmp.js +1943 -0
- package/dist/gallery/frame/chunks/markdown-356bfj4g.js +2031 -0
- package/dist/gallery/frame/chunks/markdown-44n9ea69.js +2406 -0
- package/dist/gallery/frame/chunks/markdown-56enrf2r.js +39 -0
- package/dist/gallery/frame/chunks/markdown-5avymm8t.js +725 -0
- package/dist/gallery/frame/chunks/markdown-6r4svars.js +33 -0
- package/dist/gallery/frame/chunks/markdown-6zz8efx2.js +24 -0
- package/dist/gallery/frame/chunks/markdown-7jp6prnb.js +19 -0
- package/dist/gallery/frame/chunks/markdown-809k7q41.js +55 -0
- package/dist/gallery/frame/chunks/markdown-82xmzhzd.js +1076 -0
- package/dist/gallery/frame/chunks/markdown-92n37gd4.js +7627 -0
- package/dist/gallery/frame/chunks/markdown-ag2sssf8.js +36 -0
- package/dist/gallery/frame/chunks/markdown-b4s7ntq2.js +19 -0
- package/dist/gallery/frame/chunks/markdown-c9k0fvp1.js +100 -0
- package/dist/gallery/frame/chunks/markdown-ceefymn8.js +125 -0
- package/dist/gallery/frame/chunks/markdown-d1wfbs70.js +243 -0
- package/dist/gallery/frame/chunks/markdown-dy5sgjhc.js +11022 -0
- package/dist/gallery/frame/chunks/markdown-dz3z5w30.js +30868 -0
- package/dist/gallery/frame/chunks/markdown-eg6ngz7y.js +30340 -0
- package/dist/gallery/frame/chunks/markdown-ff5wvb43.js +1149 -0
- package/dist/gallery/frame/chunks/markdown-ffg06nfz.js +86 -0
- package/dist/gallery/frame/chunks/markdown-fvfncadf.js +472 -0
- package/dist/gallery/frame/chunks/markdown-gvyszqpv.js +19 -0
- package/dist/gallery/frame/chunks/markdown-gx1p0ew0.js +88 -0
- package/dist/gallery/frame/chunks/markdown-gxye5frz.js +65 -0
- package/dist/gallery/frame/chunks/markdown-h7pzv34e.js +105 -0
- package/dist/gallery/frame/chunks/markdown-hab789t9.js +47 -0
- package/dist/gallery/frame/chunks/markdown-hjp6sj8t.js +125 -0
- package/dist/gallery/frame/chunks/markdown-jka64qgp.js +1993 -0
- package/dist/gallery/frame/chunks/markdown-k8xkn8mw.js +56 -0
- package/dist/gallery/frame/chunks/markdown-m7frq8x8.js +112 -0
- package/dist/gallery/frame/chunks/markdown-p740v4qm.js +36 -0
- package/dist/gallery/frame/chunks/markdown-pxfeh231.js +57 -0
- package/dist/gallery/frame/chunks/markdown-pzgd8z3s.js +87 -0
- package/dist/gallery/frame/chunks/markdown-qx3cngz2.js +1673 -0
- package/dist/gallery/frame/chunks/markdown-rawhfvvc.js +85 -0
- package/dist/gallery/frame/chunks/markdown-rd8w0ng9.js +49 -0
- package/dist/gallery/frame/chunks/markdown-rm2yxfjj.js +2773 -0
- package/dist/gallery/frame/chunks/markdown-s1jatrnk.js +993 -0
- package/dist/gallery/frame/chunks/markdown-sgfg5vvq.js +59 -0
- package/dist/gallery/frame/chunks/markdown-t4ethv9d.js +409 -0
- package/dist/gallery/frame/chunks/markdown-tnmabqvz.js +36 -0
- package/dist/gallery/frame/chunks/markdown-tvdpppw8.js +5292 -0
- package/dist/gallery/frame/chunks/markdown-v0x27twr.js +22 -0
- package/dist/gallery/frame/chunks/markdown-v14td68x.js +36 -0
- package/dist/gallery/frame/chunks/markdown-w6sgd54z.js +36 -0
- package/dist/gallery/frame/chunks/markdown-xw8dsk8b.js +2125 -0
- package/dist/gallery/frame/chunks/markdown-y18mve73.js +85 -0
- package/dist/gallery/frame/chunks/markdown-yengsms5.js +427 -0
- package/dist/gallery/frame/chunks/markdown-z1wc0v0d.js +1826 -0
- package/dist/gallery/frame/chunks/markdown-zazx9h4n.js +329 -0
- package/dist/gallery/frame/chunks/mermaid-de7p4v0x.js +30 -0
- package/dist/gallery/frame/chunks/mindmap-definition-FAOFIHXS-ra44cbem.js +1216 -0
- package/dist/gallery/frame/chunks/packet-7NZHBO7P-xppm5913.js +10 -0
- package/dist/gallery/frame/chunks/pegDiagram-VL7TDLO6-a2a4wg03.js +163 -0
- package/dist/gallery/frame/chunks/pie-RZYD4A2V-hnbxk152.js +10 -0
- package/dist/gallery/frame/chunks/pieDiagram-7S7Q4E2Y-f1vxjtmx.js +311 -0
- package/dist/gallery/frame/chunks/quadrantDiagram-CIZ2JOQS-ywafa6kh.js +1390 -0
- package/dist/gallery/frame/chunks/radar-I7S5WNFK-ewqd27se.js +10 -0
- package/dist/gallery/frame/chunks/railroad-3IZDKUUU-5hy4bdx3.js +10 -0
- package/dist/gallery/frame/chunks/railroad-abnf-AHOZXSZD-bh5sxz0d.js +10 -0
- package/dist/gallery/frame/chunks/railroad-ebnf-EBAXGLYW-6b9c3zeh.js +10 -0
- package/dist/gallery/frame/chunks/railroad-peg-LSFZ7HO6-avc8v39s.js +10 -0
- package/dist/gallery/frame/chunks/railroadDiagram-AXF67PYL-qc61wax4.js +131 -0
- package/dist/gallery/frame/chunks/requirementDiagram-LRYGKXZP-9bs4786c.js +1295 -0
- package/dist/gallery/frame/chunks/sankeyDiagram-W5VNT64P-awt1em8b.js +1339 -0
- package/dist/gallery/frame/chunks/sequenceDiagram-SI44F4Z6-qaf9mz3t.js +4324 -0
- package/dist/gallery/frame/chunks/sizeCapture-X5ZJPWSS-9fq685xd.js +64 -0
- package/dist/gallery/frame/chunks/stateDiagram-OKZ733FA-vx7a7hh0.js +456 -0
- package/dist/gallery/frame/chunks/stateDiagram-v2-UEYNNEHI-x4m6ws5t.js +46 -0
- package/dist/gallery/frame/chunks/swimlanes-SLNWSIFB-e4mxm1gh.js +8287 -0
- package/dist/gallery/frame/chunks/swimlanesDiagram-ULZ7WXOC-0h93e2qq.js +42 -0
- package/dist/gallery/frame/chunks/timeline-definition-Z64GVDOM-wp0bhbzg.js +1549 -0
- package/dist/gallery/frame/chunks/treeView-QDETBFTQ-hcx7n0vt.js +10 -0
- package/dist/gallery/frame/chunks/treemap-6X3UGDF4-av02skdw.js +10 -0
- package/dist/gallery/frame/chunks/vennDiagram-T6HMQDX7-pmvk1p7n.js +2566 -0
- package/dist/gallery/frame/chunks/wardley-OPB4EBWU-njsp6e40.js +10 -0
- package/dist/gallery/frame/chunks/wardleyDiagram-T6FBY63Y-eggmnv4z.js +979 -0
- package/dist/gallery/frame/chunks/xychartDiagram-ELKLHX3M-q40tnpe5.js +1954 -0
- package/dist/gallery/frame/frame.css +183 -0
- package/dist/gallery/frame/runtime/chart.js +42744 -0
- package/dist/gallery/frame/runtime/html.js +14 -0
- package/dist/gallery/frame/runtime/markdown.js +1403 -0
- package/dist/gallery/frame/runtime/mermaid.js +32 -0
- package/dist/gallery/frame/runtime/svg.js +14 -0
- package/dist/gallery/frame/runtime/tsx.js +52 -0
- package/dist/gallery/index.html +104 -0
- package/docs/reference/cli.md +163 -0
- package/docs/reference/export.md +106 -0
- package/docs/reference/html.md +215 -0
- package/docs/reference/http.md +85 -0
- package/docs/reference/mcp.md +78 -0
- package/docs/reference/security.md +119 -0
- package/docs/reference/storage.md +93 -0
- package/docs/reference/tsx.md +101 -0
- package/docs/reference/validation.md +212 -0
- package/package.json +89 -0
- package/scripts/build-gallery.ts +127 -0
- package/scripts/launch-netns.sh +67 -0
- package/skills/facet/SKILL.md +100 -0
- package/src/cli/client.ts +311 -0
- package/src/cli/commands/create.ts +49 -0
- package/src/cli/commands/doctor.ts +249 -0
- package/src/cli/commands/export.ts +179 -0
- package/src/cli/commands/instantiate.ts +35 -0
- package/src/cli/commands/list.ts +49 -0
- package/src/cli/commands/open.ts +75 -0
- package/src/cli/commands/pin.ts +39 -0
- package/src/cli/commands/promote.ts +49 -0
- package/src/cli/commands/publish.ts +122 -0
- package/src/cli/commands/read-back.ts +56 -0
- package/src/cli/commands/status.ts +114 -0
- package/src/cli/commands/watch.ts +146 -0
- package/src/cli/main.ts +522 -0
- package/src/cli/output.ts +106 -0
- package/src/cli/parser.ts +376 -0
- package/src/cli/presenter.ts +212 -0
- package/src/cli/service-metadata.ts +134 -0
- package/src/cli/spawn-service.ts +326 -0
- package/src/css.d.ts +4 -0
- package/src/gallery-web/app.ts +1581 -0
- package/src/gallery-web/export-menu.ts +121 -0
- package/src/gallery-web/export.ts +112 -0
- package/src/gallery-web/favicon.ts +54 -0
- package/src/gallery-web/frame/entries/chart.ts +6 -0
- package/src/gallery-web/frame/entries/html.ts +6 -0
- package/src/gallery-web/frame/entries/markdown.ts +6 -0
- package/src/gallery-web/frame/entries/mermaid.ts +6 -0
- package/src/gallery-web/frame/entries/svg.ts +6 -0
- package/src/gallery-web/frame/entries/tsx.ts +6 -0
- package/src/gallery-web/frame/frame-payload.ts +91 -0
- package/src/gallery-web/frame/renderer-validation.ts +13 -0
- package/src/gallery-web/frame/renderers/chart.ts +182 -0
- package/src/gallery-web/frame/renderers/dompurify-shim.ts +112 -0
- package/src/gallery-web/frame/renderers/html.ts +48 -0
- package/src/gallery-web/frame/renderers/markdown.ts +81 -0
- package/src/gallery-web/frame/renderers/mermaid.ts +120 -0
- package/src/gallery-web/frame/renderers/registry.ts +260 -0
- package/src/gallery-web/frame/renderers/svg.ts +393 -0
- package/src/gallery-web/frame/renderers/tsx.ts +61 -0
- package/src/gallery-web/frame/runtime.ts +501 -0
- package/src/gallery-web/frame/styles/artifact.css +8424 -0
- package/src/gallery-web/frame/styles/frame.css +183 -0
- package/src/gallery-web/frame/styles/html-source.css +23 -0
- package/src/gallery-web/frame/view-box.ts +33 -0
- package/src/gallery-web/frame-html.ts +72 -0
- package/src/gallery-web/index.html +104 -0
- package/src/gallery-web/session.ts +107 -0
- package/src/gallery-web/sse-client.ts +212 -0
- package/src/gallery-web/styles/app.css +420 -0
- package/src/gallery-web/styles/tokens.css +112 -0
- package/src/gallery-web/styles/verdict.css +186 -0
- package/src/gallery-web/swap.ts +64 -0
- package/src/gallery-web/theme.ts +40 -0
- package/src/gallery-web/view-state.ts +93 -0
- package/src/harness-adapters/claude-code/README.md +10 -0
- package/src/harness-adapters/claude-code/facet.sh +4 -0
- package/src/harness-adapters/codex/README.md +10 -0
- package/src/harness-adapters/codex/facet.sh +4 -0
- package/src/harness-adapters/mcp/README.md +13 -0
- package/src/harness-adapters/mcp/cli-bridge.ts +216 -0
- package/src/harness-adapters/mcp/main.ts +15 -0
- package/src/harness-adapters/mcp/server.ts +164 -0
- package/src/harness-adapters/mcp/tool-schemas.ts +53 -0
- package/src/harness-adapters/opencode/README.md +10 -0
- package/src/harness-adapters/opencode/facet.sh +4 -0
- package/src/runtime/compiled-entrypoints.ts +69 -0
- package/src/service/dispatcher.ts +717 -0
- package/src/service/export.ts +218 -0
- package/src/service/http-utils.ts +55 -0
- package/src/service/lexical/expectations.ts +210 -0
- package/src/service/lifecycle/idle-controller.ts +125 -0
- package/src/service/lifecycle/orphan-cleanup.ts +128 -0
- package/src/service/lifecycle/process-lock.ts +235 -0
- package/src/service/main.ts +136 -0
- package/src/service/process.ts +240 -0
- package/src/service/router-guards.ts +239 -0
- package/src/service/router.ts +627 -0
- package/src/service/security/auth.ts +168 -0
- package/src/service/security/host-origin.ts +138 -0
- package/src/service/security/http-guards.ts +13 -0
- package/src/service/security/leases.ts +182 -0
- package/src/service/security/token-store.ts +138 -0
- package/src/service/server.ts +356 -0
- package/src/service/store/database.ts +61 -0
- package/src/service/store/evidence-retention.ts +234 -0
- package/src/service/store/migrations.ts +167 -0
- package/src/service/store/repository-lifecycle.ts +121 -0
- package/src/service/store/repository.ts +732 -0
- package/src/service/store/schema.ts +218 -0
- package/src/service/stored-verdict.ts +97 -0
- package/src/service/stream.ts +301 -0
- package/src/service/verdict-enrichment.ts +45 -0
- package/src/shared/build/frame-bundle-plugins.ts +61 -0
- package/src/shared/build-mode.ts +28 -0
- package/src/shared/config/evidence-read.ts +98 -0
- package/src/shared/config/limits.ts +56 -0
- package/src/shared/config/paths.ts +96 -0
- package/src/shared/contracts/artifact-types.ts +2 -0
- package/src/shared/contracts/artifact.ts +98 -0
- package/src/shared/contracts/commands/_shared.ts +59 -0
- package/src/shared/contracts/commands/guards.ts +86 -0
- package/src/shared/contracts/commands/index.ts +204 -0
- package/src/shared/contracts/commands/names.ts +35 -0
- package/src/shared/contracts/commands/requests.ts +149 -0
- package/src/shared/contracts/commands/results.ts +200 -0
- package/src/shared/contracts/envelope.ts +140 -0
- package/src/shared/contracts/events.ts +41 -0
- package/src/shared/contracts/observed-counts.ts +19 -0
- package/src/shared/contracts/renderers.ts +6 -0
- package/src/shared/contracts/validation.ts +296 -0
- package/src/shared/errors/facet-error.ts +127 -0
- package/src/shared/errors/store-error.ts +68 -0
- package/src/shared/evidence-image.ts +37 -0
- package/src/shared/export.ts +31 -0
- package/src/shared/html/policy.ts +108 -0
- package/src/shared/html/style-vocabulary.ts +352 -0
- package/src/shared/logging/logger.ts +108 -0
- package/src/shared/logging/redact.ts +49 -0
- package/src/shared/security/frozen-csp.ts +13 -0
- package/src/shared/storage-version.ts +2 -0
- package/src/shared/tsx/execution.ts +29 -0
- package/src/shared/tsx/import-policy.ts +94 -0
- package/src/shared/util/dir-permissions.ts +94 -0
- package/src/shared/util/mermaid-nodes.ts +230 -0
- package/src/shared/util/process.ts +67 -0
- package/src/shared/util/time.ts +21 -0
- package/src/shared/version.ts +3 -0
- package/src/validation/sandbox/limits.ts +41 -0
- package/src/validation/sandbox/netns.ts +177 -0
- package/src/validation/tier0/chart.ts +177 -0
- package/src/validation/tier0/dom-shim.ts +54 -0
- package/src/validation/tier0/html.ts +408 -0
- package/src/validation/tier0/markdown.ts +285 -0
- package/src/validation/tier0/mermaid.ts +98 -0
- package/src/validation/tier0/runner.ts +504 -0
- package/src/validation/tier0/svg.ts +321 -0
- package/src/validation/tier0/tsx/allowlist-resolver.ts +77 -0
- package/src/validation/tier0/tsx/ast-policy-capabilities.ts +238 -0
- package/src/validation/tier0/tsx/ast-policy-shared.ts +71 -0
- package/src/validation/tier0/tsx/ast-policy.ts +181 -0
- package/src/validation/tier0/tsx/compiler.ts +156 -0
- package/src/validation/tier0/worker-dispatch.ts +211 -0
- package/src/validation/tier0/worker-entry.ts +96 -0
- package/src/validation/tier0/worker-input.ts +75 -0
- package/src/validation/tier1/browser-process.ts +147 -0
- package/src/validation/tier1/cdp-pipe.ts +281 -0
- package/src/validation/tier1/entries/chart.ts +5 -0
- package/src/validation/tier1/entries/html.ts +6 -0
- package/src/validation/tier1/entries/markdown.ts +5 -0
- package/src/validation/tier1/entries/mermaid.ts +5 -0
- package/src/validation/tier1/entries/svg.ts +5 -0
- package/src/validation/tier1/entries/tsx.ts +5 -0
- package/src/validation/tier1/frame-target.ts +162 -0
- package/src/validation/tier1/harness-entry.ts +261 -0
- package/src/validation/tier1/harness.ts +215 -0
- package/src/validation/tier1/isolated-probe.ts +94 -0
- package/src/validation/tier1/launcher.ts +177 -0
- package/src/validation/tier1/limits.ts +80 -0
- package/src/validation/tier1/nonce.ts +17 -0
- package/src/validation/tier1/protocol-probe.ts +519 -0
- package/src/validation/tier1/runner.ts +1122 -0
- package/src/validation/tier1/verdict.ts +246 -0
- package/src/validation/tier1/webp.ts +69 -0
- package/templates/README.md +88 -0
- package/templates/bar-compare.vl.json +63 -0
- package/templates/capacity-report.tsx +102 -0
- package/templates/decision-record.md +24 -0
- package/templates/deployment-state.mmd +40 -0
- package/templates/exemplar.md +91 -0
- package/templates/facet-story.tsx +799 -0
- package/templates/fleet-dashboard.html +141 -0
- package/templates/html-release-ledger.html +91 -0
- package/templates/html-status-report.html +114 -0
- package/templates/incident-console.tsx +147 -0
- package/templates/legion-boundaries.mmd +23 -0
- package/templates/legion-flow.mmd +15 -0
- package/templates/legion-sequence.mmd +14 -0
- package/templates/legion-state.mmd +7 -0
- package/templates/metric-card.svg +29 -0
- package/templates/observability-map.svg +101 -0
- package/templates/pipeline-audit.md +108 -0
- package/templates/release-metrics.vl.json +98 -0
- package/templates/service-topology.mmd +47 -0
- package/templates/status-report.md +32 -0
- package/templates/system-map.svg +53 -0
- package/templates/timeseries.vl.json +71 -0
- package/templates/tsx-interactive-counter.tsx +19 -0
- package/templates/tsx-status-report.tsx +31 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 0 mermaid parser.
|
|
3
|
+
*
|
|
4
|
+
* `mermaid.parse()` is the canonical mermaid grammar check: it runs
|
|
5
|
+
* the JISON parser over the input and returns `{ diagramType, config }`
|
|
6
|
+
* for valid sources or THROWS a parse error for invalid ones. No DOM,
|
|
7
|
+
* no rendering — but the library itself depends on a browser-shaped
|
|
8
|
+
* DOM (DOMPurify, etc.) at IMPORT TIME.
|
|
9
|
+
*
|
|
10
|
+
* `dom-shim.ts` installs a linkedom-based structural DOM stub BEFORE
|
|
11
|
+
* this module imports mermaid, so the library's import-time
|
|
12
|
+
* DOMPurify check succeeds. The shim is structural: it never executes
|
|
13
|
+
* artifact source against the DOM. netns ensures any library-initiated
|
|
14
|
+
* network egress cannot reach a host.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import "./dom-shim";
|
|
18
|
+
import mermaid from "mermaid";
|
|
19
|
+
|
|
20
|
+
import type { DiscriminativeError, VerdictObserved } from "../../shared/contracts/validation";
|
|
21
|
+
import { countMermaidNodeDeclarations } from "../../shared/util/mermaid-nodes";
|
|
22
|
+
|
|
23
|
+
export interface MermaidParseOk {
|
|
24
|
+
readonly status: "ok";
|
|
25
|
+
readonly observed: VerdictObserved;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MermaidParseFail {
|
|
29
|
+
readonly status: "error";
|
|
30
|
+
readonly observed: VerdictObserved;
|
|
31
|
+
readonly errors: readonly DiscriminativeError[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type MermaidParseResult = MermaidParseOk | MermaidParseFail;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Count node declarations across the source. This is the lexical
|
|
38
|
+
* counter the parser agrees with when the parse succeeds and disagrees
|
|
39
|
+
* with when a hostile artifact lies in its mermaid nodes; the canonical
|
|
40
|
+
* prediction lives in `shared/util/mermaid-nodes.ts` (the service-side
|
|
41
|
+
* expectations import the same one).
|
|
42
|
+
*/
|
|
43
|
+
function countMermaidNodes(bytes: Uint8Array): number {
|
|
44
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
45
|
+
return countMermaidNodeDeclarations(text) ?? 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface MermaidResolved {
|
|
49
|
+
readonly diagramType: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parse the source bytes as a mermaid diagram. On success, returns
|
|
54
|
+
* the lexical mermaid-node count. On failure, returns the parser's
|
|
55
|
+
* error text wrapped as a discriminative error.
|
|
56
|
+
*/
|
|
57
|
+
export async function parseMermaid(bytes: Uint8Array): Promise<MermaidParseResult> {
|
|
58
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
59
|
+
const lexicalNodes = countMermaidNodes(bytes);
|
|
60
|
+
try {
|
|
61
|
+
const resolved = (await (mermaid as { parse: (s: string) => Promise<MermaidResolved> }).parse(
|
|
62
|
+
text,
|
|
63
|
+
)) as MermaidResolved;
|
|
64
|
+
void resolved;
|
|
65
|
+
return {
|
|
66
|
+
status: "ok",
|
|
67
|
+
observed: {
|
|
68
|
+
rendererRootSvgCount: 1,
|
|
69
|
+
graphCount: 1,
|
|
70
|
+
mermaidNodeCount: lexicalNodes,
|
|
71
|
+
visibleSvgCount: 1,
|
|
72
|
+
errorCount: 0,
|
|
73
|
+
opaqueRegionCount: 0,
|
|
74
|
+
externalImageCount: 0,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
} catch (error) {
|
|
78
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
79
|
+
return {
|
|
80
|
+
status: "error",
|
|
81
|
+
observed: {
|
|
82
|
+
rendererRootSvgCount: 0,
|
|
83
|
+
graphCount: 0,
|
|
84
|
+
mermaidNodeCount: 0,
|
|
85
|
+
visibleSvgCount: 0,
|
|
86
|
+
errorCount: 1,
|
|
87
|
+
opaqueRegionCount: 0,
|
|
88
|
+
externalImageCount: 0,
|
|
89
|
+
},
|
|
90
|
+
errors: [
|
|
91
|
+
{
|
|
92
|
+
code: "mermaid_parse_error",
|
|
93
|
+
message,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 0 runner — the PARENT side of the Tier 0 worker protocol.
|
|
3
|
+
*
|
|
4
|
+
* Spawns the worker subprocess inside a rootless network namespace
|
|
5
|
+
* (via `sandbox/netns.ts`), writes the input envelope to the worker's
|
|
6
|
+
* STDIN, reads STDOUT with a hard byte cap, enforces a wall-clock
|
|
7
|
+
* timeout, kills the worker on overrun, and parses the STDOUT payload
|
|
8
|
+
* via a STRICT zod schema.
|
|
9
|
+
*
|
|
10
|
+
* Worker stdout is treated as UNTRUSTED protocol input. Anything
|
|
11
|
+
* beyond a single well-formed Tier0Result is mapped to a typed
|
|
12
|
+
* `FacetError`:
|
|
13
|
+
*
|
|
14
|
+
* - `tier0_unavailable` — unshare / userns not available.
|
|
15
|
+
* - `tier0_timeout` — wall-clock budget exceeded.
|
|
16
|
+
* - `tier0_output_cap` — stdout exceeded the byte cap.
|
|
17
|
+
* - `tier0_worker_died` — worker exited via signal.
|
|
18
|
+
* - `tier0_protocol_error` — non-JSON, schema mismatch, or extra
|
|
19
|
+
* trailing bytes.
|
|
20
|
+
*
|
|
21
|
+
* Parser-level outcomes (status="error") are returned as Tier0WorkerResult
|
|
22
|
+
* with discriminativeErrors populated; the caller persists them as a
|
|
23
|
+
* render_run bound to (artifactId, revisionSha).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { ChildProcess } from "node:child_process";
|
|
27
|
+
import { resolve as resolvePath } from "node:path";
|
|
28
|
+
|
|
29
|
+
import type { ArtifactType } from "../../shared/contracts/artifact";
|
|
30
|
+
import {
|
|
31
|
+
buildTier0WorkerArgs,
|
|
32
|
+
isCompiledRuntime,
|
|
33
|
+
type Tier0WorkerCommand,
|
|
34
|
+
} from "../../shared/build-mode";
|
|
35
|
+
import { FacetError } from "../../shared/errors/facet-error";
|
|
36
|
+
import {
|
|
37
|
+
LexicalCountersSchema,
|
|
38
|
+
Tier0WorkerResultSchema,
|
|
39
|
+
type LexicalCounters,
|
|
40
|
+
type InsecureLevel,
|
|
41
|
+
type Tier0Input,
|
|
42
|
+
type Tier0Runner,
|
|
43
|
+
type Tier0WorkerResult,
|
|
44
|
+
} from "../../shared/contracts/validation";
|
|
45
|
+
|
|
46
|
+
import {
|
|
47
|
+
probeNetnsSupport,
|
|
48
|
+
resolveTier0Isolation,
|
|
49
|
+
spawnDirectWorker,
|
|
50
|
+
spawnNetnsWorker,
|
|
51
|
+
} from "../sandbox/netns";
|
|
52
|
+
|
|
53
|
+
export { resolveTier0Isolation } from "../sandbox/netns";
|
|
54
|
+
|
|
55
|
+
/** Probe the host capability used by Tier 0 without bypassing its cache. */
|
|
56
|
+
export function probeTier0Isolation() {
|
|
57
|
+
return probeNetnsSupport();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
import { TIER0_OUTPUT_CAP_BYTES, TIER0_TIMEOUT_MS, TIER0_TSX_TIMEOUT_MS } from "../sandbox/limits";
|
|
61
|
+
|
|
62
|
+
const TIER0_WORKER_ENTRY = resolvePath(import.meta.dir, "worker-entry.ts");
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The wire envelope the parent writes to the worker's STDIN. The
|
|
66
|
+
* source bytes are base64-encoded so JSON can carry them; the worker
|
|
67
|
+
* decodes them back into a Uint8Array before invoking the parser.
|
|
68
|
+
*/
|
|
69
|
+
interface WorkerInputEnvelope {
|
|
70
|
+
readonly schemaVersion: "facet.tier0.v2";
|
|
71
|
+
readonly requestId: string;
|
|
72
|
+
readonly revisionSha: string;
|
|
73
|
+
readonly artifactType: ArtifactType;
|
|
74
|
+
readonly renderer: Tier0Input["renderer"];
|
|
75
|
+
readonly sourceBase64: string;
|
|
76
|
+
readonly lexical: LexicalCounters;
|
|
77
|
+
readonly execution?: Tier0Input["execution"];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface Tier0RunnerTestHooks {
|
|
81
|
+
readonly workerEntry?: string;
|
|
82
|
+
readonly timeoutMs?: number;
|
|
83
|
+
readonly tsxTimeoutMs?: number;
|
|
84
|
+
readonly outputCap?: number;
|
|
85
|
+
readonly onWorkerSpawn?: (pid: number) => void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build the STDIN envelope from a `Tier0Input`. The lexical counters
|
|
90
|
+
* are validated against the closed schema here so a misbehaving
|
|
91
|
+
* caller cannot smuggle extra fields into the worker.
|
|
92
|
+
*/
|
|
93
|
+
function buildInputEnvelope(input: Tier0Input, requestId: string): WorkerInputEnvelope {
|
|
94
|
+
// The contract says input.source is Uint8Array; we accept anything
|
|
95
|
+
// `instanceof Uint8Array` and base64-encode it.
|
|
96
|
+
const sourceBytes =
|
|
97
|
+
input.source instanceof Uint8Array ? input.source : new Uint8Array(input.source);
|
|
98
|
+
return {
|
|
99
|
+
schemaVersion: "facet.tier0.v2",
|
|
100
|
+
requestId,
|
|
101
|
+
revisionSha: input.revisionSha,
|
|
102
|
+
artifactType: input.artifactType,
|
|
103
|
+
renderer: input.renderer,
|
|
104
|
+
sourceBase64: Buffer.from(sourceBytes).toString("base64"),
|
|
105
|
+
lexical: LexicalCountersSchema.parse(input.lexical),
|
|
106
|
+
...(input.execution === undefined ? {} : { execution: input.execution }),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Strip the worker's trailing newline (if any) and parse the JSON.
|
|
112
|
+
* A non-JSON payload, a schema mismatch, or extra trailing bytes
|
|
113
|
+
* produces a typed `tier0_protocol_error`.
|
|
114
|
+
*
|
|
115
|
+
* Exported (with a test-only prefix) so the strict-zod guard is
|
|
116
|
+
* directly testable; production code only calls it through
|
|
117
|
+
* `runTier0`.
|
|
118
|
+
*/
|
|
119
|
+
// oxlint-disable-next-line no-underscore-dangle
|
|
120
|
+
export function _parseWorkerStdout(stdout: string, outputCap: number): Tier0WorkerResult {
|
|
121
|
+
const trimmed = stdout.trim();
|
|
122
|
+
if (trimmed.length === 0) {
|
|
123
|
+
throw new FacetError("tier0_protocol_error", "Worker emitted empty stdout", {
|
|
124
|
+
retryable: false,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
let parsedJson: unknown;
|
|
128
|
+
try {
|
|
129
|
+
parsedJson = JSON.parse(trimmed);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
132
|
+
throw new FacetError("tier0_protocol_error", `Worker stdout is not valid JSON: ${message}`, {
|
|
133
|
+
retryable: false,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
const result = Tier0WorkerResultSchema.safeParse(parsedJson);
|
|
137
|
+
if (!result.success) {
|
|
138
|
+
throw new FacetError(
|
|
139
|
+
"tier0_protocol_error",
|
|
140
|
+
`Worker stdout does not match Tier0WorkerResultSchema: ${result.error.issues
|
|
141
|
+
.slice(0, 3)
|
|
142
|
+
.map((issue) => issue.message)
|
|
143
|
+
.join("; ")}`,
|
|
144
|
+
{ retryable: false },
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
// Reject extra trailing bytes — the worker MUST emit exactly one
|
|
148
|
+
// JSON object. A trailing newline is allowed (we trimmed) but any
|
|
149
|
+
// additional content after the closing brace is a protocol violation.
|
|
150
|
+
// The output-cap check above bounds the total size so this is a
|
|
151
|
+
// final correctness gate.
|
|
152
|
+
void outputCap;
|
|
153
|
+
return result.data;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface PendingRequest {
|
|
157
|
+
readonly requestId: string;
|
|
158
|
+
readonly worker: ChildProcess;
|
|
159
|
+
readonly resolve: (result: Tier0WorkerResult) => void;
|
|
160
|
+
readonly reject: (error: FacetError) => void;
|
|
161
|
+
readonly timer: ReturnType<typeof setTimeout>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface RunnerOptions {
|
|
165
|
+
readonly workerCommand: Tier0WorkerCommand;
|
|
166
|
+
readonly timeoutMs: number;
|
|
167
|
+
readonly tsxTimeoutMs: number;
|
|
168
|
+
readonly outputCap: number;
|
|
169
|
+
readonly onWorkerSpawn?: (pid: number) => void;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function workerDiedError(
|
|
173
|
+
code: number | null,
|
|
174
|
+
signal: NodeJS.Signals | null,
|
|
175
|
+
stderr: Buffer,
|
|
176
|
+
): FacetError {
|
|
177
|
+
const stderrText = stderr.toString("utf8").trim() || null;
|
|
178
|
+
if (signal !== null) {
|
|
179
|
+
return new FacetError("tier0_worker_died", `Worker terminated by signal ${signal}`, {
|
|
180
|
+
retryable: false,
|
|
181
|
+
details: { signal, stderr: stderrText },
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (code === null) {
|
|
185
|
+
return new FacetError("tier0_worker_died", "Worker exited without code or signal", {
|
|
186
|
+
retryable: false,
|
|
187
|
+
details: { stderr: stderrText },
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return new FacetError("tier0_worker_died", `Worker exited with non-zero code ${code}`, {
|
|
191
|
+
retryable: false,
|
|
192
|
+
details: { exitCode: code, stderr: stderrText },
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function workerStartError(error: unknown, stderr: Buffer): FacetError {
|
|
197
|
+
const fields =
|
|
198
|
+
error !== null && typeof error === "object" ? (error as Record<string, unknown>) : {};
|
|
199
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
200
|
+
const code = typeof fields.code === "string" ? fields.code : null;
|
|
201
|
+
const errno =
|
|
202
|
+
typeof fields.errno === "string" || typeof fields.errno === "number" ? fields.errno : null;
|
|
203
|
+
const syscall = typeof fields.syscall === "string" ? fields.syscall : null;
|
|
204
|
+
const stderrText = stderr.toString("utf8").trim() || null;
|
|
205
|
+
const diagnostics = [
|
|
206
|
+
`error=${message}`,
|
|
207
|
+
...(code === null ? [] : [`code=${code}`]),
|
|
208
|
+
...(errno === null ? [] : [`errno=${errno}`]),
|
|
209
|
+
...(syscall === null ? [] : [`syscall=${syscall}`]),
|
|
210
|
+
...(stderrText === null ? [] : [`stderr=${stderrText}`]),
|
|
211
|
+
];
|
|
212
|
+
return new FacetError(
|
|
213
|
+
"tier0_worker_died",
|
|
214
|
+
`Worker process failed to start: ${diagnostics.join("; ")}`,
|
|
215
|
+
{
|
|
216
|
+
retryable: false,
|
|
217
|
+
details: {
|
|
218
|
+
code,
|
|
219
|
+
errno,
|
|
220
|
+
syscall,
|
|
221
|
+
stderr: stderrText,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function parseWorkerResponse(
|
|
228
|
+
line: Buffer,
|
|
229
|
+
requestId: string,
|
|
230
|
+
outputCap: number,
|
|
231
|
+
): Tier0WorkerResult {
|
|
232
|
+
let rawEnvelope: unknown;
|
|
233
|
+
try {
|
|
234
|
+
rawEnvelope = JSON.parse(line.toString("utf8"));
|
|
235
|
+
} catch (error) {
|
|
236
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
237
|
+
throw new FacetError("tier0_protocol_error", `Worker stdout is not valid JSON: ${message}`, {
|
|
238
|
+
retryable: false,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
if (typeof rawEnvelope !== "object" || rawEnvelope === null || Array.isArray(rawEnvelope)) {
|
|
242
|
+
throw new FacetError("tier0_protocol_error", "Worker response is not an object envelope", {
|
|
243
|
+
retryable: false,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
const envelope = rawEnvelope as { requestId?: unknown; result?: unknown };
|
|
247
|
+
if (envelope.requestId !== requestId) {
|
|
248
|
+
throw new FacetError("tier0_protocol_error", "Worker response requestId did not match", {
|
|
249
|
+
retryable: false,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
if (envelope.result === undefined) {
|
|
253
|
+
throw new FacetError("tier0_protocol_error", "Worker response omitted result", {
|
|
254
|
+
retryable: false,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return _parseWorkerStdout(JSON.stringify(envelope.result), outputCap);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function createRunner(level: InsecureLevel, options: RunnerOptions): Tier0Runner {
|
|
261
|
+
let child: ChildProcess | null = null;
|
|
262
|
+
let pending: PendingRequest | null = null;
|
|
263
|
+
let stdoutBuffer = Buffer.alloc(0);
|
|
264
|
+
let stderrBuffer = Buffer.alloc(0);
|
|
265
|
+
let queued: Promise<void> = Promise.resolve();
|
|
266
|
+
let requestSequence = 0;
|
|
267
|
+
let closed = false;
|
|
268
|
+
|
|
269
|
+
function rejectPending(target: PendingRequest, error: FacetError): void {
|
|
270
|
+
if (pending !== target) return;
|
|
271
|
+
clearTimeout(target.timer);
|
|
272
|
+
pending = null;
|
|
273
|
+
target.reject(error);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function resolvePending(target: PendingRequest, result: Tier0WorkerResult): void {
|
|
277
|
+
if (pending !== target) return;
|
|
278
|
+
clearTimeout(target.timer);
|
|
279
|
+
pending = null;
|
|
280
|
+
target.resolve(result);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function clearWorker(target: ChildProcess, kill: boolean): void {
|
|
284
|
+
if (child !== target) return;
|
|
285
|
+
child = null;
|
|
286
|
+
stdoutBuffer = Buffer.alloc(0);
|
|
287
|
+
stderrBuffer = Buffer.alloc(0);
|
|
288
|
+
if (!kill) return;
|
|
289
|
+
try {
|
|
290
|
+
target.kill("SIGKILL");
|
|
291
|
+
} catch {
|
|
292
|
+
// The worker may have exited between protocol failure and teardown.
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function failWorker(target: ChildProcess, error: FacetError, kill: boolean): void {
|
|
297
|
+
if (pending?.worker === target) rejectPending(pending, error);
|
|
298
|
+
clearWorker(target, kill);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function handleStdout(target: ChildProcess, chunk: Buffer): void {
|
|
302
|
+
if (child !== target || pending === null || pending.worker !== target) {
|
|
303
|
+
failWorker(
|
|
304
|
+
target,
|
|
305
|
+
new FacetError("tier0_protocol_error", "Worker emitted stdout without an active request", {
|
|
306
|
+
retryable: false,
|
|
307
|
+
}),
|
|
308
|
+
true,
|
|
309
|
+
);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
stdoutBuffer = Buffer.concat([stdoutBuffer, chunk]);
|
|
313
|
+
const newline = stdoutBuffer.indexOf(0x0a);
|
|
314
|
+
if (newline < 0) {
|
|
315
|
+
if (stdoutBuffer.byteLength > options.outputCap) {
|
|
316
|
+
failWorker(
|
|
317
|
+
target,
|
|
318
|
+
new FacetError("tier0_output_cap", "Worker stdout exceeded the byte cap", {
|
|
319
|
+
retryable: false,
|
|
320
|
+
details: { capBytes: options.outputCap },
|
|
321
|
+
}),
|
|
322
|
+
true,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const line = stdoutBuffer.subarray(0, newline);
|
|
328
|
+
const trailing = stdoutBuffer.subarray(newline + 1);
|
|
329
|
+
stdoutBuffer = trailing;
|
|
330
|
+
if (trailing.byteLength > 0 && trailing[0] !== 0x7b) {
|
|
331
|
+
failWorker(
|
|
332
|
+
target,
|
|
333
|
+
new FacetError("tier0_protocol_error", "Worker emitted invalid trailing stdout bytes", {
|
|
334
|
+
retryable: false,
|
|
335
|
+
}),
|
|
336
|
+
true,
|
|
337
|
+
);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (line.byteLength > options.outputCap) {
|
|
341
|
+
failWorker(
|
|
342
|
+
target,
|
|
343
|
+
new FacetError("tier0_output_cap", "Worker stdout exceeded the byte cap", {
|
|
344
|
+
retryable: false,
|
|
345
|
+
details: { capBytes: options.outputCap },
|
|
346
|
+
}),
|
|
347
|
+
true,
|
|
348
|
+
);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const active = pending;
|
|
352
|
+
try {
|
|
353
|
+
resolvePending(active, parseWorkerResponse(line, active.requestId, options.outputCap));
|
|
354
|
+
} catch (error) {
|
|
355
|
+
const facet = FacetError.from(error);
|
|
356
|
+
failWorker(target, facet, true);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function spawnWorker(): ChildProcess {
|
|
361
|
+
const isolation = resolveTier0Isolation(level);
|
|
362
|
+
if (isolation === "netns") {
|
|
363
|
+
const probe = probeNetnsSupport();
|
|
364
|
+
if (!probe.available) {
|
|
365
|
+
throw new FacetError(
|
|
366
|
+
"tier0_unavailable",
|
|
367
|
+
`Tier 0 cannot run: netns unavailable (${probe.reason ?? "unknown"})`,
|
|
368
|
+
{ retryable: false, details: { reason: probe.reason ?? "unknown" } },
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const started =
|
|
373
|
+
isolation === "netns"
|
|
374
|
+
? spawnNetnsWorker(buildTier0WorkerArgs(options.workerCommand))
|
|
375
|
+
: spawnDirectWorker(buildTier0WorkerArgs(options.workerCommand));
|
|
376
|
+
child = started;
|
|
377
|
+
options.onWorkerSpawn?.(started.pid!);
|
|
378
|
+
started.stderr?.on("data", (chunk: Buffer) => {
|
|
379
|
+
stderrBuffer = Buffer.concat([stderrBuffer, Buffer.from(chunk)]).subarray(
|
|
380
|
+
0,
|
|
381
|
+
options.outputCap,
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
started.stderr?.pipe(process.stderr, { end: false });
|
|
385
|
+
started.stdout?.on("data", (chunk: Buffer) => handleStdout(started, Buffer.from(chunk)));
|
|
386
|
+
started.once("error", (error) => {
|
|
387
|
+
failWorker(started, workerStartError(error, stderrBuffer), false);
|
|
388
|
+
});
|
|
389
|
+
started.once("exit", (code, signal) => {
|
|
390
|
+
failWorker(started, workerDiedError(code, signal, stderrBuffer), false);
|
|
391
|
+
});
|
|
392
|
+
return started;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function runRequest(input: Tier0Input): Promise<Tier0WorkerResult> {
|
|
396
|
+
if (closed) {
|
|
397
|
+
throw new FacetError("tier0_worker_died", "Tier 0 runner is closed", { retryable: false });
|
|
398
|
+
}
|
|
399
|
+
const worker = child ?? spawnWorker();
|
|
400
|
+
if (child !== worker)
|
|
401
|
+
throw new FacetError("tier0_worker_died", "Worker exited before request", {
|
|
402
|
+
retryable: false,
|
|
403
|
+
});
|
|
404
|
+
const requestId = String(++requestSequence);
|
|
405
|
+
const envelopeJson = `${JSON.stringify(buildInputEnvelope(input, requestId))}\n`;
|
|
406
|
+
return new Promise<Tier0WorkerResult>((resolve, reject) => {
|
|
407
|
+
const timeoutMs = input.artifactType === "tsx" ? options.tsxTimeoutMs : options.timeoutMs;
|
|
408
|
+
const target: PendingRequest = {
|
|
409
|
+
requestId,
|
|
410
|
+
worker,
|
|
411
|
+
resolve,
|
|
412
|
+
reject,
|
|
413
|
+
timer: setTimeout(() => {
|
|
414
|
+
failWorker(
|
|
415
|
+
worker,
|
|
416
|
+
new FacetError("tier0_timeout", `Tier 0 worker timed out after ${timeoutMs}ms`, {
|
|
417
|
+
retryable: false,
|
|
418
|
+
details: { timeoutMs },
|
|
419
|
+
}),
|
|
420
|
+
true,
|
|
421
|
+
);
|
|
422
|
+
}, timeoutMs),
|
|
423
|
+
};
|
|
424
|
+
pending = target;
|
|
425
|
+
if (stdoutBuffer.byteLength > 0) handleStdout(worker, Buffer.alloc(0));
|
|
426
|
+
if (pending !== target) return;
|
|
427
|
+
try {
|
|
428
|
+
worker.stdin?.write(envelopeJson);
|
|
429
|
+
} catch {
|
|
430
|
+
failWorker(
|
|
431
|
+
worker,
|
|
432
|
+
new FacetError("tier0_worker_died", "Failed to write to Tier 0 worker", {
|
|
433
|
+
retryable: false,
|
|
434
|
+
}),
|
|
435
|
+
true,
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const run = (input: Tier0Input): Promise<Tier0WorkerResult> => {
|
|
442
|
+
const request = queued.then(() => runRequest(input));
|
|
443
|
+
queued = request.then(
|
|
444
|
+
() => undefined,
|
|
445
|
+
() => undefined,
|
|
446
|
+
);
|
|
447
|
+
return request;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const close = (): void => {
|
|
451
|
+
if (closed) return;
|
|
452
|
+
closed = true;
|
|
453
|
+
const active = child;
|
|
454
|
+
if (active === null) return;
|
|
455
|
+
failWorker(
|
|
456
|
+
active,
|
|
457
|
+
new FacetError("tier0_worker_died", "Tier 0 runner closed", { retryable: false }),
|
|
458
|
+
true,
|
|
459
|
+
);
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
return Object.assign(run, { close });
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Run the Tier 0 worker against an artifact's immutable bytes. The
|
|
467
|
+
* returned worker result ALWAYS conforms to `Tier0WorkerResultSchema`; the
|
|
468
|
+
* `status` field distinguishes a clean parse (`ok`) from a parser
|
|
469
|
+
* rejection (`error`). System-level failures throw `FacetError` with
|
|
470
|
+
* a typed `tier0_*` code.
|
|
471
|
+
*/
|
|
472
|
+
export async function runTier0(input: Tier0Input): Promise<Tier0WorkerResult> {
|
|
473
|
+
const runner = createTier0Runner(0);
|
|
474
|
+
try {
|
|
475
|
+
return await runner(input);
|
|
476
|
+
} finally {
|
|
477
|
+
runner.close?.();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Create a Tier 0 runner using the requested isolation level. */
|
|
482
|
+
export function createTier0Runner(level: InsecureLevel): Tier0Runner {
|
|
483
|
+
return createRunner(level, {
|
|
484
|
+
workerCommand: isCompiledRuntime()
|
|
485
|
+
? { mode: "compiled" }
|
|
486
|
+
: { mode: "source", entrypoint: TIER0_WORKER_ENTRY },
|
|
487
|
+
timeoutMs: TIER0_TIMEOUT_MS,
|
|
488
|
+
tsxTimeoutMs: TIER0_TSX_TIMEOUT_MS,
|
|
489
|
+
outputCap: TIER0_OUTPUT_CAP_BYTES,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export function createTier0RunnerForTests(
|
|
494
|
+
level: InsecureLevel,
|
|
495
|
+
hooks: Tier0RunnerTestHooks,
|
|
496
|
+
): Tier0Runner {
|
|
497
|
+
return createRunner(level, {
|
|
498
|
+
workerCommand: { mode: "source", entrypoint: hooks.workerEntry ?? TIER0_WORKER_ENTRY },
|
|
499
|
+
timeoutMs: hooks.timeoutMs ?? TIER0_TIMEOUT_MS,
|
|
500
|
+
tsxTimeoutMs: hooks.tsxTimeoutMs ?? TIER0_TSX_TIMEOUT_MS,
|
|
501
|
+
outputCap: hooks.outputCap ?? TIER0_OUTPUT_CAP_BYTES,
|
|
502
|
+
...(hooks.onWorkerSpawn === undefined ? {} : { onWorkerSpawn: hooks.onWorkerSpawn }),
|
|
503
|
+
});
|
|
504
|
+
}
|