@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.
Files changed (193) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -2
  3. package/dist/browser-worker.js +639 -0
  4. package/dist/browser.d.ts +57 -0
  5. package/dist/browser.js +167 -0
  6. package/dist/conformance/host/01-plain-open/expected.json +5 -0
  7. package/dist/conformance/host/01-plain-open/note.mk.md +8 -0
  8. package/dist/conformance/host/01-plain-open/scenario.json +6 -0
  9. package/dist/conformance/host/02-run-manual-grant-accepted/expected.json +16 -0
  10. package/dist/conformance/host/02-run-manual-grant-accepted/note.mk.md +5 -0
  11. package/dist/conformance/host/02-run-manual-grant-accepted/scenario.json +14 -0
  12. package/dist/conformance/host/03-run-manual-grant-refused/expected.json +18 -0
  13. package/dist/conformance/host/03-run-manual-grant-refused/note.mk.md +5 -0
  14. package/dist/conformance/host/03-run-manual-grant-refused/scenario.json +14 -0
  15. package/dist/conformance/host/04-run-auto-write-denied/expected.json +16 -0
  16. package/dist/conformance/host/04-run-auto-write-denied/note.mk.md +10 -0
  17. package/dist/conformance/host/04-run-auto-write-denied/scenario.json +13 -0
  18. package/dist/conformance/host/05-export-html-pack-directive/expected.json +14 -0
  19. package/dist/conformance/host/05-export-html-pack-directive/note.mk.md +8 -0
  20. package/dist/conformance/host/05-export-html-pack-directive/scenario.json +6 -0
  21. package/dist/conformance/host/06-install-pack-then-render/expected.json +18 -0
  22. package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/pack.json +13 -0
  23. package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/webview.js +1 -0
  24. package/dist/conformance/host/06-install-pack-then-render/note.mk.md +5 -0
  25. package/dist/conformance/host/06-install-pack-then-render/scenario.json +15 -0
  26. package/dist/conformance/host/07-complete-in-directive/expected.json +14 -0
  27. package/dist/conformance/host/07-complete-in-directive/note.mk.md +3 -0
  28. package/dist/conformance/host/07-complete-in-directive/scenario.json +6 -0
  29. package/dist/conformance/host/08-hover-standard-component/expected.json +5 -0
  30. package/dist/conformance/host/08-hover-standard-component/note.mk.md +5 -0
  31. package/dist/conformance/host/08-hover-standard-component/scenario.json +6 -0
  32. package/dist/conformance/host/09-insert-skeleton/expected.json +5 -0
  33. package/dist/conformance/host/09-insert-skeleton/note.mk.md +4 -0
  34. package/dist/conformance/host/09-insert-skeleton/scenario.json +6 -0
  35. package/dist/conformance/host/10-scripts-disabled/expected.json +11 -0
  36. package/dist/conformance/host/10-scripts-disabled/note.mk.md +5 -0
  37. package/dist/conformance/host/10-scripts-disabled/scenario.json +9 -0
  38. package/dist/conformance/host/11-complete-closing-fence/expected.json +5 -0
  39. package/dist/conformance/host/11-complete-closing-fence/note.mk.md +5 -0
  40. package/dist/conformance/host/11-complete-closing-fence/scenario.json +12 -0
  41. package/dist/conformance/host/12-complete-nested-container/expected.json +5 -0
  42. package/dist/conformance/host/12-complete-nested-container/note.mk.md +5 -0
  43. package/dist/conformance/host/12-complete-nested-container/scenario.json +12 -0
  44. package/dist/conformance/host/13-install-pack-declined/expected.json +12 -0
  45. package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/pack.json +13 -0
  46. package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/webview.js +1 -0
  47. package/dist/conformance/host/13-install-pack-declined/note.mk.md +5 -0
  48. package/dist/conformance/host/13-install-pack-declined/scenario.json +10 -0
  49. package/dist/conformance/host/14-remove-pack/expected.json +14 -0
  50. package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/pack.json +13 -0
  51. package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/webview.js +1 -0
  52. package/dist/conformance/host/14-remove-pack/note.mk.md +5 -0
  53. package/dist/conformance/host/14-remove-pack/scenario.json +11 -0
  54. package/dist/conformance/host/15-load-packs-engine-views/expected.json +19 -0
  55. package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/pack.json +9 -0
  56. package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/webview.js +1 -0
  57. package/dist/conformance/host/15-load-packs-engine-views/note.mk.md +9 -0
  58. package/dist/conformance/host/15-load-packs-engine-views/scenario.json +10 -0
  59. package/dist/conformance/host/16-pack-trust/expected.json +37 -0
  60. package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/pack.json +12 -0
  61. package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/webview.js +1 -0
  62. package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/pack.json +12 -0
  63. package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/webview.js +1 -0
  64. package/dist/conformance/host/16-pack-trust/note.mk.md +5 -0
  65. package/dist/conformance/host/16-pack-trust/scenario.json +26 -0
  66. package/dist/conformance/host/README.md +221 -0
  67. package/dist/conformance.d.ts +2 -0
  68. package/dist/conformance.js +5 -0
  69. package/dist/diagnostics/render-diagnostics.d.ts +41 -0
  70. package/dist/diagnostics/render-diagnostics.js +46 -0
  71. package/dist/export/cascade.d.ts +98 -0
  72. package/dist/export/cascade.js +159 -0
  73. package/dist/export/export-zip.d.ts +19 -0
  74. package/dist/export/export-zip.js +38 -0
  75. package/dist/export/image-embed.d.ts +126 -0
  76. package/dist/export/image-embed.js +274 -0
  77. package/dist/export/note-export.d.ts +332 -0
  78. package/dist/export/note-export.js +366 -0
  79. package/dist/export/note-links.d.ts +83 -0
  80. package/dist/export/note-links.js +241 -0
  81. package/dist/glue.wasm +0 -0
  82. package/dist/host/adapter.d.ts +223 -0
  83. package/dist/host/adapter.js +9 -0
  84. package/dist/host/cascade-export-behavior.d.ts +120 -0
  85. package/dist/host/cascade-export-behavior.js +151 -0
  86. package/dist/host/conformance-runner.d.ts +156 -0
  87. package/dist/host/conformance-runner.js +634 -0
  88. package/dist/host/create-host.d.ts +253 -0
  89. package/dist/host/create-host.js +329 -0
  90. package/dist/host/editor-behavior.d.ts +157 -0
  91. package/dist/host/editor-behavior.js +227 -0
  92. package/dist/host/export/md-plain.d.ts +15 -0
  93. package/dist/host/export/md-plain.js +125 -0
  94. package/dist/host/export-behavior.d.ts +113 -0
  95. package/dist/host/export-behavior.js +257 -0
  96. package/dist/host/labels.d.ts +24 -0
  97. package/dist/host/labels.js +32 -0
  98. package/dist/host/pack-archive.d.ts +30 -0
  99. package/dist/host/pack-archive.js +68 -0
  100. package/dist/host/pack-install.d.ts +88 -0
  101. package/dist/host/pack-install.js +189 -0
  102. package/dist/host/pack-load.d.ts +72 -0
  103. package/dist/host/pack-load.js +57 -0
  104. package/dist/host/pack-remove.d.ts +21 -0
  105. package/dist/host/pack-remove.js +65 -0
  106. package/dist/host/pack-trust-behavior.d.ts +79 -0
  107. package/dist/host/pack-trust-behavior.js +262 -0
  108. package/dist/host/refresh-interval.d.ts +39 -0
  109. package/dist/host/refresh-interval.js +61 -0
  110. package/dist/host/run-behavior.d.ts +89 -0
  111. package/dist/host/run-behavior.js +168 -0
  112. package/dist/host/run-diagnostics.d.ts +26 -0
  113. package/dist/host/run-diagnostics.js +43 -0
  114. package/dist/host/script-execution.d.ts +72 -0
  115. package/dist/host/script-execution.js +68 -0
  116. package/dist/host/terminal-packs.d.ts +39 -0
  117. package/dist/host/terminal-packs.js +51 -0
  118. package/dist/html-engine.d.ts +2 -0
  119. package/dist/html-engine.js +29 -0
  120. package/dist/index.d.ts +95 -0
  121. package/dist/index.js +70 -0
  122. package/dist/insert/component-catalog.d.ts +23 -0
  123. package/dist/insert/component-catalog.js +96 -0
  124. package/dist/node-fs.d.ts +14 -0
  125. package/dist/node-fs.js +70 -0
  126. package/dist/node.d.ts +5 -0
  127. package/dist/node.js +13 -0
  128. package/dist/package.json +3 -0
  129. package/dist/packs/discover.d.ts +119 -0
  130. package/dist/packs/discover.js +266 -0
  131. package/dist/packs/pack-diagnostics.d.ts +81 -0
  132. package/dist/packs/pack-diagnostics.js +105 -0
  133. package/dist/packs/pack-paths.d.ts +53 -0
  134. package/dist/packs/pack-paths.js +106 -0
  135. package/dist/packs/pack-render-registry.d.ts +83 -0
  136. package/dist/packs/pack-render-registry.js +182 -0
  137. package/dist/packs/pack-scripts.d.ts +20 -0
  138. package/dist/packs/pack-scripts.js +96 -0
  139. package/dist/packs/pack-trust.d.ts +57 -0
  140. package/dist/packs/pack-trust.js +111 -0
  141. package/dist/packs/prebuilt.d.ts +49 -0
  142. package/dist/packs/prebuilt.js +97 -0
  143. package/dist/path-util.d.ts +24 -0
  144. package/dist/path-util.js +82 -0
  145. package/dist/run/browser-isolate.d.ts +45 -0
  146. package/dist/run/browser-isolate.js +81 -0
  147. package/dist/run/bundle-document.d.ts +29 -0
  148. package/dist/run/bundle-document.js +40 -0
  149. package/dist/run/bundle-run.d.ts +188 -0
  150. package/dist/run/bundle-run.js +232 -0
  151. package/dist/run/grant-flow.d.ts +232 -0
  152. package/dist/run/grant-flow.js +390 -0
  153. package/dist/run/ip-address.d.ts +41 -0
  154. package/dist/run/ip-address.js +255 -0
  155. package/dist/run/isolate.d.ts +57 -0
  156. package/dist/run/isolate.js +22 -0
  157. package/dist/run/lua-resolver.d.ts +21 -0
  158. package/dist/run/lua-resolver.js +63 -0
  159. package/dist/run/net-bridge-worker.d.ts +26 -0
  160. package/dist/run/net-bridge-worker.js +66 -0
  161. package/dist/run/net-bridge.d.ts +56 -0
  162. package/dist/run/net-bridge.js +59 -0
  163. package/dist/run/net-pinning.d.ts +82 -0
  164. package/dist/run/net-pinning.js +137 -0
  165. package/dist/run/net-provider.d.ts +70 -0
  166. package/dist/run/net-provider.js +289 -0
  167. package/dist/run/node-isolate.d.ts +30 -0
  168. package/dist/run/node-isolate.js +117 -0
  169. package/dist/run/run-flow.d.ts +187 -0
  170. package/dist/run/run-flow.js +323 -0
  171. package/dist/run/run-host.d.ts +107 -0
  172. package/dist/run/run-host.js +241 -0
  173. package/dist/run/run-job.d.ts +130 -0
  174. package/dist/run/run-job.js +279 -0
  175. package/dist/run/run-progress.d.ts +55 -0
  176. package/dist/run/run-progress.js +42 -0
  177. package/dist/run/run-trace.d.ts +44 -0
  178. package/dist/run/run-trace.js +36 -0
  179. package/dist/run/script-requirements.d.ts +98 -0
  180. package/dist/run/script-requirements.js +212 -0
  181. package/dist/run/snapshot-storage.d.ts +25 -0
  182. package/dist/run/snapshot-storage.js +31 -0
  183. package/dist/run/stale-values.d.ts +25 -0
  184. package/dist/run/stale-values.js +32 -0
  185. package/dist/run/worker-entry-browser.d.ts +42 -0
  186. package/dist/run/worker-entry-browser.js +77 -0
  187. package/dist/run/worker-entry.d.ts +1 -0
  188. package/dist/run/worker-entry.js +96 -0
  189. package/dist/values-failure.d.ts +17 -0
  190. package/dist/values-failure.js +1 -0
  191. package/dist/values-merge.d.ts +39 -0
  192. package/dist/values-merge.js +33 -0
  193. 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;