@markii/host 0.0.1 → 0.17.0-next.3

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/README.md +20 -2
  2. package/dist/browser-worker.js +639 -0
  3. package/dist/browser.d.ts +61 -0
  4. package/dist/browser.js +172 -0
  5. package/dist/conformance/host/01-plain-open/expected.json +5 -0
  6. package/dist/conformance/host/01-plain-open/note.mk.md +8 -0
  7. package/dist/conformance/host/01-plain-open/scenario.json +6 -0
  8. package/dist/conformance/host/02-run-manual-grant-accepted/expected.json +16 -0
  9. package/dist/conformance/host/02-run-manual-grant-accepted/note.mk.md +5 -0
  10. package/dist/conformance/host/02-run-manual-grant-accepted/scenario.json +14 -0
  11. package/dist/conformance/host/03-run-manual-grant-refused/expected.json +18 -0
  12. package/dist/conformance/host/03-run-manual-grant-refused/note.mk.md +5 -0
  13. package/dist/conformance/host/03-run-manual-grant-refused/scenario.json +14 -0
  14. package/dist/conformance/host/04-run-auto-write-denied/expected.json +16 -0
  15. package/dist/conformance/host/04-run-auto-write-denied/note.mk.md +10 -0
  16. package/dist/conformance/host/04-run-auto-write-denied/scenario.json +13 -0
  17. package/dist/conformance/host/05-export-html-pack-directive/expected.json +14 -0
  18. package/dist/conformance/host/05-export-html-pack-directive/note.mk.md +8 -0
  19. package/dist/conformance/host/05-export-html-pack-directive/scenario.json +6 -0
  20. package/dist/conformance/host/06-install-pack-then-render/expected.json +18 -0
  21. package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/pack.json +13 -0
  22. package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/webview.js +1 -0
  23. package/dist/conformance/host/06-install-pack-then-render/note.mk.md +5 -0
  24. package/dist/conformance/host/06-install-pack-then-render/scenario.json +15 -0
  25. package/dist/conformance/host/07-complete-in-directive/expected.json +14 -0
  26. package/dist/conformance/host/07-complete-in-directive/note.mk.md +3 -0
  27. package/dist/conformance/host/07-complete-in-directive/scenario.json +6 -0
  28. package/dist/conformance/host/08-hover-standard-component/expected.json +5 -0
  29. package/dist/conformance/host/08-hover-standard-component/note.mk.md +5 -0
  30. package/dist/conformance/host/08-hover-standard-component/scenario.json +6 -0
  31. package/dist/conformance/host/09-insert-skeleton/expected.json +5 -0
  32. package/dist/conformance/host/09-insert-skeleton/note.mk.md +4 -0
  33. package/dist/conformance/host/09-insert-skeleton/scenario.json +6 -0
  34. package/dist/conformance/host/10-scripts-disabled/expected.json +11 -0
  35. package/dist/conformance/host/10-scripts-disabled/note.mk.md +5 -0
  36. package/dist/conformance/host/10-scripts-disabled/scenario.json +9 -0
  37. package/dist/conformance/host/11-complete-closing-fence/expected.json +5 -0
  38. package/dist/conformance/host/11-complete-closing-fence/note.mk.md +5 -0
  39. package/dist/conformance/host/11-complete-closing-fence/scenario.json +12 -0
  40. package/dist/conformance/host/12-complete-nested-container/expected.json +5 -0
  41. package/dist/conformance/host/12-complete-nested-container/note.mk.md +5 -0
  42. package/dist/conformance/host/12-complete-nested-container/scenario.json +12 -0
  43. package/dist/conformance/host/13-install-pack-declined/expected.json +12 -0
  44. package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/pack.json +13 -0
  45. package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/webview.js +1 -0
  46. package/dist/conformance/host/13-install-pack-declined/note.mk.md +5 -0
  47. package/dist/conformance/host/13-install-pack-declined/scenario.json +10 -0
  48. package/dist/conformance/host/14-remove-pack/expected.json +14 -0
  49. package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/pack.json +13 -0
  50. package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/webview.js +1 -0
  51. package/dist/conformance/host/14-remove-pack/note.mk.md +5 -0
  52. package/dist/conformance/host/14-remove-pack/scenario.json +11 -0
  53. package/dist/conformance/host/15-load-packs-engine-views/expected.json +19 -0
  54. package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/pack.json +9 -0
  55. package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/webview.js +1 -0
  56. package/dist/conformance/host/15-load-packs-engine-views/note.mk.md +9 -0
  57. package/dist/conformance/host/15-load-packs-engine-views/scenario.json +10 -0
  58. package/dist/conformance/host/16-pack-trust/expected.json +37 -0
  59. package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/pack.json +12 -0
  60. package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/webview.js +1 -0
  61. package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/pack.json +12 -0
  62. package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/webview.js +1 -0
  63. package/dist/conformance/host/16-pack-trust/note.mk.md +5 -0
  64. package/dist/conformance/host/16-pack-trust/scenario.json +26 -0
  65. package/dist/conformance/host/README.md +221 -0
  66. package/dist/conformance.d.ts +2 -0
  67. package/dist/conformance.js +5 -0
  68. package/dist/diagnostics/render-diagnostics.d.ts +41 -0
  69. package/dist/diagnostics/render-diagnostics.js +46 -0
  70. package/dist/export/cascade.d.ts +98 -0
  71. package/dist/export/cascade.js +159 -0
  72. package/dist/export/export-zip.d.ts +19 -0
  73. package/dist/export/export-zip.js +38 -0
  74. package/dist/export/image-embed.d.ts +126 -0
  75. package/dist/export/image-embed.js +274 -0
  76. package/dist/export/note-export.d.ts +332 -0
  77. package/dist/export/note-export.js +366 -0
  78. package/dist/export/note-links.d.ts +83 -0
  79. package/dist/export/note-links.js +241 -0
  80. package/dist/glue.wasm +0 -0
  81. package/dist/host/adapter.d.ts +218 -0
  82. package/dist/host/adapter.js +9 -0
  83. package/dist/host/cascade-export-behavior.d.ts +120 -0
  84. package/dist/host/cascade-export-behavior.js +151 -0
  85. package/dist/host/conformance-runner.d.ts +159 -0
  86. package/dist/host/conformance-runner.js +632 -0
  87. package/dist/host/create-host.d.ts +251 -0
  88. package/dist/host/create-host.js +329 -0
  89. package/dist/host/editor-behavior.d.ts +153 -0
  90. package/dist/host/editor-behavior.js +223 -0
  91. package/dist/host/export/md-plain.d.ts +15 -0
  92. package/dist/host/export/md-plain.js +125 -0
  93. package/dist/host/export-behavior.d.ts +111 -0
  94. package/dist/host/export-behavior.js +257 -0
  95. package/dist/host/labels.d.ts +23 -0
  96. package/dist/host/labels.js +32 -0
  97. package/dist/host/pack-archive.d.ts +30 -0
  98. package/dist/host/pack-archive.js +62 -0
  99. package/dist/host/pack-install.d.ts +87 -0
  100. package/dist/host/pack-install.js +186 -0
  101. package/dist/host/pack-load.d.ts +71 -0
  102. package/dist/host/pack-load.js +57 -0
  103. package/dist/host/pack-remove.d.ts +21 -0
  104. package/dist/host/pack-remove.js +65 -0
  105. package/dist/host/pack-trust-behavior.d.ts +79 -0
  106. package/dist/host/pack-trust-behavior.js +262 -0
  107. package/dist/host/refresh-interval.d.ts +37 -0
  108. package/dist/host/refresh-interval.js +59 -0
  109. package/dist/host/run-behavior.d.ts +88 -0
  110. package/dist/host/run-behavior.js +189 -0
  111. package/dist/host/run-diagnostics.d.ts +26 -0
  112. package/dist/host/run-diagnostics.js +43 -0
  113. package/dist/host/script-execution.d.ts +72 -0
  114. package/dist/host/script-execution.js +68 -0
  115. package/dist/host/terminal-packs.d.ts +39 -0
  116. package/dist/host/terminal-packs.js +51 -0
  117. package/dist/html-engine.d.ts +2 -0
  118. package/dist/html-engine.js +29 -0
  119. package/dist/index.d.ts +95 -0
  120. package/dist/index.js +69 -0
  121. package/dist/insert/component-catalog.d.ts +23 -0
  122. package/dist/insert/component-catalog.js +96 -0
  123. package/dist/node-fs.d.ts +14 -0
  124. package/dist/node-fs.js +70 -0
  125. package/dist/node-worker.js +20657 -0
  126. package/dist/node.d.ts +6 -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 +79 -0
  132. package/dist/packs/pack-diagnostics.js +103 -0
  133. package/dist/packs/pack-paths.d.ts +53 -0
  134. package/dist/packs/pack-paths.js +105 -0
  135. package/dist/packs/pack-render-registry.d.ts +82 -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 +85 -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 +59 -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 +72 -0
  166. package/dist/run/net-provider.js +303 -0
  167. package/dist/run/node-isolate.d.ts +22 -0
  168. package/dist/run/node-isolate.js +68 -0
  169. package/dist/run/run-flow.d.ts +186 -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 +62 -0
  186. package/dist/run/worker-entry-browser.js +172 -0
  187. package/dist/run/worker-entry.d.ts +1 -0
  188. package/dist/run/worker-entry.js +98 -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 +80 -4
@@ -0,0 +1,279 @@
1
+ /**
2
+ * The engine-neutral half of a run: everything between "a job arrived" and
3
+ * "here is its result", with nothing in it that binds to one JavaScript
4
+ * runtime.
5
+ *
6
+ * This exists because the two hosts cannot spawn the same kind of isolate
7
+ * (see `./isolate.ts`). The VS Code extension runs a `node:worker_threads`
8
+ * worker; an Obsidian plugin, living in an Electron renderer, can only run
9
+ * a Web Worker, where `node:http`, `node:dns`, and `Buffer` do not exist.
10
+ * Rather than keep two copies of the run logic in step with each other,
11
+ * the parts that genuinely differ are injected: how a network request is
12
+ * made, and where wasmoon's `glue.wasm` is found.
13
+ *
14
+ * `./worker-entry.ts` supplies the Node ones (a pinned `node:https`
15
+ * request, `glue.wasm` beside the bundle on disk).
16
+ * `./worker-entry-browser.ts` supplies the Web Worker ones (a request
17
+ * bridged to the host over `postMessage`, `glue.wasm` as a blob URL).
18
+ * Everything below runs identically under both, which is the point: the
19
+ * sandbox, the tier gate, the marshaling limits, and the failure
20
+ * classification cannot drift between hosts, because there is only one of
21
+ * each.
22
+ */
23
+ import { extractScripts, parse } from '@markii/core';
24
+ import { buildDirectiveListing, createValueStore, runDocumentScripts, } from '@markii/runtime';
25
+ import { createLuaExecutor, DEFAULT_MAX_FETCH_BYTES, } from '@markii/lua';
26
+ import { createScriptView } from '@markii/bundle';
27
+ import { cacheFilesFrom } from './bundle-run.js';
28
+ import { createSnapshotStorage } from './snapshot-storage.js';
29
+ import { createPackModuleResolver } from './lua-resolver.js';
30
+ export { isRunProgress } from './run-progress.js';
31
+ /**
32
+ * A policy denial this module's `NetProvider` raises (an ungranted host, a
33
+ * redirect off the allowlist, too many redirects, an over-size response, a
34
+ * credential-bearing redirect target, a pinning refusal from
35
+ * `./net-pinning.ts`). This is NOT the same kind of denial as
36
+ * `@markii/lua`'s own `netGrants` check inside `buildCapabilities` (which
37
+ * records on its own out-of-band `CapabilityDenials` handle): a denial
38
+ * detected HERE happens inside this provider's own `get`/`post`/`patch`, one
39
+ * level below that check. `netProviderDenial` (`@markii/lua`) brands the
40
+ * thrown `Error` with a JS `Symbol`; `@markii/lua` checks that brand on the
41
+ * JS side of the provider call, BEFORE the error crosses into Lua, records
42
+ * the denial on the same non-spoofable handle its own grant check uses, and
43
+ * re-throws a sanitized capability error. So a provider policy refusal comes
44
+ * back as `'capability-denied'` with no help from `runJob`, and no
45
+ * classification signal ever rides in a Lua-visible string.
46
+ *
47
+ * P2-c fix (docs/archive/PENTEST-REPORT-2026-08-23.md §9.3): the previous approach put a
48
+ * per-run random tag INSIDE the thrown message so `runJob` could reclassify
49
+ * the failure by scanning that message. But the message crosses back into
50
+ * Lua, where a script's own `pcall`/`tostring` reads the tag and then forges
51
+ * it into `error(tag .. ": ...")` to relabel an unrelated failure as
52
+ * capability-denied. The brand lives on the JS `Error` object and is consumed
53
+ * before the Lua boundary, so there is nothing for a script to observe or
54
+ * forge, and `runJob` no longer post-processes failure kinds at all.
55
+ */
56
+ /**
57
+ * `hostname` with IPv6 brackets stripped, for the `hostname` option
58
+ * `http.request`/`https.request` take: Node re-adds the brackets itself
59
+ * (for the `Host` header and TLS SNI) whenever the raw address contains a
60
+ * `:`, so passing an already-bracketed literal through would double them.
61
+ */
62
+ /** A `CacheProvider` over a plain snapshot object, with the writes it accumulates readable back out. */
63
+ function createSnapshotCacheProvider(snapshot) {
64
+ const store = { ...snapshot };
65
+ return {
66
+ provider: {
67
+ get: (key) => Promise.resolve(store[key]),
68
+ set: (key, entry) => {
69
+ store[key] = entry;
70
+ return Promise.resolve();
71
+ },
72
+ },
73
+ snapshot: () => ({ ...store }),
74
+ };
75
+ }
76
+ function describeThrown(err) {
77
+ return err instanceof Error ? err.message : String(err);
78
+ }
79
+ /** Runs one job to completion, never throwing — see this module's top doc comment. */
80
+ export async function runJob(job, deps) {
81
+ const cache = createSnapshotCacheProvider(job.cacheSnapshot ?? {});
82
+ const netAllowlist = job.netAllowlist ?? [];
83
+ // The SAME cap governs this worker's own bounded body read
84
+ // (`issueRequest`, B-2) and `@markii/lua`'s own `maxFetchBytes` check —
85
+ // computed once, here, rather than letting each side default
86
+ // independently, so the two can never quietly disagree.
87
+ const maxFetchBytes = job.limits?.maxFetchBytes ?? DEFAULT_MAX_FETCH_BYTES;
88
+ // GitHub issue #10, fail closed: an absent `netPolicy` (an older host, or
89
+ // a caller that hasn't been updated) means restricted addresses are NOT
90
+ // allowed — the same posture `@markii/lua`'s own defaults take, never the
91
+ // opposite.
92
+ const netPolicy = job.netPolicy ?? {
93
+ allowRestrictedAddresses: false,
94
+ };
95
+ const net = deps.createNet(netAllowlist, maxFetchBytes, netPolicy);
96
+ const tree = parse(job.text);
97
+ const scripts = extractScripts(tree);
98
+ // GitHub issue #33: the note's own directives, as capped plain data for
99
+ // the `doc` table a script sees. Built from the tree ALREADY parsed on
100
+ // the line above, so a run still parses the note exactly once, and built
101
+ // HERE rather than in either host so both isolates (the Node worker and
102
+ // the Web Worker) get a listing produced by one piece of code.
103
+ const directives = buildDirectiveListing(tree);
104
+ // Slice 2 of the .mkz Run-path arc (GitHub issue #9): a bundle-backed run
105
+ // gets a snapshot-backed ScriptView (never a live zip/disk handle — see
106
+ // `./snapshot-storage.ts`), scoped to exactly the intersection
107
+ // `createScriptView` (@markii/bundle) computes between the manifest's
108
+ // declared `permissions.bundle` and what the host's grant flow actually
109
+ // granted.
110
+ const bundleStorage = job.bundle
111
+ ? createSnapshotStorage(job.bundle.snapshot)
112
+ : undefined;
113
+ const bundleView = bundleStorage && job.bundle
114
+ ? createScriptView(bundleStorage, job.bundle.manifest, {
115
+ bundle: job.bundle.grantedBundlePermissions,
116
+ })
117
+ : undefined;
118
+ const executor = createLuaExecutor({
119
+ net,
120
+ // One allowlist governs GET/POST/PATCH alike (B-6, docs/security.md:
121
+ // "the per-host allowlist is the real boundary") — the grant prompt's
122
+ // wording ("can send data to <host>") already promises exactly this,
123
+ // so POST/PATCH must be wired to the same hosts GET is, not silently
124
+ // disabled.
125
+ netGrants: { get: netAllowlist, post: netAllowlist },
126
+ cache: cache.provider,
127
+ bundle: bundleView,
128
+ ...(job.packModules
129
+ ? { packModuleResolver: createPackModuleResolver(job.packModules) }
130
+ : {}),
131
+ maxFetchBytes,
132
+ limits: {
133
+ ...(job.limits?.wallClockMs !== undefined
134
+ ? { wallClockMs: job.limits.wallClockMs }
135
+ : {}),
136
+ ...(job.limits?.maxInstructions !== undefined
137
+ ? { maxInstructions: job.limits.maxInstructions }
138
+ : {}),
139
+ ...(job.limits?.maxMemoryBytes !== undefined
140
+ ? { maxMemoryBytes: job.limits.maxMemoryBytes }
141
+ : {}),
142
+ },
143
+ wasmUri: deps.wasmUri,
144
+ });
145
+ const store = createValueStore();
146
+ // GitHub issue #35: the ordinal is assigned HERE rather than by either
147
+ // worker entry, so the two hosts cannot number their runs differently.
148
+ let progressIndex = 0;
149
+ const postProgress = deps.postProgress;
150
+ const summary = await runDocumentScripts({
151
+ scripts,
152
+ executor,
153
+ // Announced per script, in document order, as each value is stored —
154
+ // `runDocumentScripts` swallows anything this throws, so a failing
155
+ // channel costs the run nothing.
156
+ ...(postProgress
157
+ ? {
158
+ onValue: (name, value) => {
159
+ postProgress({
160
+ kind: 'markii:run-progress',
161
+ index: progressIndex++,
162
+ name,
163
+ value,
164
+ });
165
+ },
166
+ }
167
+ : {}),
168
+ // GitHub issue #11: the trigger the host sent (default `'manual'`) — its
169
+ // mapping to a capability tier (`tierForTrigger`) is the sandbox's own
170
+ // read-only gate for auto/scheduled runs. Never note-influenced.
171
+ trigger: job.trigger ?? 'manual',
172
+ store,
173
+ // Read-only and tier-free: the listing is the note's own content, so
174
+ // it is handed to an auto/scheduled run exactly as to a manual one.
175
+ doc: { directives },
176
+ // `src=scripts/foo.lua` resolution (design point 4): the referenced
177
+ // file's source lives in the snapshot under its own bundle-relative
178
+ // path (`ScriptBlock.src` already carries the full "scripts/..." path
179
+ // — see @markii/core's `scripts.ts`), read through the SAME jailed
180
+ // snapshot storage a script's own `bundle.read` would use. A bare
181
+ // `.mk.md` run (no `job.bundle`) never sets `loadSource` at all, so a
182
+ // `src=` block there is reported as the pre-existing "no loadSource
183
+ // provided" error, unchanged.
184
+ ...(bundleStorage
185
+ ? {
186
+ loadSource: async (src) => {
187
+ const bytes = await bundleStorage.read(src);
188
+ if (bytes === undefined) {
189
+ throw new Error(`bundle script "${src}" was not found`);
190
+ }
191
+ return new TextDecoder().decode(bytes);
192
+ },
193
+ }
194
+ : {}),
195
+ });
196
+ // A net-provider policy denial already arrives here classified as
197
+ // 'capability-denied': `@markii/lua` records it on its non-spoofable
198
+ // `CapabilityDenials` handle when the branded `netProviderDenial` crosses
199
+ // its provider-call boundary (see the `NetProvider` doc comment above,
200
+ // P2-c). So `runJob` no longer post-processes any failure kind — it reads
201
+ // `entry.failureKind` verbatim, and the value store already carries the
202
+ // same kind for the corresponding `StoredValue`.
203
+ const failures = summary.results
204
+ .filter((entry) => entry.status === 'error')
205
+ .map((entry) => ({
206
+ name: entry.name,
207
+ message: entry.error ?? 'script failed',
208
+ kind: entry.failureKind ?? 'script-error',
209
+ }));
210
+ // Copied entry-by-entry onto a fresh plain object rather than returned as
211
+ // `store.snapshot()` directly: this preserves the exact N-11 pinned shape
212
+ // (a script literally named `__proto__` assigns through and leaves no own
213
+ // key), which a direct structured-clone of the store's own object would
214
+ // not. The values are otherwise passed through verbatim — failure kinds
215
+ // arrive already correct (see the failures comment above).
216
+ const values = {};
217
+ for (const [name, entry] of Object.entries(store.snapshot())) {
218
+ values[name] = entry;
219
+ }
220
+ return {
221
+ values,
222
+ failures,
223
+ cacheSnapshot: cache.snapshot(),
224
+ ...(bundleStorage
225
+ ? { cacheOut: cacheFilesFrom(bundleStorage.currentFiles()) }
226
+ : {}),
227
+ };
228
+ }
229
+ /** Turns any unexpected internal failure into an ordinary result — see the top doc comment's "never an unhandled rejection" guarantee. */
230
+ export function resultForInternalError(err, fallbackCacheSnapshot) {
231
+ return {
232
+ values: {},
233
+ failures: [
234
+ {
235
+ name: '<document>',
236
+ message: describeThrown(err),
237
+ kind: 'script-error',
238
+ },
239
+ ],
240
+ cacheSnapshot: fallbackCacheSnapshot,
241
+ };
242
+ }
243
+ const RUN_TRIGGERS = new Set([
244
+ 'manual',
245
+ 'auto',
246
+ 'scheduled',
247
+ ]);
248
+ export function isRunJob(value) {
249
+ if (typeof value !== 'object' || value === null)
250
+ return false;
251
+ const job = value;
252
+ // `trigger` is optional, but a PRESENT one must be a known trigger — a
253
+ // malformed job is rejected outright (fail-closed) rather than silently
254
+ // coerced, so a bad `trigger` can never quietly fall through to the
255
+ // full-capability `'manual'` default. Only the trusted host sets this, but
256
+ // validating it keeps the tier gate's input honest regardless.
257
+ if (Object.prototype.hasOwnProperty.call(job, 'trigger') &&
258
+ job.trigger !== undefined &&
259
+ (typeof job.trigger !== 'string' || !RUN_TRIGGERS.has(job.trigger))) {
260
+ return false;
261
+ }
262
+ // Same fail-closed shape check for `netPolicy` (GitHub issue #10): a
263
+ // PRESENT but malformed policy is rejected outright rather than silently
264
+ // falling through to the safe default, keeping the pinning gate's input
265
+ // as honest as the tier gate's above.
266
+ if (Object.prototype.hasOwnProperty.call(job, 'netPolicy') &&
267
+ job.netPolicy !== undefined &&
268
+ (typeof job.netPolicy !== 'object' ||
269
+ job.netPolicy === null ||
270
+ typeof job.netPolicy
271
+ .allowRestrictedAddresses !== 'boolean')) {
272
+ return false;
273
+ }
274
+ return (typeof job.text === 'string' &&
275
+ Array.isArray(job.netAllowlist) &&
276
+ job.netAllowlist.every((h) => typeof h === 'string') &&
277
+ typeof job.cacheSnapshot === 'object' &&
278
+ job.cacheSnapshot !== null);
279
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The per-script progress message (GitHub issue #35): its shape and its
3
+ * hostile-shape guard, and nothing else.
4
+ *
5
+ * WHY IT IS ITS OWN MODULE. The message is BUILT inside the isolate
6
+ * (`./run-job.ts`, so both worker entries emit an identical one) and READ
7
+ * on the host (`./run-host.ts`, which tells progress from the run's single
8
+ * result message). `./run-job.ts` imports `@markii/lua`, and that import
9
+ * pulls a whole WebAssembly Lua engine in behind it — a host bundle must
10
+ * never carry one, least of all the Obsidian plugin's, which runs in an
11
+ * Electron renderer and delegates every Lua execution to a Web Worker. So
12
+ * the protocol lives apart from the code that speaks it, exactly as
13
+ * `./net-bridge.ts` does for the net bridge.
14
+ *
15
+ * Nothing here imports anything but a type.
16
+ */
17
+ import type { StoredValue } from '@markii/runtime';
18
+ /**
19
+ * Worker -> host, one per script, sent the moment that script's value was
20
+ * written to the run's store and always BEFORE the single `RunResult`
21
+ * message that settles the run. Scripts run sequentially in one isolate,
22
+ * so the last script's value can be many seconds younger than the first's;
23
+ * without this the preview shows every value as stale until the whole
24
+ * batch is done.
25
+ *
26
+ * `index` is this message's ordinal within the run, counted from 0 in
27
+ * document order. The host uses it to drop a duplicate or out-of-order
28
+ * message rather than trusting arrival order alone — the channel preserves
29
+ * order, but the isolate runs untrusted script content, so nothing coming
30
+ * out of it is taken on trust.
31
+ *
32
+ * Every field is structured-clone-safe, exactly like `RunResult`: `value`
33
+ * is the same already-marshaled `StoredValue` that ends up in
34
+ * `RunResult.values`.
35
+ */
36
+ export interface RunProgress {
37
+ kind: 'markii:run-progress';
38
+ index: number;
39
+ name: string;
40
+ value: StoredValue;
41
+ }
42
+ /**
43
+ * Hostile-shape guard for a `RunProgress` message, applied by
44
+ * `./run-host.ts` to every message an isolate sends. The isolate runs
45
+ * untrusted script content, so a message claiming to be progress is
46
+ * validated the same way `./run-job.ts`'s `isRunJob` validates a job:
47
+ * every field checked, own properties only, no coercion, and `value`
48
+ * reduced to a plain object carrying one of `@markii/runtime`'s four
49
+ * closed statuses.
50
+ *
51
+ * A message that fails this is NOT progress — the caller then treats it as
52
+ * the run's final result, where `RunResult`'s own consumers already handle
53
+ * a malformed shape.
54
+ */
55
+ export declare function isRunProgress(value: unknown): value is RunProgress;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Hostile-shape guard for a `RunProgress` message, applied by
3
+ * `./run-host.ts` to every message an isolate sends. The isolate runs
4
+ * untrusted script content, so a message claiming to be progress is
5
+ * validated the same way `./run-job.ts`'s `isRunJob` validates a job:
6
+ * every field checked, own properties only, no coercion, and `value`
7
+ * reduced to a plain object carrying one of `@markii/runtime`'s four
8
+ * closed statuses.
9
+ *
10
+ * A message that fails this is NOT progress — the caller then treats it as
11
+ * the run's final result, where `RunResult`'s own consumers already handle
12
+ * a malformed shape.
13
+ */
14
+ export function isRunProgress(value) {
15
+ if (typeof value !== 'object' || value === null)
16
+ return false;
17
+ const message = value;
18
+ // Own properties only, the same discipline `apps/vscode/src/protocol.ts`
19
+ // applies to every wire message: an object that merely INHERITS `kind`
20
+ // from its prototype is not a progress message.
21
+ const own = (key) => Object.prototype.hasOwnProperty.call(message, key);
22
+ if (!own('kind') || message.kind !== 'markii:run-progress')
23
+ return false;
24
+ if (!own('index') || !own('name') || !own('value'))
25
+ return false;
26
+ if (typeof message.index !== 'number' ||
27
+ !Number.isInteger(message.index) ||
28
+ message.index < 0) {
29
+ return false;
30
+ }
31
+ if (typeof message.name !== 'string')
32
+ return false;
33
+ const stored = message.value;
34
+ if (typeof stored !== 'object' || stored === null || Array.isArray(stored)) {
35
+ return false;
36
+ }
37
+ const status = stored.status;
38
+ return (status === 'fresh' ||
39
+ status === 'stale' ||
40
+ status === 'error' ||
41
+ status === 'missing');
42
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * ITEM 3 (AGENTS.md "clean is not silent"): a `'scheduled'`/`'auto'` run
3
+ * that produces identical values is otherwise indistinguishable from one
4
+ * that never happened — no value changed, so nothing in the rendered note
5
+ * moves. This module records the last run's OUTCOME (when it ran, whether
6
+ * it succeeded, and why not if it failed) per document, independent of
7
+ * whatever values it produced, and reads it back for `preview-panel.ts`'s
8
+ * `postUpdate` to surface as a quiet marker — the same "last-known state,
9
+ * persisted, rehydrated on reopen" design `./stale-values.ts` and
10
+ * `./run-flow.ts`'s value persistence already use.
11
+ *
12
+ * `vscode`-free: takes a `GrantMemento`-shaped store (the same minimal
13
+ * `get`/`update` interface `./grant-flow.ts` already defines, which a real
14
+ * `vscode.Memento` and a plain in-memory fake both satisfy), so this stays
15
+ * plain TypeScript vitest can run directly.
16
+ */
17
+ import type { RunTrigger } from '@markii/runtime';
18
+ import type { GrantMemento, Thenable } from './grant-flow.js';
19
+ /**
20
+ * One run's recorded outcome. `reason` is present only when `ok` is
21
+ * `false`, and is a short, human phrase (never a raw stack or the full
22
+ * caught error) — the same "quiet marker + tooltip, never an error dump"
23
+ * posture every other failure surface in this codebase follows.
24
+ */
25
+ export interface RunTrace {
26
+ readonly trigger: RunTrigger;
27
+ /** Epoch milliseconds this run completed (or failed) at. */
28
+ readonly ranAt: number;
29
+ readonly ok: boolean;
30
+ readonly reason?: string;
31
+ }
32
+ /** The `workspaceState`/Memento key a document's last-run trace lives under, mirroring `./run-flow.ts`'s `valuesStorageKeyFor`/`cacheStorageKeyFor` naming. */
33
+ export declare function lastRunStorageKeyFor(documentKey: string): string;
34
+ /**
35
+ * A plausible `RunTrace` shape read back from storage — never trusted
36
+ * further than this, matching `./run-flow.ts`'s `isStoredValueRecord`'s own
37
+ * fail-safe posture: a corrupt or foreign persisted value degrades to "no
38
+ * trace", not a thrown error.
39
+ */
40
+ export declare function isRunTrace(value: unknown): value is RunTrace;
41
+ /** Reads a document's persisted last-run trace, or `undefined` when there is none / it is unusable. Never throws. */
42
+ export declare function readLastRunTrace(memento: GrantMemento, documentKey: string): RunTrace | undefined;
43
+ /** Persists `trace` as `documentKey`'s last-run outcome, superseding whatever was there before. */
44
+ export declare function writeLastRunTrace(memento: GrantMemento, documentKey: string, trace: RunTrace): Thenable<void>;
@@ -0,0 +1,36 @@
1
+ /** The `workspaceState`/Memento key a document's last-run trace lives under, mirroring `./run-flow.ts`'s `valuesStorageKeyFor`/`cacheStorageKeyFor` naming. */
2
+ export function lastRunStorageKeyFor(documentKey) {
3
+ return `markii.lastRun:${documentKey}`;
4
+ }
5
+ function isValidTrigger(value) {
6
+ return value === 'manual' || value === 'auto' || value === 'scheduled';
7
+ }
8
+ /**
9
+ * A plausible `RunTrace` shape read back from storage — never trusted
10
+ * further than this, matching `./run-flow.ts`'s `isStoredValueRecord`'s own
11
+ * fail-safe posture: a corrupt or foreign persisted value degrades to "no
12
+ * trace", not a thrown error.
13
+ */
14
+ export function isRunTrace(value) {
15
+ if (typeof value !== 'object' || value === null)
16
+ return false;
17
+ const v = value;
18
+ if (!isValidTrigger(v.trigger))
19
+ return false;
20
+ if (typeof v.ranAt !== 'number' || !Number.isFinite(v.ranAt))
21
+ return false;
22
+ if (typeof v.ok !== 'boolean')
23
+ return false;
24
+ if (v.reason !== undefined && typeof v.reason !== 'string')
25
+ return false;
26
+ return true;
27
+ }
28
+ /** Reads a document's persisted last-run trace, or `undefined` when there is none / it is unusable. Never throws. */
29
+ export function readLastRunTrace(memento, documentKey) {
30
+ const raw = memento.get(lastRunStorageKeyFor(documentKey));
31
+ return isRunTrace(raw) ? raw : undefined;
32
+ }
33
+ /** Persists `trace` as `documentKey`'s last-run outcome, superseding whatever was there before. */
34
+ export function writeLastRunTrace(memento, documentKey, trace) {
35
+ return memento.update(lastRunStorageKeyFor(documentKey), trace);
36
+ }
@@ -0,0 +1,98 @@
1
+ import { type ScriptBlock } from '@markii/core';
2
+ import type { GrantClosureScript } from '@markii/runtime';
3
+ /** One document's static run requirements. */
4
+ export interface RunRequirements {
5
+ /** Every script block found, in document order — `@markii/core`'s `extractScripts` result, unmodified. */
6
+ scripts: ScriptBlock[];
7
+ /**
8
+ * Hostnames this run's scripts are statically known to reach via
9
+ * `net.*`, deduplicated, in first-seen (document) order. Does NOT
10
+ * include hosts that could only be discovered by actually running the
11
+ * script (see `hasUnknownHosts`).
12
+ */
13
+ hosts: string[];
14
+ /**
15
+ * `true` when at least one `net.fetch_json`/`net.post`/`net.patch` call
16
+ * site's URL argument could not be resolved to a literal (a
17
+ * concatenation, a variable, any non-literal expression), OR when any
18
+ * script block is a `src=` reference (its actual Lua source lives in a
19
+ * bundle file this function never reads, so its `net.*` usage — if any —
20
+ * is entirely unknown). The caller should read this as "prompt for
21
+ * network access more broadly" rather than trusting `hosts` alone to be
22
+ * the complete set.
23
+ */
24
+ hasUnknownHosts: boolean;
25
+ /**
26
+ * `scripts` reshaped into `@markii/runtime`'s `GrantClosureScript` — the
27
+ * exact per-script record `computeGrantKey`'s `GrantClosure.scripts`
28
+ * field expects (`{ name, lang, src, code }`). `computeGrantKey` itself
29
+ * sorts records by their own encoded bytes before hashing, so array
30
+ * order here does not affect the resulting key — this is already in
31
+ * document order (from `extractScripts`), which is what makes repeated
32
+ * extraction over the same text byte-identical, satisfying the "stable
33
+ * order" requirement without this module needing to reimplement any of
34
+ * `computeGrantKey`'s own sorting.
35
+ */
36
+ grantScripts: GrantClosureScript[];
37
+ }
38
+ /**
39
+ * Bare, already-lowercased hostnames every script block in `scripts`
40
+ * declares via its own `permissions` fence attribute
41
+ * (`@markii/core`'s `ScriptBlock.permissions`), deduplicated, in
42
+ * first-seen (document, then per-script list) order. Mirrors
43
+ * `./bundle-run.ts`'s `manifestNetHosts` for the bundle-manifest
44
+ * equivalent: this is DECLARED INTENT only, exactly like a manifest's
45
+ * `permissions.net` — `scanRunRequirementHosts` above stays the ONLY
46
+ * source of the hostnames a run is actually prompted for. A caller
47
+ * compares this against the scanned set with
48
+ * `./bundle-run.ts`'s `netDeclarationDiagnostics` (the same
49
+ * declared-vs-scanned diff a bundle manifest already gets), and may show
50
+ * it next to the scanned hosts in a grant prompt as "declares: ...".
51
+ */
52
+ export declare function scriptDeclaredHosts(scripts: readonly ScriptBlock[]): string[];
53
+ /**
54
+ * Parses `text` and returns its script blocks, in both the raw
55
+ * `@markii/core` `ScriptBlock` shape and the `GrantClosureScript` shape
56
+ * `computeGrantKey` hashes. Split out from `extractRunRequirements` (below)
57
+ * so a caller that needs to resolve `src=` script content BEFORE scanning
58
+ * for hosts — a bundle run, which must read the referenced file out of its
59
+ * snapshot first — can do so without parsing the document twice.
60
+ */
61
+ export declare function extractScriptBlocks(text: string): {
62
+ scripts: ScriptBlock[];
63
+ grantScripts: GrantClosureScript[];
64
+ };
65
+ /**
66
+ * Resolves a `src=` block's actual Lua source, when the caller has one —
67
+ * for a bundle-backed run, this is the file's content out of the run's
68
+ * snapshot (`./bundle-run.ts`'s `bundleModulesFromSnapshot`). Returning
69
+ * `undefined` for a block (path missing from the snapshot, or a plain
70
+ * `.mk.md` document with no bundle to resolve from at all) leaves that
71
+ * block's hosts unknown, exactly as before this seam existed.
72
+ */
73
+ export type ResolveScriptSource = (block: ScriptBlock) => string | undefined;
74
+ /**
75
+ * Scans `scripts` for statically-resolvable `net.*` hosts — an INLINE
76
+ * block's own `code`, or (when `resolveScriptSource` is given and resolves
77
+ * it) a `src=` block's referenced source. A `src=` block with no resolution
78
+ * available folds into `hasUnknownHosts`, same as this module has always
79
+ * done for a bare `.mk.md` document's `src=` references (their source
80
+ * genuinely isn't in `text` at all).
81
+ */
82
+ export declare function scanRunRequirementHosts(scripts: readonly ScriptBlock[], resolveScriptSource?: ResolveScriptSource): {
83
+ hosts: string[];
84
+ hasUnknownHosts: boolean;
85
+ };
86
+ /**
87
+ * Computes `RunRequirements` for `text` — parses it with `@markii/core`'s
88
+ * `parse`, pulls its script blocks with `extractScripts`, and scans each
89
+ * INLINE block's own source for statically-resolvable `net.*` hosts. Pure:
90
+ * no I/O, no `vscode`, safe to call on every keystroke if a future slice
91
+ * wants a live preview of what a run would ask for.
92
+ *
93
+ * `resolveScriptSource`, when given, lets a `src=` block's hosts be
94
+ * resolved too (a bundle run passes one backed by its snapshot); omitted,
95
+ * every `src=` block folds into `hasUnknownHosts` exactly as before this
96
+ * option existed.
97
+ */
98
+ export declare function extractRunRequirements(text: string, resolveScriptSource?: ResolveScriptSource): RunRequirements;