@markii/host 0.0.0-stage → 0.17.0-next.2
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 +21 -0
- package/README.md +19 -2
- package/dist/browser-worker.js +639 -0
- package/dist/browser.d.ts +57 -0
- package/dist/browser.js +167 -0
- package/dist/conformance/host/01-plain-open/expected.json +5 -0
- package/dist/conformance/host/01-plain-open/note.mk.md +8 -0
- package/dist/conformance/host/01-plain-open/scenario.json +6 -0
- package/dist/conformance/host/02-run-manual-grant-accepted/expected.json +16 -0
- package/dist/conformance/host/02-run-manual-grant-accepted/note.mk.md +5 -0
- package/dist/conformance/host/02-run-manual-grant-accepted/scenario.json +14 -0
- package/dist/conformance/host/03-run-manual-grant-refused/expected.json +18 -0
- package/dist/conformance/host/03-run-manual-grant-refused/note.mk.md +5 -0
- package/dist/conformance/host/03-run-manual-grant-refused/scenario.json +14 -0
- package/dist/conformance/host/04-run-auto-write-denied/expected.json +16 -0
- package/dist/conformance/host/04-run-auto-write-denied/note.mk.md +10 -0
- package/dist/conformance/host/04-run-auto-write-denied/scenario.json +13 -0
- package/dist/conformance/host/05-export-html-pack-directive/expected.json +14 -0
- package/dist/conformance/host/05-export-html-pack-directive/note.mk.md +8 -0
- package/dist/conformance/host/05-export-html-pack-directive/scenario.json +6 -0
- package/dist/conformance/host/06-install-pack-then-render/expected.json +18 -0
- package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/pack.json +13 -0
- package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/06-install-pack-then-render/note.mk.md +5 -0
- package/dist/conformance/host/06-install-pack-then-render/scenario.json +15 -0
- package/dist/conformance/host/07-complete-in-directive/expected.json +14 -0
- package/dist/conformance/host/07-complete-in-directive/note.mk.md +3 -0
- package/dist/conformance/host/07-complete-in-directive/scenario.json +6 -0
- package/dist/conformance/host/08-hover-standard-component/expected.json +5 -0
- package/dist/conformance/host/08-hover-standard-component/note.mk.md +5 -0
- package/dist/conformance/host/08-hover-standard-component/scenario.json +6 -0
- package/dist/conformance/host/09-insert-skeleton/expected.json +5 -0
- package/dist/conformance/host/09-insert-skeleton/note.mk.md +4 -0
- package/dist/conformance/host/09-insert-skeleton/scenario.json +6 -0
- package/dist/conformance/host/10-scripts-disabled/expected.json +11 -0
- package/dist/conformance/host/10-scripts-disabled/note.mk.md +5 -0
- package/dist/conformance/host/10-scripts-disabled/scenario.json +9 -0
- package/dist/conformance/host/11-complete-closing-fence/expected.json +5 -0
- package/dist/conformance/host/11-complete-closing-fence/note.mk.md +5 -0
- package/dist/conformance/host/11-complete-closing-fence/scenario.json +12 -0
- package/dist/conformance/host/12-complete-nested-container/expected.json +5 -0
- package/dist/conformance/host/12-complete-nested-container/note.mk.md +5 -0
- package/dist/conformance/host/12-complete-nested-container/scenario.json +12 -0
- package/dist/conformance/host/13-install-pack-declined/expected.json +12 -0
- package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/pack.json +13 -0
- package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/13-install-pack-declined/note.mk.md +5 -0
- package/dist/conformance/host/13-install-pack-declined/scenario.json +10 -0
- package/dist/conformance/host/14-remove-pack/expected.json +14 -0
- package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/pack.json +13 -0
- package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/14-remove-pack/note.mk.md +5 -0
- package/dist/conformance/host/14-remove-pack/scenario.json +11 -0
- package/dist/conformance/host/15-load-packs-engine-views/expected.json +19 -0
- package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/pack.json +9 -0
- package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/webview.js +1 -0
- package/dist/conformance/host/15-load-packs-engine-views/note.mk.md +9 -0
- package/dist/conformance/host/15-load-packs-engine-views/scenario.json +10 -0
- package/dist/conformance/host/16-pack-trust/expected.json +37 -0
- package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/pack.json +12 -0
- package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/pack.json +12 -0
- package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/webview.js +1 -0
- package/dist/conformance/host/16-pack-trust/note.mk.md +5 -0
- package/dist/conformance/host/16-pack-trust/scenario.json +26 -0
- package/dist/conformance/host/README.md +221 -0
- package/dist/conformance.d.ts +2 -0
- package/dist/conformance.js +5 -0
- package/dist/diagnostics/render-diagnostics.d.ts +41 -0
- package/dist/diagnostics/render-diagnostics.js +46 -0
- package/dist/export/cascade.d.ts +98 -0
- package/dist/export/cascade.js +159 -0
- package/dist/export/export-zip.d.ts +19 -0
- package/dist/export/export-zip.js +38 -0
- package/dist/export/image-embed.d.ts +126 -0
- package/dist/export/image-embed.js +274 -0
- package/dist/export/note-export.d.ts +332 -0
- package/dist/export/note-export.js +366 -0
- package/dist/export/note-links.d.ts +83 -0
- package/dist/export/note-links.js +241 -0
- package/dist/glue.wasm +0 -0
- package/dist/host/adapter.d.ts +223 -0
- package/dist/host/adapter.js +9 -0
- package/dist/host/cascade-export-behavior.d.ts +120 -0
- package/dist/host/cascade-export-behavior.js +151 -0
- package/dist/host/conformance-runner.d.ts +156 -0
- package/dist/host/conformance-runner.js +634 -0
- package/dist/host/create-host.d.ts +253 -0
- package/dist/host/create-host.js +329 -0
- package/dist/host/editor-behavior.d.ts +157 -0
- package/dist/host/editor-behavior.js +227 -0
- package/dist/host/export/md-plain.d.ts +15 -0
- package/dist/host/export/md-plain.js +125 -0
- package/dist/host/export-behavior.d.ts +113 -0
- package/dist/host/export-behavior.js +257 -0
- package/dist/host/labels.d.ts +24 -0
- package/dist/host/labels.js +32 -0
- package/dist/host/pack-archive.d.ts +30 -0
- package/dist/host/pack-archive.js +68 -0
- package/dist/host/pack-install.d.ts +88 -0
- package/dist/host/pack-install.js +189 -0
- package/dist/host/pack-load.d.ts +72 -0
- package/dist/host/pack-load.js +57 -0
- package/dist/host/pack-remove.d.ts +21 -0
- package/dist/host/pack-remove.js +65 -0
- package/dist/host/pack-trust-behavior.d.ts +79 -0
- package/dist/host/pack-trust-behavior.js +262 -0
- package/dist/host/refresh-interval.d.ts +39 -0
- package/dist/host/refresh-interval.js +61 -0
- package/dist/host/run-behavior.d.ts +89 -0
- package/dist/host/run-behavior.js +168 -0
- package/dist/host/run-diagnostics.d.ts +26 -0
- package/dist/host/run-diagnostics.js +43 -0
- package/dist/host/script-execution.d.ts +72 -0
- package/dist/host/script-execution.js +68 -0
- package/dist/host/terminal-packs.d.ts +39 -0
- package/dist/host/terminal-packs.js +51 -0
- package/dist/html-engine.d.ts +2 -0
- package/dist/html-engine.js +29 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.js +70 -0
- package/dist/insert/component-catalog.d.ts +23 -0
- package/dist/insert/component-catalog.js +96 -0
- package/dist/node-fs.d.ts +14 -0
- package/dist/node-fs.js +70 -0
- package/dist/node.d.ts +5 -0
- package/dist/node.js +13 -0
- package/dist/package.json +3 -0
- package/dist/packs/discover.d.ts +119 -0
- package/dist/packs/discover.js +266 -0
- package/dist/packs/pack-diagnostics.d.ts +81 -0
- package/dist/packs/pack-diagnostics.js +105 -0
- package/dist/packs/pack-paths.d.ts +53 -0
- package/dist/packs/pack-paths.js +106 -0
- package/dist/packs/pack-render-registry.d.ts +83 -0
- package/dist/packs/pack-render-registry.js +182 -0
- package/dist/packs/pack-scripts.d.ts +20 -0
- package/dist/packs/pack-scripts.js +96 -0
- package/dist/packs/pack-trust.d.ts +57 -0
- package/dist/packs/pack-trust.js +111 -0
- package/dist/packs/prebuilt.d.ts +49 -0
- package/dist/packs/prebuilt.js +97 -0
- package/dist/path-util.d.ts +24 -0
- package/dist/path-util.js +82 -0
- package/dist/run/browser-isolate.d.ts +45 -0
- package/dist/run/browser-isolate.js +81 -0
- package/dist/run/bundle-document.d.ts +29 -0
- package/dist/run/bundle-document.js +40 -0
- package/dist/run/bundle-run.d.ts +188 -0
- package/dist/run/bundle-run.js +232 -0
- package/dist/run/grant-flow.d.ts +232 -0
- package/dist/run/grant-flow.js +390 -0
- package/dist/run/ip-address.d.ts +41 -0
- package/dist/run/ip-address.js +255 -0
- package/dist/run/isolate.d.ts +57 -0
- package/dist/run/isolate.js +22 -0
- package/dist/run/lua-resolver.d.ts +21 -0
- package/dist/run/lua-resolver.js +63 -0
- package/dist/run/net-bridge-worker.d.ts +26 -0
- package/dist/run/net-bridge-worker.js +66 -0
- package/dist/run/net-bridge.d.ts +56 -0
- package/dist/run/net-bridge.js +59 -0
- package/dist/run/net-pinning.d.ts +82 -0
- package/dist/run/net-pinning.js +137 -0
- package/dist/run/net-provider.d.ts +70 -0
- package/dist/run/net-provider.js +289 -0
- package/dist/run/node-isolate.d.ts +30 -0
- package/dist/run/node-isolate.js +117 -0
- package/dist/run/run-flow.d.ts +187 -0
- package/dist/run/run-flow.js +323 -0
- package/dist/run/run-host.d.ts +107 -0
- package/dist/run/run-host.js +241 -0
- package/dist/run/run-job.d.ts +130 -0
- package/dist/run/run-job.js +279 -0
- package/dist/run/run-progress.d.ts +55 -0
- package/dist/run/run-progress.js +42 -0
- package/dist/run/run-trace.d.ts +44 -0
- package/dist/run/run-trace.js +36 -0
- package/dist/run/script-requirements.d.ts +98 -0
- package/dist/run/script-requirements.js +212 -0
- package/dist/run/snapshot-storage.d.ts +25 -0
- package/dist/run/snapshot-storage.js +31 -0
- package/dist/run/stale-values.d.ts +25 -0
- package/dist/run/stale-values.js +32 -0
- package/dist/run/worker-entry-browser.d.ts +42 -0
- package/dist/run/worker-entry-browser.js +77 -0
- package/dist/run/worker-entry.d.ts +1 -0
- package/dist/run/worker-entry.js +96 -0
- package/dist/values-failure.d.ts +17 -0
- package/dist/values-failure.js +1 -0
- package/dist/values-merge.d.ts +39 -0
- package/dist/values-merge.js +33 -0
- package/package.json +84 -4
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host-side runner for slice 1 of the extension's v2 Run arc (GitHub
|
|
3
|
+
* issue #1's locked design comment). Plain Node — no `vscode` import — so
|
|
4
|
+
* it is unit-testable with Vitest and reusable unchanged once slice 2
|
|
5
|
+
* wires a command/button to it.
|
|
6
|
+
*
|
|
7
|
+
* `spawnRun` is the whole contract: give it a note's text, its net
|
|
8
|
+
* allowlist, a persisted cache snapshot, and a deadline; get back a
|
|
9
|
+
* `RunResult` that never rejects. The EXTERNAL watchdog
|
|
10
|
+
* (`setTimeout` -> `worker.terminate()`) is what makes this safe to call
|
|
11
|
+
* against untrusted script content — `./worker-entry.ts`'s own in-VM
|
|
12
|
+
* limits are a second, INNER layer (docs/security.md), but this file's job
|
|
13
|
+
* is the outer one: a kill switch that works even if everything inside the
|
|
14
|
+
* worker is compromised or wedged, because `terminate()` acts on the
|
|
15
|
+
* thread from outside it.
|
|
16
|
+
*/
|
|
17
|
+
import type { CacheEntry } from '@markii/lua';
|
|
18
|
+
import type { RunTrigger, StoredValue } from '@markii/runtime';
|
|
19
|
+
import type { RunJob, RunResult } from './worker-entry.js';
|
|
20
|
+
import type { IsolateSpawner } from './isolate.js';
|
|
21
|
+
export type { IsolateSpawner, RunIsolate, SpawnIsolateOptions, } from './isolate.js';
|
|
22
|
+
export type { RunJob, RunResult, RunFailure } from './worker-entry.js';
|
|
23
|
+
export type { RunProgress } from './run-progress.js';
|
|
24
|
+
/** What a caller of the shared run path supplies; the host adds the isolate (`SpawnRunOptions.spawnIsolate`, `.workerPath`). */
|
|
25
|
+
export type SpawnRunRequest = Omit<SpawnRunOptions, 'workerPath' | 'spawnIsolate'>;
|
|
26
|
+
export interface SpawnRunOptions {
|
|
27
|
+
text: string;
|
|
28
|
+
/**
|
|
29
|
+
* How this run was triggered (GitHub issue #11), forwarded verbatim to the
|
|
30
|
+
* worker's `RunJob.trigger` and, from there, to `runDocumentScripts` — the
|
|
31
|
+
* value that `@markii/runtime`'s `tierForTrigger` maps to the execution
|
|
32
|
+
* tier THE SANDBOX ENFORCES. `'manual'` unlocks every granted capability;
|
|
33
|
+
* `'auto'`/`'scheduled'` are read-only regardless of what was granted.
|
|
34
|
+
* Omitted defaults to `'manual'` in the worker, preserving the pre-#11
|
|
35
|
+
* behavior for any caller that has not been updated.
|
|
36
|
+
*/
|
|
37
|
+
trigger?: RunTrigger;
|
|
38
|
+
netAllowlist: string[];
|
|
39
|
+
/**
|
|
40
|
+
* The DNS-rebinding / private-range policy (GitHub issue #10), forwarded
|
|
41
|
+
* verbatim to the worker's `RunJob.netPolicy` — see `./net-pinning.ts`'s
|
|
42
|
+
* `PinPolicy` and `./worker-entry.ts`'s `createNetProvider`. Omitted
|
|
43
|
+
* defaults to the fail-closed posture in the worker
|
|
44
|
+
* (`allowRestrictedAddresses: false`), same pattern `trigger` and
|
|
45
|
+
* `packModules` already follow.
|
|
46
|
+
*/
|
|
47
|
+
netPolicy?: RunJob['netPolicy'];
|
|
48
|
+
cacheSnapshot: Record<string, CacheEntry>;
|
|
49
|
+
/** Wall-clock budget for the whole run, enforced by this file's own external watchdog — never delegated to the worker. */
|
|
50
|
+
timeoutMs: number;
|
|
51
|
+
/** Forwarded verbatim to the worker's `RunJob.limits` (the sandbox's own, INNER resource caps) — see `./worker-entry.ts`. */
|
|
52
|
+
limits?: RunJob['limits'];
|
|
53
|
+
/** Forwarded verbatim to the worker's `RunJob.bundle` (the bundle-fs capability snapshot) — see `./worker-entry.ts`. Absent for a bare `.mk.md` run. */
|
|
54
|
+
bundle?: RunJob['bundle'];
|
|
55
|
+
/** Forwarded verbatim to the worker's `RunJob.packModules` (pre-read pack Lua modules) — see `./worker-entry.ts`. Absent when no packs are configured. */
|
|
56
|
+
packModules?: RunJob['packModules'];
|
|
57
|
+
/**
|
|
58
|
+
* The KIND of isolate this run spawns. Required: this module has no
|
|
59
|
+
* default so it stays free of `node:*`. A Node host passes
|
|
60
|
+
* `workerThreadIsolate` from `@markii/host/node` (the only kind that
|
|
61
|
+
* accepts a V8 heap cap); a host whose runtime cannot create worker
|
|
62
|
+
* threads, such as an Electron renderer, supplies a Web Worker one. See
|
|
63
|
+
* `./isolate.ts` for the constraints an implementation must meet, and
|
|
64
|
+
* for what a Web Worker cannot bound that a worker thread can.
|
|
65
|
+
*
|
|
66
|
+
* The watchdog, the exactly-once settlement, and the never-rejects
|
|
67
|
+
* contract are NOT part of what this overrides: they stay in `spawnRun`
|
|
68
|
+
* for every host, so a new isolate kind cannot accidentally ship without
|
|
69
|
+
* a kill switch.
|
|
70
|
+
*/
|
|
71
|
+
spawnIsolate: IsolateSpawner;
|
|
72
|
+
/**
|
|
73
|
+
* Called once per script as its value arrives from the isolate, in
|
|
74
|
+
* document order, while the run is still going (GitHub issue #35). This
|
|
75
|
+
* is what lets a preview un-stale one component at a time instead of
|
|
76
|
+
* waiting for the whole batch: scripts run sequentially in one isolate,
|
|
77
|
+
* so the first value is often known seconds before the last.
|
|
78
|
+
*
|
|
79
|
+
* Every message is validated (`isRunProgress`) and ordered before it
|
|
80
|
+
* reaches this callback, and anything it throws is swallowed — reporting
|
|
81
|
+
* progress must never be able to fail a run or break the never-rejects
|
|
82
|
+
* contract below. It is never called after the run has settled.
|
|
83
|
+
*/
|
|
84
|
+
onValue?: (name: string, value: StoredValue, index: number) => void;
|
|
85
|
+
/**
|
|
86
|
+
* The entry the isolate loads. This package cannot know how a given host
|
|
87
|
+
* bundles or lays out its own `dist/` (see e.g.
|
|
88
|
+
* `apps/vscode/src/worker-path.ts`), so the host always passes it. The
|
|
89
|
+
* Node-only dev/Vitest fallback lives in `@markii/host/node`
|
|
90
|
+
* (`defaultWorkerPath`).
|
|
91
|
+
*/
|
|
92
|
+
workerPath: string;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Spawns one ephemeral worker, runs `options.text`'s scripts in it under
|
|
96
|
+
* the manual tier, and resolves with the outcome. NEVER REJECTS — every
|
|
97
|
+
* way this can go wrong (a message never arrives, the worker throws, the
|
|
98
|
+
* worker exits on its own, the external watchdog fires) resolves with an
|
|
99
|
+
* ordinary `RunResult` carrying a synthetic failure instead. Settlement is
|
|
100
|
+
* exactly-once: whichever of `message` / `error` / `exit` / the watchdog
|
|
101
|
+
* fires FIRST wins; every later event on the same worker is a no-op (the
|
|
102
|
+
* `settled` guard below), so there is no risk of a double-`resolve` race
|
|
103
|
+
* even though several of these could in principle fire in close
|
|
104
|
+
* succession (e.g. `terminate()` after settling on `message` still
|
|
105
|
+
* produces its own `exit` event).
|
|
106
|
+
*/
|
|
107
|
+
export declare function spawnRun(options: SpawnRunOptions): Promise<RunResult>;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// From `./run-progress.ts`, NOT from `./run-job.ts`: that module imports
|
|
2
|
+
// `@markii/lua`, and a value import of it here would drag a WebAssembly Lua
|
|
3
|
+
// engine into every host bundle that runs a job (the Obsidian plugin's
|
|
4
|
+
// renderer bundle above all, which never executes Lua itself).
|
|
5
|
+
import { isRunProgress } from './run-progress.js';
|
|
6
|
+
/**
|
|
7
|
+
* `resourceLimits.maxOldGenerationSizeMb` for the spawned worker (A-1): a
|
|
8
|
+
* script's own Lua memory is already capped in-VM (`@markii/lua`'s
|
|
9
|
+
* `limits.maxMemoryBytes`), but nothing previously bounded the JS/V8 heap
|
|
10
|
+
* OF THE WORKER ITSELF — a large marshaled return value, an oversize
|
|
11
|
+
* fetched/cached JSON payload, or any other JS-side allocation this file's
|
|
12
|
+
* own code performs could still OOM that heap. Without an explicit
|
|
13
|
+
* `resourceLimits`, a V8 OOM inside a `worker_threads` worker is fatal to
|
|
14
|
+
* the WHOLE PROCESS (the extension host), not just that thread — exactly
|
|
15
|
+
* the failure mode the external, always-available `terminate()` watchdog
|
|
16
|
+
* above is meant to make impossible for a wedged/hostile script. 128MB is
|
|
17
|
+
* comfortably above what one script's marshaled result or one cached
|
|
18
|
+
* fetch response (`@markii/lua`'s `DEFAULT_MAX_FETCH_BYTES`, 2MB) should
|
|
19
|
+
* ever need, while still being small enough that a runaway allocation hits
|
|
20
|
+
* this cap, and the resulting worker `'error'` event, long before it could
|
|
21
|
+
* threaten the host process's own heap.
|
|
22
|
+
*/
|
|
23
|
+
const WORKER_MAX_OLD_GENERATION_SIZE_MB = 128;
|
|
24
|
+
/**
|
|
25
|
+
* How many per-script progress values one run may report (GitHub issue
|
|
26
|
+
* #35). A real note has a handful of script blocks; this cap exists only
|
|
27
|
+
* so a compromised or malfunctioning isolate cannot grow the host's memory
|
|
28
|
+
* by inventing names, since the accumulated map is what a killed run's
|
|
29
|
+
* synthetic result carries. Messages past the cap are dropped, and the run
|
|
30
|
+
* itself is unaffected — its own `RunResult.values` is the authority
|
|
31
|
+
* whenever the worker gets to send one.
|
|
32
|
+
*/
|
|
33
|
+
const MAX_PROGRESS_VALUES = 1000;
|
|
34
|
+
function describeThrown(err) {
|
|
35
|
+
return err instanceof Error ? err.message : String(err);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Spawns one ephemeral worker, runs `options.text`'s scripts in it under
|
|
39
|
+
* the manual tier, and resolves with the outcome. NEVER REJECTS — every
|
|
40
|
+
* way this can go wrong (a message never arrives, the worker throws, the
|
|
41
|
+
* worker exits on its own, the external watchdog fires) resolves with an
|
|
42
|
+
* ordinary `RunResult` carrying a synthetic failure instead. Settlement is
|
|
43
|
+
* exactly-once: whichever of `message` / `error` / `exit` / the watchdog
|
|
44
|
+
* fires FIRST wins; every later event on the same worker is a no-op (the
|
|
45
|
+
* `settled` guard below), so there is no risk of a double-`resolve` race
|
|
46
|
+
* even though several of these could in principle fire in close
|
|
47
|
+
* succession (e.g. `terminate()` after settling on `message` still
|
|
48
|
+
* produces its own `exit` event).
|
|
49
|
+
*/
|
|
50
|
+
export async function spawnRun(options) {
|
|
51
|
+
const workerPath = options.workerPath;
|
|
52
|
+
const job = {
|
|
53
|
+
text: options.text,
|
|
54
|
+
netAllowlist: options.netAllowlist,
|
|
55
|
+
cacheSnapshot: options.cacheSnapshot,
|
|
56
|
+
...(options.trigger !== undefined ? { trigger: options.trigger } : {}),
|
|
57
|
+
...(options.netPolicy !== undefined
|
|
58
|
+
? { netPolicy: options.netPolicy }
|
|
59
|
+
: {}),
|
|
60
|
+
...(options.limits !== undefined ? { limits: options.limits } : {}),
|
|
61
|
+
...(options.bundle !== undefined ? { bundle: options.bundle } : {}),
|
|
62
|
+
...(options.packModules !== undefined
|
|
63
|
+
? { packModules: options.packModules }
|
|
64
|
+
: {}),
|
|
65
|
+
};
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
let settled = false;
|
|
68
|
+
let watchdogFired = false;
|
|
69
|
+
// GitHub issue #35: every value the isolate has reported so far, in
|
|
70
|
+
// arrival order. A Map (not a plain object) because a script may
|
|
71
|
+
// legitimately be named `__proto__` (docs/spec.md's name grammar) and
|
|
72
|
+
// a plain assignment of that key sets a prototype instead of storing a
|
|
73
|
+
// value; `valuesSoFar` converts through `Object.fromEntries`, which
|
|
74
|
+
// defines it as an own property.
|
|
75
|
+
const progressValues = new Map();
|
|
76
|
+
// The last ordinal accepted, so a duplicate or out-of-order message
|
|
77
|
+
// from a misbehaving isolate is dropped rather than overwriting a
|
|
78
|
+
// value that already arrived. -1 means "nothing accepted yet".
|
|
79
|
+
let lastProgressIndex = -1;
|
|
80
|
+
/**
|
|
81
|
+
* The values this run has seen so far — what a run that never gets to
|
|
82
|
+
* send its own `RunResult` (the watchdog fired, the isolate died) is
|
|
83
|
+
* settled with, so the scripts that DID finish keep the values they
|
|
84
|
+
* produced instead of the whole run coming back empty. A worker that
|
|
85
|
+
* does send a result supersedes this entirely: its own `values` is
|
|
86
|
+
* complete and authoritative.
|
|
87
|
+
*/
|
|
88
|
+
function valuesSoFar() {
|
|
89
|
+
return Object.fromEntries(progressValues);
|
|
90
|
+
}
|
|
91
|
+
const worker = options.spawnIsolate({
|
|
92
|
+
entryPath: workerPath,
|
|
93
|
+
maxOldGenerationSizeMb: WORKER_MAX_OLD_GENERATION_SIZE_MB,
|
|
94
|
+
});
|
|
95
|
+
function watchdogFailure() {
|
|
96
|
+
return {
|
|
97
|
+
values: valuesSoFar(),
|
|
98
|
+
failures: [
|
|
99
|
+
{
|
|
100
|
+
name: '<document>',
|
|
101
|
+
message: `run exceeded its ${options.timeoutMs}ms watchdog and was terminated`,
|
|
102
|
+
kind: 'limit',
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
cacheSnapshot: options.cacheSnapshot,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const watchdog = setTimeout(() => {
|
|
109
|
+
watchdogFired = true;
|
|
110
|
+
// Fire-and-forget: `terminate()`'s own returned promise resolves
|
|
111
|
+
// once the thread is actually gone, but we don't need to wait on
|
|
112
|
+
// it here -- the worker's `exit` event (handled below) is what
|
|
113
|
+
// settles this run's promise, and it fires as part of the same
|
|
114
|
+
// termination sequence.
|
|
115
|
+
worker.kill();
|
|
116
|
+
// ...except where there IS no exit event to wait for. A Web Worker's
|
|
117
|
+
// `terminate()` is immediate and silent, so a host using one would
|
|
118
|
+
// otherwise hang here forever, having killed the isolate and then
|
|
119
|
+
// waited for a notification that never comes. Settling directly is
|
|
120
|
+
// correct for both kinds: a worker thread's `exit` still arrives and
|
|
121
|
+
// is a no-op against `settled`, and this run's outcome does not
|
|
122
|
+
// depend on how promptly the runtime reports the death of something
|
|
123
|
+
// already terminated.
|
|
124
|
+
settle(watchdogFailure());
|
|
125
|
+
}, options.timeoutMs);
|
|
126
|
+
// Never let this timer keep the host process/extension-host alive on
|
|
127
|
+
// its own.
|
|
128
|
+
watchdog.unref?.();
|
|
129
|
+
function settle(result) {
|
|
130
|
+
if (settled)
|
|
131
|
+
return;
|
|
132
|
+
settled = true;
|
|
133
|
+
clearTimeout(watchdog);
|
|
134
|
+
resolve(result);
|
|
135
|
+
// Ephemeral-per-run: tear the worker down once we have ANY outcome,
|
|
136
|
+
// including a normal successful `message`. `terminate()` on a
|
|
137
|
+
// worker that has already exited (or is already exiting) is a safe
|
|
138
|
+
// no-op, per Node's `worker_threads` documentation.
|
|
139
|
+
worker.kill();
|
|
140
|
+
}
|
|
141
|
+
worker.onMessage((message) => {
|
|
142
|
+
// GitHub issue #35: progress first, result last. A message that
|
|
143
|
+
// passes the progress guard is never treated as a result, and one
|
|
144
|
+
// that does not is the run's single result message — the same
|
|
145
|
+
// message this handler has always received.
|
|
146
|
+
if (isRunProgress(message)) {
|
|
147
|
+
if (settled)
|
|
148
|
+
return;
|
|
149
|
+
if (message.index <= lastProgressIndex)
|
|
150
|
+
return;
|
|
151
|
+
lastProgressIndex = message.index;
|
|
152
|
+
// The cap bounds how many DISTINCT names are held; re-reporting a
|
|
153
|
+
// name already in the map is a legitimate run (two script blocks
|
|
154
|
+
// may share a name, and the last one wins in the store too), so it
|
|
155
|
+
// is never refused.
|
|
156
|
+
if (!progressValues.has(message.name) &&
|
|
157
|
+
progressValues.size >= MAX_PROGRESS_VALUES) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
progressValues.set(message.name, message.value);
|
|
161
|
+
try {
|
|
162
|
+
options.onValue?.(message.name, message.value, message.index);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Reporting a value must never be able to fail the run — see
|
|
166
|
+
// `SpawnRunOptions.onValue`.
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
settle(message);
|
|
171
|
+
});
|
|
172
|
+
worker.onError((err) => {
|
|
173
|
+
settle({
|
|
174
|
+
values: valuesSoFar(),
|
|
175
|
+
failures: [
|
|
176
|
+
{
|
|
177
|
+
name: '<worker>',
|
|
178
|
+
message: describeThrown(err),
|
|
179
|
+
kind: 'script-error',
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
cacheSnapshot: options.cacheSnapshot,
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
worker.onExit((code) => {
|
|
186
|
+
// A `message` already having settled this run is the ordinary
|
|
187
|
+
// happy path -- `settle`'s own `terminate()` call produces exactly
|
|
188
|
+
// this `exit` event, and `settle`'s guard makes it a no-op here.
|
|
189
|
+
if (settled)
|
|
190
|
+
return;
|
|
191
|
+
if (watchdogFired) {
|
|
192
|
+
settle(watchdogFailure());
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
// The worker exited on its own, without the watchdog and without
|
|
196
|
+
// ever posting a result -- e.g. a hostile/rigged script that calls
|
|
197
|
+
// `process.exit()` directly (Node terminates only the calling
|
|
198
|
+
// worker thread for that, not the host process, but this run still
|
|
199
|
+
// never got a proper `RunResult` out of it). Resolve, never
|
|
200
|
+
// reject -- see this function's doc comment.
|
|
201
|
+
settle({
|
|
202
|
+
values: valuesSoFar(),
|
|
203
|
+
failures: [
|
|
204
|
+
{
|
|
205
|
+
name: '<worker>',
|
|
206
|
+
message: `worker exited unexpectedly (code ${String(code)}) before returning a result`,
|
|
207
|
+
kind: 'script-error',
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
cacheSnapshot: options.cacheSnapshot,
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
// N-2 fix (docs/archive/PENTEST-REPORT-2026-08-23.md): `postMessage` structured-clones
|
|
214
|
+
// `job` synchronously, so an uncloneable payload (e.g. a value containing
|
|
215
|
+
// a function, or an object with a throwing `toString`) throws a
|
|
216
|
+
// `DataCloneError` right here, before any of the worker's own event
|
|
217
|
+
// handlers above have a chance to settle this promise. Left unguarded,
|
|
218
|
+
// that throw would propagate out of the executor and REJECT `spawnRun`'s
|
|
219
|
+
// promise, breaking its documented never-rejects contract. Route it
|
|
220
|
+
// through the same synthetic-failure `settle` path as every other
|
|
221
|
+
// failure mode instead, and let `settle`'s own `terminate()` clean up the
|
|
222
|
+
// worker that was already spawned (a safe no-op if the thread never
|
|
223
|
+
// finished starting).
|
|
224
|
+
try {
|
|
225
|
+
worker.send(job);
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
settle({
|
|
229
|
+
values: {},
|
|
230
|
+
failures: [
|
|
231
|
+
{
|
|
232
|
+
name: '<document>',
|
|
233
|
+
message: `run could not be started: ${describeThrown(err)}`,
|
|
234
|
+
kind: 'script-error',
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
cacheSnapshot: options.cacheSnapshot,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { type FailureKind, type RunTrigger, type StoredValue } from '@markii/runtime';
|
|
2
|
+
import { type CacheEntry, type NetProvider } from '@markii/lua';
|
|
3
|
+
import type { BundleFsGrant, BundleManifest } from '@markii/bundle';
|
|
4
|
+
import type { PackModulesMap } from './lua-resolver.js';
|
|
5
|
+
import type { PinPolicy } from './net-pinning.js';
|
|
6
|
+
import type { RunProgress } from './run-progress.js';
|
|
7
|
+
export type { RunProgress } from './run-progress.js';
|
|
8
|
+
export { isRunProgress } from './run-progress.js';
|
|
9
|
+
/**
|
|
10
|
+
* What a runtime must supply for `runJob` to work there.
|
|
11
|
+
*
|
|
12
|
+
* `createNet` receives the SAME arguments the Node provider always took,
|
|
13
|
+
* so a browser implementation cannot quietly widen the allowlist or the
|
|
14
|
+
* fetch cap: both are handed to it, and `@markii/lua` enforces its own
|
|
15
|
+
* copy of the cap independently regardless.
|
|
16
|
+
*/
|
|
17
|
+
export interface RunJobDeps {
|
|
18
|
+
createNet: (netAllowlist: string[], maxFetchBytes: number, netPolicy: PinPolicy) => NetProvider;
|
|
19
|
+
/** Where wasmoon should load `glue.wasm` from; `undefined` lets it use its own default resolution. */
|
|
20
|
+
wasmUri: string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Posts one per-script progress message back to the host (GitHub issue
|
|
23
|
+
* #35). Supplied by each worker entry so BOTH isolates emit the identical
|
|
24
|
+
* message: the shape, the ordinal, and the moment it is sent are decided
|
|
25
|
+
* here, and the entry only knows how to put a message on its own channel
|
|
26
|
+
* (`parentPort.postMessage` for the Node worker thread,
|
|
27
|
+
* `self.postMessage` for the Web Worker).
|
|
28
|
+
*
|
|
29
|
+
* Optional so an entry that has not been updated simply reports nothing
|
|
30
|
+
* during a run and its final result is unchanged.
|
|
31
|
+
*/
|
|
32
|
+
postProgress?: (message: RunProgress) => void;
|
|
33
|
+
}
|
|
34
|
+
/** The one job message this worker ever receives, posted once by `run-host.ts`. */
|
|
35
|
+
export interface RunJob {
|
|
36
|
+
text: string;
|
|
37
|
+
/**
|
|
38
|
+
* How this run was triggered (GitHub issue #11). Passed straight to
|
|
39
|
+
* `runDocumentScripts`, whose `tierForTrigger` (`@markii/runtime`) is THE
|
|
40
|
+
* SECURITY GATE: `'manual'` runs at the full capability tier, while
|
|
41
|
+
* `'auto'` and `'scheduled'` are forced to the read-only tier (GET,
|
|
42
|
+
* cache and bundle reads, cache writes — never POST/PATCH/bundle-write)
|
|
43
|
+
* regardless of what the net/bundle grants allow. Absent (an older host,
|
|
44
|
+
* or `run-host.ts` omitting it) defaults to `'manual'`, preserving the
|
|
45
|
+
* pre-#11 behavior; this default is safe because only the trusted host
|
|
46
|
+
* ever sets this field — note content can never influence it.
|
|
47
|
+
*/
|
|
48
|
+
trigger?: RunTrigger;
|
|
49
|
+
/** Hostnames (exact match, case-insensitive) this run's `net.*` calls may reach. */
|
|
50
|
+
netAllowlist: string[];
|
|
51
|
+
/** The persisted `cache.get` state to seed this run with (see `./script-requirements.ts`'s sibling host-side persistence). */
|
|
52
|
+
cacheSnapshot: Record<string, CacheEntry>;
|
|
53
|
+
/**
|
|
54
|
+
* Optional resource-limit overrides, forwarded verbatim to
|
|
55
|
+
* `createLuaExecutor`'s `limits`/`maxFetchBytes`. Left `undefined`, the
|
|
56
|
+
* sandbox's own defaults (`@markii/lua`'s `DEFAULT_LIMITS`/
|
|
57
|
+
* `DEFAULT_MAX_FETCH_BYTES`) apply.
|
|
58
|
+
*/
|
|
59
|
+
limits?: {
|
|
60
|
+
maxFetchBytes?: number;
|
|
61
|
+
wallClockMs?: number;
|
|
62
|
+
maxInstructions?: number;
|
|
63
|
+
maxMemoryBytes?: number;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Slice 2 of the `.mkz` Run-path arc (GitHub issue #9): the bundle
|
|
67
|
+
* filesystem capability for a bundle-backed run, entirely as an
|
|
68
|
+
* in-memory snapshot — see `./bundle-run.ts`'s doc comment for what this
|
|
69
|
+
* host-built snapshot contains and why. Absent for a bare `.mk.md` run,
|
|
70
|
+
* exactly as before this field existed.
|
|
71
|
+
*/
|
|
72
|
+
bundle?: {
|
|
73
|
+
/** Bundle-relative path -> bytes, built host-side by `./bundle-run.ts`'s `buildBundleSnapshot`. */
|
|
74
|
+
snapshot: Record<string, Uint8Array>;
|
|
75
|
+
/** The bundle's manifest — `@markii/bundle`'s `createScriptView` reads its `permissions.bundle` declaration to intersect against `grantedBundlePermissions`. */
|
|
76
|
+
manifest: BundleManifest;
|
|
77
|
+
/** The user-granted bundle-fs permissions for this run (already the OUTCOME of the host's own grant flow, not the manifest's raw declaration) — `createScriptView` intersects this with what the manifest declares. */
|
|
78
|
+
grantedBundlePermissions: BundleFsGrant[];
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Slice 5 of the pack-loading arc (GitHub issue #3): shared Lua modules
|
|
82
|
+
* from every configured, installed pack's `scripts/` directory, pre-read
|
|
83
|
+
* on the extension host (`../packs/pack-scripts.ts`) since this worker has
|
|
84
|
+
* no filesystem access of its own. Builds this run's `PackModuleResolver`
|
|
85
|
+
* (`../packs/lua-resolver.ts`) — a pure, synchronous, in-memory lookup, no
|
|
86
|
+
* I/O inside the worker. Absent for a run with no packs configured, in
|
|
87
|
+
* which case `require "packName/..."` denies exactly as it always has
|
|
88
|
+
* (`@markii/lua`'s own "no resolver configured" capability denial).
|
|
89
|
+
*/
|
|
90
|
+
packModules?: PackModulesMap;
|
|
91
|
+
/**
|
|
92
|
+
* The DNS-rebinding / private-range policy (GitHub issue #10) this run's
|
|
93
|
+
* `net.*` calls are pinned under — see `./net-pinning.ts`'s `PinPolicy`.
|
|
94
|
+
* Threaded the same way `trigger` and `packModules` are: forwarded
|
|
95
|
+
* verbatim from `SpawnRunOptions` (`./run-host.ts`) through to here, with
|
|
96
|
+
* no note content able to influence it. FAIL CLOSED: absent (an older
|
|
97
|
+
* host, or a caller that hasn't been updated) means
|
|
98
|
+
* `allowRestrictedAddresses: false` — the safe default — not "no policy
|
|
99
|
+
* at all".
|
|
100
|
+
*/
|
|
101
|
+
netPolicy?: PinPolicy;
|
|
102
|
+
}
|
|
103
|
+
/** One failed script, in the shape the host needs to drive the grant/UI flow — never a raw thrown error. */
|
|
104
|
+
export interface RunFailure {
|
|
105
|
+
/** The script's declared `name`, or `'<document>'` for a failure that happened outside any single script (e.g. the text failed to parse). */
|
|
106
|
+
name: string;
|
|
107
|
+
message: string;
|
|
108
|
+
kind: FailureKind;
|
|
109
|
+
}
|
|
110
|
+
/** The one result message this worker ever posts back. Every field is structured-clone-safe. */
|
|
111
|
+
export interface RunResult {
|
|
112
|
+
/** `ValueStore.snapshot()` — every script's outcome, keyed by name. */
|
|
113
|
+
values: Record<string, StoredValue>;
|
|
114
|
+
failures: RunFailure[];
|
|
115
|
+
/** The mutated cache state, to be persisted by the host for the next run. */
|
|
116
|
+
cacheSnapshot: Record<string, CacheEntry>;
|
|
117
|
+
/**
|
|
118
|
+
* Present only when `RunJob.bundle` was set: the FULL, post-run contents
|
|
119
|
+
* of the bundle snapshot's `.cache/` subtree (not a diff) — the host
|
|
120
|
+
* persists this verbatim (directory form: written back into the bundle
|
|
121
|
+
* dir; zip form: extension storage keyed by bundle identity) and seeds
|
|
122
|
+
* the next run's snapshot from it. Absent for a bare `.mk.md` run.
|
|
123
|
+
*/
|
|
124
|
+
cacheOut?: Record<string, Uint8Array>;
|
|
125
|
+
}
|
|
126
|
+
/** Runs one job to completion, never throwing — see this module's top doc comment. */
|
|
127
|
+
export declare function runJob(job: RunJob, deps: RunJobDeps): Promise<RunResult>;
|
|
128
|
+
/** Turns any unexpected internal failure into an ordinary result — see the top doc comment's "never an unhandled rejection" guarantee. */
|
|
129
|
+
export declare function resultForInternalError(err: unknown, fallbackCacheSnapshot: Record<string, CacheEntry>): RunResult;
|
|
130
|
+
export declare function isRunJob(value: unknown): value is RunJob;
|