@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,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Node half of the Run path's isolate seam: a `node:worker_threads`
|
|
3
|
+
* worker, plus the dev/Vitest worker-entry fallback. Split out of
|
|
4
|
+
* `./run-host.ts` so the watchdog, the exactly-once settlement and the
|
|
5
|
+
* never-rejects contract stay in a module with no `node:*` import and run
|
|
6
|
+
* unchanged for every host kind. Reached only through `@markii/host/node`.
|
|
7
|
+
*/
|
|
8
|
+
import { Worker } from 'node:worker_threads';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import { spawnRun } from './run-host.js';
|
|
12
|
+
/** `execArgv` needed to run a `.ts` worker entry directly (dev/Vitest only) — see `./worker-entry.ts`'s doc comment on why a plain `node` `.ts` import can't resolve this repo's `@markii/*` bare specifiers or its `./foo.js` -> `./foo.ts` import convention on its own. `--require tsx/cjs` (rather than `tsx/esm`) is deliberate: empirically, the ESM loader hook deadlocks Node's CJS/ESM interop translator for a worker thread requiring `.js`-suffixed relative imports that resolve to `.ts` files (this repo's convention throughout `@markii/*`), while the CJS `require` hook has no such issue. */
|
|
13
|
+
const TSX_DEV_EXEC_ARGV = ['--require', 'tsx/cjs'];
|
|
14
|
+
/**
|
|
15
|
+
* The ONLY fallback this shared package supplies for an unset `workerPath`:
|
|
16
|
+
* the sibling `worker-entry.ts`, run straight from source via `tsx`
|
|
17
|
+
* (`TSX_DEV_EXEC_ARGV`, above). That is correct for this package's own
|
|
18
|
+
* dev/Vitest runs — `__dirname` at that point is this real source
|
|
19
|
+
* directory — but it says nothing about where a packaged host's bundled
|
|
20
|
+
* worker lives, because this package does not know that host's `dist/`
|
|
21
|
+
* layout. A packaged host (e.g. `apps/vscode`) MUST resolve its own bundled
|
|
22
|
+
* worker path itself and pass it as `SpawnRunOptions.workerPath`; see
|
|
23
|
+
* `apps/vscode/src/worker-path.ts` for that host's resolution and
|
|
24
|
+
* `apps/vscode/esbuild.config.mjs`'s worker build for where it bundles to.
|
|
25
|
+
*/
|
|
26
|
+
export function defaultWorkerPath() {
|
|
27
|
+
// The built package is ESM, where `__dirname` does not exist: a packaged
|
|
28
|
+
// host always passes its own bundled worker, so say so instead of
|
|
29
|
+
// throwing a ReferenceError.
|
|
30
|
+
const devSource = typeof __dirname === 'undefined'
|
|
31
|
+
? undefined
|
|
32
|
+
: path.join(__dirname, 'worker-entry.ts');
|
|
33
|
+
if (devSource !== undefined && existsSync(devSource))
|
|
34
|
+
return devSource;
|
|
35
|
+
throw new Error('spawnRun: could not resolve the worker entry file (looked for ' +
|
|
36
|
+
`${devSource ?? 'worker-entry.ts beside the package source'}) — pass workerPath explicitly`);
|
|
37
|
+
}
|
|
38
|
+
function execArgvFor(workerPath) {
|
|
39
|
+
return workerPath.endsWith('.ts') ? TSX_DEV_EXEC_ARGV : undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Dev/Vitest only: point `tsx` at the repo's `tsconfig.dev.json` so a
|
|
43
|
+
* `.ts` worker entry resolves `@markii/*` to each package's SOURCE.
|
|
44
|
+
*
|
|
45
|
+
* Without this the worker is a plain Node resolution: a bare `@markii/lua`
|
|
46
|
+
* goes through `node_modules` to that package's `exports`, which name the
|
|
47
|
+
* built `dist/`. `npm run build` only type-checks (`tsc --noEmit`); the
|
|
48
|
+
* emitting script is `build:dist`, which runs at release. So `dist/` is
|
|
49
|
+
* whatever a past release left behind, and a real-isolate probe would
|
|
50
|
+
* silently exercise that stale code instead of the source it is written to
|
|
51
|
+
* test. A capability added since the last `build:dist` simply would not
|
|
52
|
+
* exist inside the worker.
|
|
53
|
+
*
|
|
54
|
+
* Gated on a `.ts` entry, the same signal `execArgvFor` already uses for
|
|
55
|
+
* the tsx hook, so a packaged host passing its own bundled `.js` worker is
|
|
56
|
+
* never touched by this.
|
|
57
|
+
*/
|
|
58
|
+
function devTsconfigEnv(entryPath) {
|
|
59
|
+
if (!entryPath.endsWith('.ts') || typeof __dirname === 'undefined') {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const tsconfig = path.resolve(__dirname, '../../../..', 'tsconfig.dev.json');
|
|
63
|
+
if (!existsSync(tsconfig))
|
|
64
|
+
return undefined;
|
|
65
|
+
return { env: { ...process.env, TSX_TSCONFIG_PATH: tsconfig } };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The default isolate: a `node:worker_threads` worker, which is what every
|
|
69
|
+
* Node host (the VS Code extension host, this package's own tests) uses. It
|
|
70
|
+
* is the only kind that accepts `resourceLimits`, so it is also the only
|
|
71
|
+
* kind whose V8 heap is bounded — see `./isolate.ts` for why a host might
|
|
72
|
+
* still have to supply the other one.
|
|
73
|
+
*/
|
|
74
|
+
export const workerThreadIsolate = (options) => {
|
|
75
|
+
const execArgv = options.execArgv ?? execArgvFor(options.entryPath);
|
|
76
|
+
const worker = new Worker(options.entryPath, {
|
|
77
|
+
...(execArgv ? { execArgv } : {}),
|
|
78
|
+
...(devTsconfigEnv(options.entryPath) ?? {}),
|
|
79
|
+
resourceLimits: {
|
|
80
|
+
maxOldGenerationSizeMb: options.maxOldGenerationSizeMb,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
send: (job) => {
|
|
85
|
+
worker.postMessage(job);
|
|
86
|
+
},
|
|
87
|
+
kill: () => {
|
|
88
|
+
void worker.terminate();
|
|
89
|
+
},
|
|
90
|
+
onMessage: (listener) => {
|
|
91
|
+
// `on`, not `once`: a run now sends one progress message per script
|
|
92
|
+
// (GitHub issue #35) ahead of its single result message, and
|
|
93
|
+
// `spawnRun` is what tells the two apart. Messages arriving after
|
|
94
|
+
// the run settles are harmless — `settled` guards every path — and
|
|
95
|
+
// the worker is terminated on settle anyway.
|
|
96
|
+
worker.on('message', listener);
|
|
97
|
+
},
|
|
98
|
+
onError: (listener) => {
|
|
99
|
+
worker.once('error', listener);
|
|
100
|
+
},
|
|
101
|
+
onExit: (listener) => {
|
|
102
|
+
worker.once('exit', listener);
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* `spawnRun` with the Node defaults filled in: the worker-thread isolate and
|
|
108
|
+
* the dev/Vitest worker entry. For this package's own tests and any Node
|
|
109
|
+
* caller that wants them; a packaged host passes its own `workerPath`.
|
|
110
|
+
*/
|
|
111
|
+
export function spawnRunInThread(options) {
|
|
112
|
+
return spawnRun({
|
|
113
|
+
...options,
|
|
114
|
+
workerPath: options.workerPath ?? defaultWorkerPath(),
|
|
115
|
+
spawnIsolate: options.spawnIsolate ?? workerThreadIsolate,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { GrantMemento, PromptHost, PromptManyHosts, PromptUnknownHosts } from './grant-flow.js';
|
|
2
|
+
import type { RunResult, SpawnRunRequest } from './run-host.js';
|
|
3
|
+
import type { ValuesFailure } from '../values-failure.js';
|
|
4
|
+
import type { RunTrigger, StoredValue } from '@markii/runtime';
|
|
5
|
+
import type { BundleManifest } from '@markii/bundle';
|
|
6
|
+
import type { PackModulesMap } from './lua-resolver.js';
|
|
7
|
+
/** A `CacheEntry`-keyed snapshot, structurally — this module never imports `@markii/lua` just for the type; it only ever passes the value through. */
|
|
8
|
+
export type CacheSnapshot = Record<string, unknown>;
|
|
9
|
+
/**
|
|
10
|
+
* A sane upper bound on a persisted cache snapshot: real cached `net.*`
|
|
11
|
+
* responses are small JSON payloads, so the cap exists to guarantee a
|
|
12
|
+
* runaway/adversarial script can never grow `workspaceState` without
|
|
13
|
+
* bound. A snapshot over this size is DROPPED, not truncated — a partial
|
|
14
|
+
* cache would silently look valid while missing entries a script assumed
|
|
15
|
+
* were still there, which is worse than starting the next run with none
|
|
16
|
+
* at all.
|
|
17
|
+
*/
|
|
18
|
+
export declare const MAX_CACHE_SNAPSHOT_BYTES = 1000000;
|
|
19
|
+
/** The single `workspaceState`/Memento key a document's cache snapshot lives under. */
|
|
20
|
+
export declare function cacheStorageKeyFor(documentKey: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* The `workspaceState`/Memento key a document's LAST-KNOWN values live under
|
|
23
|
+
* (GitHub issue #11, gap 1). Separate from the cache snapshot
|
|
24
|
+
* (`cacheStorageKeyFor`): the cache is the sandbox's own `cache.get` state,
|
|
25
|
+
* keyed and TTL'd inside Lua; THIS is the rendered value store
|
|
26
|
+
* (`ValueStore.snapshot()`), the numbers a data-bound component actually
|
|
27
|
+
* showed. Persisting it is what lets a reopened monitoring note render its
|
|
28
|
+
* last figures instantly and offline, marked stale, before (or without) any
|
|
29
|
+
* re-run — exactly the "opening a note ... showing its last data marked as
|
|
30
|
+
* stale" behavior docs/scripting.md and docs/spec.md require.
|
|
31
|
+
*/
|
|
32
|
+
export declare function valuesStorageKeyFor(documentKey: string): string;
|
|
33
|
+
/** Same cap and drop-not-truncate posture as `MAX_CACHE_SNAPSHOT_BYTES` — a runaway value store never grows `workspaceState` without bound, and a too-large snapshot is dropped whole (a partial value store would silently omit names a note binds). */
|
|
34
|
+
export declare const MAX_VALUES_SNAPSHOT_BYTES = 1000000;
|
|
35
|
+
/**
|
|
36
|
+
* Reads a document's persisted last-known value store, or `{}` when there is
|
|
37
|
+
* none / it is unusable. Never throws, never trusts the stored shape past
|
|
38
|
+
* "plain object" — see `isStoredValueRecord`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function readPersistedValues(memento: GrantMemento, documentKey: string): Record<string, StoredValue>;
|
|
41
|
+
/**
|
|
42
|
+
* Merges a completed run's value store onto the previously-persisted one,
|
|
43
|
+
* keeping the LAST-KNOWN-GOOD value for any name whose fresh run FAILED
|
|
44
|
+
* (GitHub issue #11). A scheduled/auto run that hits a transient failure
|
|
45
|
+
* (offline, an API hiccup, an ungranted host under the read-only tier) must
|
|
46
|
+
* not wipe the good numbers a monitoring note was already showing — mirroring
|
|
47
|
+
* the same "the cache only ever remembers good data" guarantee
|
|
48
|
+
* docs/scripting.md gives `cache.get`. For each name in the new run: a
|
|
49
|
+
* non-error outcome always wins; an error outcome is kept ONLY if there is no
|
|
50
|
+
* prior good value to fall back to (so the error surfaces on a note that
|
|
51
|
+
* never succeeded), otherwise the prior value is retained and marked stale.
|
|
52
|
+
* Names present only in the prior store (a script the note no longer has) are
|
|
53
|
+
* dropped — the note's current scripts define the namespace.
|
|
54
|
+
*/
|
|
55
|
+
export declare function mergePersistedValues(previous: Record<string, StoredValue>, next: Record<string, StoredValue>): Record<string, StoredValue>;
|
|
56
|
+
/** A plausible cache-snapshot shape read back from storage — never trusts it further than "is this a plain object at all" (an old/foreign/corrupt value degrades to "no cache", matching this whole module's fail-safe posture). */
|
|
57
|
+
export declare function isCacheSnapshotShape(value: unknown): value is CacheSnapshot;
|
|
58
|
+
/**
|
|
59
|
+
* The JSON text for `snapshot`, or `undefined` when it must be dropped
|
|
60
|
+
* instead of persisted: either it isn't serializable at all (a value
|
|
61
|
+
* `JSON.stringify` cannot handle), or its serialized size exceeds
|
|
62
|
+
* `MAX_CACHE_SNAPSHOT_BYTES`. Callers write `undefined` back to storage in
|
|
63
|
+
* that case — see `MAX_CACHE_SNAPSHOT_BYTES`'s doc comment on why a partial
|
|
64
|
+
* write is never attempted.
|
|
65
|
+
*/
|
|
66
|
+
export declare function serializeCacheSnapshotIfSmallEnough(snapshot: CacheSnapshot): string | undefined;
|
|
67
|
+
export interface RunOnceOptions {
|
|
68
|
+
/** Stable identity for the note this run belongs to — `vscode.Uri.toString()` of the document, in the real adapter. Shared with `./grant-flow.ts`'s `documentKey`. */
|
|
69
|
+
documentKey: string;
|
|
70
|
+
text: string;
|
|
71
|
+
/**
|
|
72
|
+
* How this run was triggered (GitHub issue #11). `'manual'` (the default
|
|
73
|
+
* when omitted) is the only trigger that runs the INTERACTIVE grant flow
|
|
74
|
+
* (`runGrantFlow`) — prompting on a miss and persisting the answer — and
|
|
75
|
+
* the only one the worker maps to the full `'manual'` capability tier.
|
|
76
|
+
* `'auto'` (run-on-open) and `'scheduled'` (interval) both resolve grants
|
|
77
|
+
* NON-INTERACTIVELY (`resolveStoredGrant`): they reuse a grant the user
|
|
78
|
+
* already made by hand, never prompt, never persist, and the worker runs
|
|
79
|
+
* them at the read-only tier (`@markii/runtime`'s `tierForTrigger`). This
|
|
80
|
+
* is the run-path half of the "effects always cost a click" rule
|
|
81
|
+
* (docs/scripting.md); the tier gate in the worker is the other half.
|
|
82
|
+
*/
|
|
83
|
+
trigger?: RunTrigger;
|
|
84
|
+
/**
|
|
85
|
+
* The DNS-rebinding / private-range policy (GitHub issue #10), forwarded
|
|
86
|
+
* verbatim to `spawnRun`'s `netPolicy` — see `./net-pinning.ts`'s
|
|
87
|
+
* `PinPolicy`. Omitted defaults to the fail-closed posture the worker
|
|
88
|
+
* itself applies (`allowRestrictedAddresses: false`); this is the
|
|
89
|
+
* deployment-level opt-in `markii.allowPrivateNetworkAddresses` maps to
|
|
90
|
+
* (`preview-panel.ts`), never something a note can set for itself.
|
|
91
|
+
*/
|
|
92
|
+
netPolicy?: SpawnRunRequest['netPolicy'];
|
|
93
|
+
memento: GrantMemento;
|
|
94
|
+
promptHost: PromptHost;
|
|
95
|
+
promptUnknownHosts: PromptUnknownHosts;
|
|
96
|
+
/** PROMPT-STORM guard's consolidated gate — see `./grant-flow.ts`'s `MAX_HOST_PROMPTS`. */
|
|
97
|
+
promptManyHosts: PromptManyHosts;
|
|
98
|
+
/** Injected so this function is testable with a fake worker runner — the real adapter passes `./run-host.ts`'s `spawnRun`. */
|
|
99
|
+
spawnRun: (options: SpawnRunRequest) => Promise<RunResult>;
|
|
100
|
+
/**
|
|
101
|
+
* Called once per script as its value arrives, while the run is still
|
|
102
|
+
* going (GitHub issue #35) — a host uses it to un-stale one bound
|
|
103
|
+
* component at a time instead of waiting for the whole batch. The value
|
|
104
|
+
* handed over has already been through `scrubStoredValueForWire`, so it
|
|
105
|
+
* is safe to post straight to a renderer, exactly like the `values` this
|
|
106
|
+
* function finally returns.
|
|
107
|
+
*
|
|
108
|
+
* Purely a notification. Persistence is NOT done here: the merged value
|
|
109
|
+
* store is written once, at the end (see below), so a run killed
|
|
110
|
+
* half-way can never leave a half-written record behind.
|
|
111
|
+
*/
|
|
112
|
+
onValue?: (name: string, value: StoredValue) => void;
|
|
113
|
+
timeoutMs: number;
|
|
114
|
+
/**
|
|
115
|
+
* Slice 2 of the `.mkz` Run-path arc (GitHub issue #9): present only for a
|
|
116
|
+
* bundle-backed document. `buildSnapshot` is called exactly once per run
|
|
117
|
+
* (`./bundle-run.ts`'s `buildBundleSnapshot`, already applied to whatever
|
|
118
|
+
* `BundleStorage` the caller has) — deferred to run time rather than
|
|
119
|
+
* built eagerly, so a snapshot is only ever read when a run actually
|
|
120
|
+
* happens. As of the F-1 fix it is built BEFORE the grant flow rather
|
|
121
|
+
* than immediately before `spawnRun`, so a `src=` script's resolved
|
|
122
|
+
* content (`bundleModulesFromSnapshot`) can be folded into the grant
|
|
123
|
+
* closure `computeGrantKey` hashes, ahead of any prompting — still the
|
|
124
|
+
* same single call per run, just earlier. `persistCacheOut` receives the
|
|
125
|
+
* worker's `RunResult.cacheOut`
|
|
126
|
+
* (its bundle `.cache/` subtree, post-run) when the run produced one, and
|
|
127
|
+
* is the caller's own persistence: writing back into a directory-form
|
|
128
|
+
* bundle on disk, or into extension storage keyed by bundle identity for
|
|
129
|
+
* a read-only zip form (see `preview-panel.ts`'s adapters).
|
|
130
|
+
*/
|
|
131
|
+
bundle?: {
|
|
132
|
+
manifest: BundleManifest;
|
|
133
|
+
buildSnapshot: () => Promise<Record<string, Uint8Array>>;
|
|
134
|
+
persistCacheOut: (cacheOut: Record<string, Uint8Array>) => Promise<void>;
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Slice 5 of the pack-loading arc (GitHub issue #3): pre-read pack Lua
|
|
138
|
+
* modules for every installed pack (`../packs/pack-scripts.ts`'s
|
|
139
|
+
* `loadPackModules`), forwarded verbatim to `spawnRun`'s `packModules` so
|
|
140
|
+
* the worker's `require "packName/..."` can resolve them. Absent when no
|
|
141
|
+
* packs are configured — `require` for a pack namespace then denies
|
|
142
|
+
* exactly as it did before packs existed.
|
|
143
|
+
*/
|
|
144
|
+
packModules?: PackModulesMap;
|
|
145
|
+
}
|
|
146
|
+
export interface RunOnceResult {
|
|
147
|
+
values: Record<string, StoredValue>;
|
|
148
|
+
failures: ValuesFailure[];
|
|
149
|
+
/**
|
|
150
|
+
* The same failures, message included, for the host's DIAGNOSTICS
|
|
151
|
+
* surface only (the "Markii" output channel, a developer console) —
|
|
152
|
+
* never for the rendered page or any per-note UI, which take the
|
|
153
|
+
* scrubbed `failures` above (D-1: a message can be a raw Lua traceback,
|
|
154
|
+
* or a denial that embeds the request URL). AGENTS.md's "clean is not
|
|
155
|
+
* silent": a failure whose reason reaches neither surface is a bug, and
|
|
156
|
+
* `{name, kind}` alone is not a reason a user can act on.
|
|
157
|
+
*/
|
|
158
|
+
failureDetails: {
|
|
159
|
+
name: string;
|
|
160
|
+
kind: ValuesFailure['kind'];
|
|
161
|
+
message: string;
|
|
162
|
+
}[];
|
|
163
|
+
/**
|
|
164
|
+
* Consent unification (SECURITY-RELEVANT): lines describing any mismatch
|
|
165
|
+
* between what is DECLARED (a bundle manifest's `permissions.net`, and/or
|
|
166
|
+
* an individual script's own `permissions` fence attribute — batch 7
|
|
167
|
+
* #47) and the hosts this run's scripts actually reach, from
|
|
168
|
+
* `./bundle-run.ts`'s `netDeclarationDiagnostics`. `[]` when there is
|
|
169
|
+
* nothing to compare against (no bundle manifest and no script declares
|
|
170
|
+
* `permissions`) or every declared host agrees with the scan. For the
|
|
171
|
+
* host's diagnostics surface only — never the rendered page — same
|
|
172
|
+
* posture as `failureDetails` above.
|
|
173
|
+
*/
|
|
174
|
+
netDeclarationDiagnostics: string[];
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Runs one manual pass of `options.text`'s scripts end to end: the grant
|
|
178
|
+
* flow decides the net allowlist (prompting only on a miss), the persisted
|
|
179
|
+
* cache snapshot seeds the run, and the run's own (possibly mutated) cache
|
|
180
|
+
* snapshot is written back — capped, never partially. The returned shape
|
|
181
|
+
* is exactly what the `values` wire message (`../protocol.ts`) needs,
|
|
182
|
+
* minus the `revision` tag, which the caller (who knows what revision this
|
|
183
|
+
* run was actually performed against) attaches. Every `StoredValue`'s raw
|
|
184
|
+
* `error` text is stripped before it is returned (D-1, `scrubValuesForWire`
|
|
185
|
+
* above) — same treatment `failures` already got.
|
|
186
|
+
*/
|
|
187
|
+
export declare function runOnce(options: RunOnceOptions): Promise<RunOnceResult>;
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2's top-level orchestration for one `markii.runScripts` press:
|
|
3
|
+
* extract requirements -> run the grant flow -> read/spawn/persist the
|
|
4
|
+
* cache snapshot -> shape the result for the `values` wire message
|
|
5
|
+
* (`../protocol.ts`). Deliberately vscode-free, like every other module
|
|
6
|
+
* under `./`: the ONLY vscode-shaped things this needs (a Memento, a
|
|
7
|
+
* `spawnRun`-like runner, and two prompt functions) arrive as plain
|
|
8
|
+
* parameters, so `extension.ts`/`preview-panel.ts` — the only files
|
|
9
|
+
* allowed to import `vscode` — reduce to supplying those and posting the
|
|
10
|
+
* resulting message. See `./grant-flow.ts` and `./run-host.ts` for the
|
|
11
|
+
* pieces this file composes.
|
|
12
|
+
*/
|
|
13
|
+
import { extractScriptBlocks, scanRunRequirementHosts, scriptDeclaredHosts, } from './script-requirements.js';
|
|
14
|
+
import { resolveStoredGrant, runGrantFlow } from './grant-flow.js';
|
|
15
|
+
import { bundleModulesFromSnapshot, manifestBundleFsGrants, manifestNetHosts, netDeclarationDiagnostics, } from './bundle-run.js';
|
|
16
|
+
/**
|
|
17
|
+
* Strips a `StoredValue`'s `error` field before it ever reaches the wire
|
|
18
|
+
* (D-1): `.error` is whatever text the executor happened to produce —
|
|
19
|
+
* a raw Lua traceback, or (worse, per B-3) a net-provider denial message
|
|
20
|
+
* that can embed the actual request URL/host — and none of that is meant
|
|
21
|
+
* for the rendered page (AGENTS.md's cleanliness principle: quiet markers,
|
|
22
|
+
* never error dumps). This mirrors exactly what `runOnce` below already
|
|
23
|
+
* does to `RunResult.failures` (reduced to `{name, kind}`, never the raw
|
|
24
|
+
* message): `failureKind` is kept, so a renderer (`@markii/react`'s
|
|
25
|
+
* `failure-presentation.ts`) still branches on the closed `FailureKind`
|
|
26
|
+
* taxonomy — nothing in this reference renderer ever reads `.error` for
|
|
27
|
+
* display, only for a value that DIDN'T fail (`status !== 'error'`), where
|
|
28
|
+
* `error` is absent anyway.
|
|
29
|
+
*/
|
|
30
|
+
function scrubStoredValueForWire(value) {
|
|
31
|
+
if (value.status !== 'error' || value.error === undefined)
|
|
32
|
+
return value;
|
|
33
|
+
const { error: _error, ...rest } = value;
|
|
34
|
+
return rest;
|
|
35
|
+
}
|
|
36
|
+
function scrubValuesForWire(values) {
|
|
37
|
+
// `Object.fromEntries` for the same reason `mergePersistedValues` uses it:
|
|
38
|
+
// a plain `scrubbed[name] = ...` silently loses the value of a script
|
|
39
|
+
// legitimately named `__proto__`.
|
|
40
|
+
return Object.fromEntries(Object.entries(values).map(([name, value]) => [
|
|
41
|
+
name,
|
|
42
|
+
scrubStoredValueForWire(value),
|
|
43
|
+
]));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A sane upper bound on a persisted cache snapshot: real cached `net.*`
|
|
47
|
+
* responses are small JSON payloads, so the cap exists to guarantee a
|
|
48
|
+
* runaway/adversarial script can never grow `workspaceState` without
|
|
49
|
+
* bound. A snapshot over this size is DROPPED, not truncated — a partial
|
|
50
|
+
* cache would silently look valid while missing entries a script assumed
|
|
51
|
+
* were still there, which is worse than starting the next run with none
|
|
52
|
+
* at all.
|
|
53
|
+
*/
|
|
54
|
+
export const MAX_CACHE_SNAPSHOT_BYTES = 1_000_000;
|
|
55
|
+
/** The single `workspaceState`/Memento key a document's cache snapshot lives under. */
|
|
56
|
+
export function cacheStorageKeyFor(documentKey) {
|
|
57
|
+
return `markii.runCache:${documentKey}`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The `workspaceState`/Memento key a document's LAST-KNOWN values live under
|
|
61
|
+
* (GitHub issue #11, gap 1). Separate from the cache snapshot
|
|
62
|
+
* (`cacheStorageKeyFor`): the cache is the sandbox's own `cache.get` state,
|
|
63
|
+
* keyed and TTL'd inside Lua; THIS is the rendered value store
|
|
64
|
+
* (`ValueStore.snapshot()`), the numbers a data-bound component actually
|
|
65
|
+
* showed. Persisting it is what lets a reopened monitoring note render its
|
|
66
|
+
* last figures instantly and offline, marked stale, before (or without) any
|
|
67
|
+
* re-run — exactly the "opening a note ... showing its last data marked as
|
|
68
|
+
* stale" behavior docs/scripting.md and docs/spec.md require.
|
|
69
|
+
*/
|
|
70
|
+
export function valuesStorageKeyFor(documentKey) {
|
|
71
|
+
return `markii.runValues:${documentKey}`;
|
|
72
|
+
}
|
|
73
|
+
/** Same cap and drop-not-truncate posture as `MAX_CACHE_SNAPSHOT_BYTES` — a runaway value store never grows `workspaceState` without bound, and a too-large snapshot is dropped whole (a partial value store would silently omit names a note binds). */
|
|
74
|
+
export const MAX_VALUES_SNAPSHOT_BYTES = 1_000_000;
|
|
75
|
+
/**
|
|
76
|
+
* True for a value this module is willing to persist and re-hydrate as a
|
|
77
|
+
* last-known value store — the same "is this a plain object at all"
|
|
78
|
+
* shallow trust `isCacheSnapshotShape` applies. A renderer-side hostile-shape
|
|
79
|
+
* guard (`../protocol.ts`'s `isWireStoredValueRecord`) still re-validates
|
|
80
|
+
* every entry when the host re-posts these over the wire, so this only needs
|
|
81
|
+
* to keep a corrupt/foreign top-level value from being merged.
|
|
82
|
+
*/
|
|
83
|
+
function isStoredValueRecord(value) {
|
|
84
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Reads a document's persisted last-known value store, or `{}` when there is
|
|
88
|
+
* none / it is unusable. Never throws, never trusts the stored shape past
|
|
89
|
+
* "plain object" — see `isStoredValueRecord`.
|
|
90
|
+
*/
|
|
91
|
+
export function readPersistedValues(memento, documentKey) {
|
|
92
|
+
const raw = memento.get(valuesStorageKeyFor(documentKey));
|
|
93
|
+
return isStoredValueRecord(raw) ? raw : {};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Merges a completed run's value store onto the previously-persisted one,
|
|
97
|
+
* keeping the LAST-KNOWN-GOOD value for any name whose fresh run FAILED
|
|
98
|
+
* (GitHub issue #11). A scheduled/auto run that hits a transient failure
|
|
99
|
+
* (offline, an API hiccup, an ungranted host under the read-only tier) must
|
|
100
|
+
* not wipe the good numbers a monitoring note was already showing — mirroring
|
|
101
|
+
* the same "the cache only ever remembers good data" guarantee
|
|
102
|
+
* docs/scripting.md gives `cache.get`. For each name in the new run: a
|
|
103
|
+
* non-error outcome always wins; an error outcome is kept ONLY if there is no
|
|
104
|
+
* prior good value to fall back to (so the error surfaces on a note that
|
|
105
|
+
* never succeeded), otherwise the prior value is retained and marked stale.
|
|
106
|
+
* Names present only in the prior store (a script the note no longer has) are
|
|
107
|
+
* dropped — the note's current scripts define the namespace.
|
|
108
|
+
*/
|
|
109
|
+
export function mergePersistedValues(previous, next) {
|
|
110
|
+
// Built through entries and `Object.fromEntries` rather than
|
|
111
|
+
// `merged[name] = ...`: assigning the name `__proto__` on a plain object
|
|
112
|
+
// sets its prototype instead of creating an own property, silently losing
|
|
113
|
+
// that value, and `__proto__` is a legal script name (docs/spec.md's
|
|
114
|
+
// `[A-Za-z_][A-Za-z0-9_-]*`). The same guard is in `./stale-values.ts`.
|
|
115
|
+
const merged = [];
|
|
116
|
+
for (const [name, value] of Object.entries(next)) {
|
|
117
|
+
if (typeof value !== 'object' || value === null)
|
|
118
|
+
continue;
|
|
119
|
+
if (value.status !== 'error') {
|
|
120
|
+
merged.push([name, value]);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
// `previous` is a PERSISTED store, guarded only down to "plain object"
|
|
124
|
+
// when it was read back, so an individual entry can be anything.
|
|
125
|
+
const priorRaw = Object.hasOwn(previous, name) ? previous[name] : undefined;
|
|
126
|
+
const prior = typeof priorRaw === 'object' && priorRaw !== null ? priorRaw : undefined;
|
|
127
|
+
merged.push([
|
|
128
|
+
name,
|
|
129
|
+
prior && prior.status !== 'error' ? { ...prior, status: 'stale' } : value,
|
|
130
|
+
]);
|
|
131
|
+
}
|
|
132
|
+
return Object.fromEntries(merged);
|
|
133
|
+
}
|
|
134
|
+
/** The JSON-and-size-guarded form of a value store to persist, or `undefined` to drop it (unserializable, or over `MAX_VALUES_SNAPSHOT_BYTES`) — same contract as `serializeCacheSnapshotIfSmallEnough`. */
|
|
135
|
+
function valuesSnapshotIsSmallEnough(values) {
|
|
136
|
+
let serialized;
|
|
137
|
+
try {
|
|
138
|
+
serialized = JSON.stringify(values);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
return serialized.length <= MAX_VALUES_SNAPSHOT_BYTES;
|
|
144
|
+
}
|
|
145
|
+
/** A plausible cache-snapshot shape read back from storage — never trusts it further than "is this a plain object at all" (an old/foreign/corrupt value degrades to "no cache", matching this whole module's fail-safe posture). */
|
|
146
|
+
export function isCacheSnapshotShape(value) {
|
|
147
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The JSON text for `snapshot`, or `undefined` when it must be dropped
|
|
151
|
+
* instead of persisted: either it isn't serializable at all (a value
|
|
152
|
+
* `JSON.stringify` cannot handle), or its serialized size exceeds
|
|
153
|
+
* `MAX_CACHE_SNAPSHOT_BYTES`. Callers write `undefined` back to storage in
|
|
154
|
+
* that case — see `MAX_CACHE_SNAPSHOT_BYTES`'s doc comment on why a partial
|
|
155
|
+
* write is never attempted.
|
|
156
|
+
*/
|
|
157
|
+
export function serializeCacheSnapshotIfSmallEnough(snapshot) {
|
|
158
|
+
let serialized;
|
|
159
|
+
try {
|
|
160
|
+
serialized = JSON.stringify(snapshot);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
return serialized.length <= MAX_CACHE_SNAPSHOT_BYTES ? serialized : undefined;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Runs one manual pass of `options.text`'s scripts end to end: the grant
|
|
169
|
+
* flow decides the net allowlist (prompting only on a miss), the persisted
|
|
170
|
+
* cache snapshot seeds the run, and the run's own (possibly mutated) cache
|
|
171
|
+
* snapshot is written back — capped, never partially. The returned shape
|
|
172
|
+
* is exactly what the `values` wire message (`../protocol.ts`) needs,
|
|
173
|
+
* minus the `revision` tag, which the caller (who knows what revision this
|
|
174
|
+
* run was actually performed against) attaches. Every `StoredValue`'s raw
|
|
175
|
+
* `error` text is stripped before it is returned (D-1, `scrubValuesForWire`
|
|
176
|
+
* above) — same treatment `failures` already got.
|
|
177
|
+
*/
|
|
178
|
+
export async function runOnce(options) {
|
|
179
|
+
const { scripts, grantScripts } = extractScriptBlocks(options.text);
|
|
180
|
+
// F-1 fix, still true under the consent-unification change below: the
|
|
181
|
+
// bundle snapshot is built BEFORE the host scan so a `src=` script's
|
|
182
|
+
// resolved content can both (a) be folded into the grant closure
|
|
183
|
+
// `computeGrantKey` hashes (`bundleModulesFromSnapshot`'s doc comment)
|
|
184
|
+
// and (b) let the scan resolve that block's `net.*` hosts instead of
|
|
185
|
+
// folding it into `hasUnknownHosts` — see `./bundle-run.ts`'s "Consent
|
|
186
|
+
// unification" section. Built exactly once per run either way.
|
|
187
|
+
const bundleSnapshot = options.bundle
|
|
188
|
+
? await options.bundle.buildSnapshot()
|
|
189
|
+
: undefined;
|
|
190
|
+
const bundleModules = options.bundle && bundleSnapshot
|
|
191
|
+
? bundleModulesFromSnapshot(grantScripts, bundleSnapshot)
|
|
192
|
+
: undefined;
|
|
193
|
+
// SECURITY-RELEVANT (consent unification): the static scan of this run's
|
|
194
|
+
// executable closure is the ONLY source of the hostnames prompted for —
|
|
195
|
+
// for a bundle too. A `src=` block resolves through `bundleModules` when
|
|
196
|
+
// its file is in the snapshot; otherwise it folds into
|
|
197
|
+
// `hasUnknownHosts`, exactly like a plain `.mk.md` document's `src=`
|
|
198
|
+
// reference always has. The manifest's `permissions.net` is never
|
|
199
|
+
// consulted here — see below for how it's compared instead.
|
|
200
|
+
const { hosts: scannedHosts, hasUnknownHosts } = scanRunRequirementHosts(scripts, bundleModules
|
|
201
|
+
? (block) => block.src !== undefined ? bundleModules[block.src] : undefined
|
|
202
|
+
: undefined);
|
|
203
|
+
// The manifest's declared `permissions.net` (for a bundle) and each
|
|
204
|
+
// script's own `permissions` fence attribute are BOTH declared intent
|
|
205
|
+
// only: neither widens or narrows `scannedHosts` above. A mismatch
|
|
206
|
+
// between what is declared (either source) and what is scanned is
|
|
207
|
+
// surfaced as diagnostics lines instead (host's diagnostics surface
|
|
208
|
+
// only, never the rendered page) — see `netDeclarationDiagnostics`'s
|
|
209
|
+
// doc comment. The two declared sets are merged into one comparison:
|
|
210
|
+
// a host a script declares but a bundle manifest does not (or vice
|
|
211
|
+
// versa) is not itself a mismatch worth its own line, only a mismatch
|
|
212
|
+
// against what actually runs.
|
|
213
|
+
const declaredNetHosts = [
|
|
214
|
+
...(options.bundle ? manifestNetHosts(options.bundle.manifest) : []),
|
|
215
|
+
...scriptDeclaredHosts(scripts),
|
|
216
|
+
];
|
|
217
|
+
const netDiagnostics = netDeclarationDiagnostics(declaredNetHosts, scannedHosts);
|
|
218
|
+
// The `bundle` capability needs no user-facing prompt any more (consent
|
|
219
|
+
// unification): the manifest's declared bundle-fs grants are passed
|
|
220
|
+
// straight through as this run's granted permissions.
|
|
221
|
+
// `@markii/bundle`'s `createScriptView` still intersects this against
|
|
222
|
+
// the manifest's own declaration, the path-jail still confines every
|
|
223
|
+
// read/write, and the read-only tier for auto/scheduled triggers still
|
|
224
|
+
// blocks `bundle.write` outright — only the prompt is gone.
|
|
225
|
+
const grantedBundlePermissions = options.bundle
|
|
226
|
+
? manifestBundleFsGrants(options.bundle.manifest)
|
|
227
|
+
: [];
|
|
228
|
+
const trigger = options.trigger ?? 'manual';
|
|
229
|
+
const grantRequirements = {
|
|
230
|
+
scripts,
|
|
231
|
+
grantScripts,
|
|
232
|
+
hosts: scannedHosts,
|
|
233
|
+
hasUnknownHosts,
|
|
234
|
+
...(bundleModules ? { bundleModules } : {}),
|
|
235
|
+
};
|
|
236
|
+
// GitHub issue #11: only a MANUAL run prompts and persists. An auto/
|
|
237
|
+
// scheduled run resolves grants non-interactively from what the user
|
|
238
|
+
// already granted by hand for this exact closure — never a modal on a
|
|
239
|
+
// timer, never a widened allowlist. See `resolveStoredGrant`'s doc comment.
|
|
240
|
+
const grant = trigger === 'manual'
|
|
241
|
+
? await runGrantFlow({
|
|
242
|
+
documentKey: options.documentKey,
|
|
243
|
+
requirements: grantRequirements,
|
|
244
|
+
memento: options.memento,
|
|
245
|
+
promptHost: options.promptHost,
|
|
246
|
+
declaredHosts: scriptDeclaredHosts(scripts),
|
|
247
|
+
promptUnknownHosts: options.promptUnknownHosts,
|
|
248
|
+
promptManyHosts: options.promptManyHosts,
|
|
249
|
+
})
|
|
250
|
+
: await resolveStoredGrant({
|
|
251
|
+
documentKey: options.documentKey,
|
|
252
|
+
requirements: grantRequirements,
|
|
253
|
+
memento: options.memento,
|
|
254
|
+
});
|
|
255
|
+
const cacheKey = cacheStorageKeyFor(options.documentKey);
|
|
256
|
+
const rawCache = options.memento.get(cacheKey);
|
|
257
|
+
const cacheSnapshot = isCacheSnapshotShape(rawCache) ? rawCache : {};
|
|
258
|
+
const result = await options.spawnRun({
|
|
259
|
+
text: options.text,
|
|
260
|
+
trigger,
|
|
261
|
+
// GitHub issue #35: forwarded per script, scrubbed the same way the
|
|
262
|
+
// final `values` are (D-1) so a raw executor message can never reach a
|
|
263
|
+
// renderer through the faster path either.
|
|
264
|
+
...(options.onValue
|
|
265
|
+
? {
|
|
266
|
+
onValue: (name, value) => {
|
|
267
|
+
options.onValue?.(name, scrubStoredValueForWire(value));
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
: {}),
|
|
271
|
+
netAllowlist: grant.allowedHosts,
|
|
272
|
+
...(options.netPolicy !== undefined
|
|
273
|
+
? { netPolicy: options.netPolicy }
|
|
274
|
+
: {}),
|
|
275
|
+
cacheSnapshot: cacheSnapshot,
|
|
276
|
+
timeoutMs: options.timeoutMs,
|
|
277
|
+
...(options.packModules !== undefined
|
|
278
|
+
? { packModules: options.packModules }
|
|
279
|
+
: {}),
|
|
280
|
+
...(options.bundle && bundleSnapshot
|
|
281
|
+
? {
|
|
282
|
+
bundle: {
|
|
283
|
+
snapshot: bundleSnapshot,
|
|
284
|
+
manifest: options.bundle.manifest,
|
|
285
|
+
grantedBundlePermissions,
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
: {}),
|
|
289
|
+
});
|
|
290
|
+
const nextSnapshotText = serializeCacheSnapshotIfSmallEnough(result.cacheSnapshot);
|
|
291
|
+
await options.memento.update(cacheKey, nextSnapshotText === undefined ? undefined : result.cacheSnapshot);
|
|
292
|
+
if (options.bundle && result.cacheOut) {
|
|
293
|
+
await options.bundle.persistCacheOut(result.cacheOut);
|
|
294
|
+
}
|
|
295
|
+
// GitHub issue #11, gap 1: persist the (wire-scrubbed) value store as this
|
|
296
|
+
// note's last-known values, merged onto the prior store so a failed
|
|
297
|
+
// scheduled/auto run never wipes good numbers (`mergePersistedValues`).
|
|
298
|
+
// Dropped whole, never partially, if it fails the size/serialize guard.
|
|
299
|
+
//
|
|
300
|
+
// GitHub issue #35 deliberately did NOT move this write per value. One
|
|
301
|
+
// write at the end is the only way a killed run cannot leave a partial
|
|
302
|
+
// record, and there is nothing to gain from the earlier writes: a run
|
|
303
|
+
// that dies half-way still arrives here, because `spawnRun` carries the
|
|
304
|
+
// values that already landed into its synthetic failure result, so the
|
|
305
|
+
// scripts that finished are persisted either way.
|
|
306
|
+
const scrubbedValues = scrubValuesForWire(result.values);
|
|
307
|
+
const valuesKey = valuesStorageKeyFor(options.documentKey);
|
|
308
|
+
const mergedValues = mergePersistedValues(readPersistedValues(options.memento, options.documentKey), scrubbedValues);
|
|
309
|
+
await options.memento.update(valuesKey, valuesSnapshotIsSmallEnough(mergedValues) ? mergedValues : undefined);
|
|
310
|
+
return {
|
|
311
|
+
values: scrubbedValues,
|
|
312
|
+
failures: result.failures.map((failure) => ({
|
|
313
|
+
name: failure.name,
|
|
314
|
+
kind: failure.kind,
|
|
315
|
+
})),
|
|
316
|
+
failureDetails: result.failures.map((failure) => ({
|
|
317
|
+
name: failure.name,
|
|
318
|
+
kind: failure.kind,
|
|
319
|
+
message: failure.message,
|
|
320
|
+
})),
|
|
321
|
+
netDeclarationDiagnostics: netDiagnostics,
|
|
322
|
+
};
|
|
323
|
+
}
|