@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,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2 of the `.mkz` Run-path arc (GitHub issue #9's locked design
|
|
3
|
+
* comment): the host-side pieces that turn a bundle's `@markii/bundle`
|
|
4
|
+
* `BundleStorage` into the SNAPSHOT a worker run is handed, and back a
|
|
5
|
+
* worker's `.cache/` output into something the host can persist. Kept
|
|
6
|
+
* `vscode`-free, like every other module under `./run` — `preview-panel.ts`
|
|
7
|
+
* (the only file allowed to import `vscode` alongside `extension.ts`)
|
|
8
|
+
* supplies the actual `BundleStorage`/`Memento`/filesystem calls.
|
|
9
|
+
*
|
|
10
|
+
* ## Why a snapshot at all
|
|
11
|
+
*
|
|
12
|
+
* The worker is a separate `worker_thread` and must never hold a live
|
|
13
|
+
* handle to a zip archive or the user's disk (docs/security.md's isolate
|
|
14
|
+
* requirement, restated in issue #9's design comment) — so the host reads
|
|
15
|
+
* whatever a run may legitimately touch into plain bytes BEFORE spawning
|
|
16
|
+
* the worker, and the worker's `@markii/bundle` `ScriptView` is backed by
|
|
17
|
+
* that in-memory snapshot alone (`./snapshot-storage.ts`), never by the
|
|
18
|
+
* real storage.
|
|
19
|
+
*
|
|
20
|
+
* ## What's in the snapshot
|
|
21
|
+
*
|
|
22
|
+
* The design comment scopes this to "the `scripts/` tree, the subset of
|
|
23
|
+
* `assets/` reachable, and current `.cache/` contents" — deliberately NOT
|
|
24
|
+
* `manifest.json` or the document itself, even though `@markii/bundle`'s
|
|
25
|
+
* `ScriptView.read` places no path restriction on a granted read (only
|
|
26
|
+
* `write` is jailed to `.cache/`). A script that specifically wants to
|
|
27
|
+
* introspect its own manifest or document text cannot do so through
|
|
28
|
+
* `bundle.read` in this slice — `bundle.read("manifest.json")` comes back
|
|
29
|
+
* `undefined`, indistinguishable from "no such file", exactly like any
|
|
30
|
+
* other path the snapshot didn't collect (AGENTS.md's cleanliness
|
|
31
|
+
* principle: a quiet miss, never a special-cased error). This is a
|
|
32
|
+
* deliberate scope line for this slice, not an oversight — flagged in the
|
|
33
|
+
* slice's own report rather than silently widened.
|
|
34
|
+
*
|
|
35
|
+
* ## Consent unification (SECURITY-RELEVANT)
|
|
36
|
+
*
|
|
37
|
+
* Two rules this module and its callers (`./run-flow.ts`, `./grant-flow.ts`)
|
|
38
|
+
* now hold to, so a bundle's consent story matches a bare `.mk.md`
|
|
39
|
+
* document's exactly:
|
|
40
|
+
*
|
|
41
|
+
* - `manifestNetHosts` (the manifest's `permissions.net` declaration) is
|
|
42
|
+
* never merged into what a run prompts for. The static scan
|
|
43
|
+
* (`./script-requirements.ts`) is the only source of prompted hostnames,
|
|
44
|
+
* for a bundle too — see `netDeclarationDiagnostics` below for how a
|
|
45
|
+
* mismatch is surfaced instead.
|
|
46
|
+
* - The `bundle` capability (`permissions.bundle`) needs no user-facing
|
|
47
|
+
* prompt at all: `@markii/bundle`'s path-jail confines every read to the
|
|
48
|
+
* snapshot and every write to `.cache/`, and the read-only tier for
|
|
49
|
+
* auto/scheduled triggers blocks `bundle.write` outright regardless of
|
|
50
|
+
* what the manifest declares. `manifestBundleFsGrants`'s result is passed
|
|
51
|
+
* straight through as the run's granted bundle-fs permissions — see
|
|
52
|
+
* `./run-flow.ts`.
|
|
53
|
+
*/
|
|
54
|
+
import type { BundleManifest, BundleStorage } from '@markii/bundle';
|
|
55
|
+
import type { BundleFsGrant } from '@markii/bundle';
|
|
56
|
+
import type { GrantClosureScript } from '@markii/runtime';
|
|
57
|
+
/**
|
|
58
|
+
* Total byte budget for one run's bundle snapshot (across `scripts/` +
|
|
59
|
+
* `.cache/` + `assets/` combined). Mirrors the same order of magnitude as
|
|
60
|
+
* `bundle-resolve.ts`'s `DEFAULT_MAX_EMBEDDED_ASSET_BYTES` (20MB) — a
|
|
61
|
+
* personal note bundle's scripts and cache are typically tiny; this budget
|
|
62
|
+
* exists so a hostile or oversized bundle degrades quietly (files beyond
|
|
63
|
+
* the cap are simply absent from the snapshot, exactly like a path the
|
|
64
|
+
* snapshot never collected) rather than exhausting worker/host memory or
|
|
65
|
+
* ballooning the `postMessage` structured-clone payload.
|
|
66
|
+
*/
|
|
67
|
+
export declare const DEFAULT_MAX_BUNDLE_SNAPSHOT_BYTES: number;
|
|
68
|
+
/**
|
|
69
|
+
* Per-file cap applied on top of the total budget (C-1, PENTEST-REPORT
|
|
70
|
+
* follow-up): a single huge file could otherwise consume the ENTIRE total
|
|
71
|
+
* budget by itself, and — before this fix — would be read into memory in
|
|
72
|
+
* full before the total-budget check ever saw it. Sized the same as the
|
|
73
|
+
* total budget by default; a caller wanting a stricter per-file limit can
|
|
74
|
+
* override it independently via `maxFileBytes`.
|
|
75
|
+
*/
|
|
76
|
+
export declare const DEFAULT_MAX_BUNDLE_SNAPSHOT_FILE_BYTES: number;
|
|
77
|
+
export interface BuildBundleSnapshotOptions {
|
|
78
|
+
readonly maxTotalBytes?: number;
|
|
79
|
+
readonly maxFileBytes?: number;
|
|
80
|
+
}
|
|
81
|
+
export interface BundleSnapshotResult {
|
|
82
|
+
/** Bundle-relative path -> file bytes, ready to hand to `spawnRun`'s `bundle.snapshot`. */
|
|
83
|
+
readonly files: Record<string, Uint8Array>;
|
|
84
|
+
/** `true` when at least one otherwise-eligible file was left out because the budget was reached — a quiet degrade, never a thrown error. */
|
|
85
|
+
readonly truncated: boolean;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Reads every file under `scripts/`, `.cache/`, and `assets/` out of
|
|
89
|
+
* `storage`, up to `maxTotalBytes` total. `storage.list()` already returns
|
|
90
|
+
* paths sorted, so which files get dropped once the budget is hit is
|
|
91
|
+
* deterministic rather than depending on iteration order.
|
|
92
|
+
*/
|
|
93
|
+
export declare function buildBundleSnapshot(storage: BundleStorage, options?: BuildBundleSnapshotOptions): Promise<BundleSnapshotResult>;
|
|
94
|
+
/**
|
|
95
|
+
* F-1 fix: the `bundleModules` half of the grant closure `computeGrantKey`
|
|
96
|
+
* (`@markii/runtime`) hashes — resolved from `snapshot` (the same in-memory
|
|
97
|
+
* bundle snapshot a run's `ScriptView` is backed by, see this module's top
|
|
98
|
+
* doc comment) rather than left empty.
|
|
99
|
+
*
|
|
100
|
+
* Before this fix, `grant-flow.ts`'s `closureFrom` always passed
|
|
101
|
+
* `bundleModules: {}`, so a `src=scripts/etl.lua` block's `code` was `""`
|
|
102
|
+
* (@markii/core's `ScriptBlock` leaves it empty for a `src=` reference — the
|
|
103
|
+
* actual source lives in the referenced file) and the grant key never
|
|
104
|
+
* reflected that file's bytes at all: swapping the file's contents while
|
|
105
|
+
* the note's own text stayed byte-identical re-ran new code under an old,
|
|
106
|
+
* un-reprompted grant.
|
|
107
|
+
*
|
|
108
|
+
* Only `src=` targets are resolved here — `require()` (bundle-local or
|
|
109
|
+
* pack) stays unwired for the whole `.mkz` Run-path arc (see
|
|
110
|
+
* `grant-flow.ts`'s top doc comment), so a script's closure has no other
|
|
111
|
+
* bundle-local code to fold in yet. A `src=` path missing from `snapshot`
|
|
112
|
+
* (file deleted, or dropped by `buildBundleSnapshot`'s own budget) is
|
|
113
|
+
* simply omitted from the result — never thrown — so a note whose script
|
|
114
|
+
* references a missing file still gets a (now-narrower, but well-defined)
|
|
115
|
+
* grant key instead of failing the whole grant flow.
|
|
116
|
+
*/
|
|
117
|
+
export declare function bundleModulesFromSnapshot(scripts: readonly GrantClosureScript[], snapshot: Record<string, Uint8Array>): Record<string, string>;
|
|
118
|
+
/** Every `.cache/`-prefixed entry in `files`, unchanged — the shape a worker's `RunResult.cacheOut` carries back and a host persists. */
|
|
119
|
+
export declare function cacheFilesFrom(files: Record<string, Uint8Array>): Record<string, Uint8Array>;
|
|
120
|
+
/**
|
|
121
|
+
* Overlays `persisted` (a prior run's `.cache/` output, read back from
|
|
122
|
+
* wherever the host keeps it) onto `base` (a freshly built snapshot),
|
|
123
|
+
* persisted entries winning on a path collision — the "seed the next run's
|
|
124
|
+
* snapshot from the persisted cache" half of the design. Used for the
|
|
125
|
+
* zip-form path, where the archive's own on-disk `.cache/` entries (if any)
|
|
126
|
+
* are stale the moment a run has written a persisted cache elsewhere (the
|
|
127
|
+
* host never rewrites the user's zip — see `docs` design comment).
|
|
128
|
+
*/
|
|
129
|
+
export declare function withPersistedCache(base: Record<string, Uint8Array>, persisted: Record<string, Uint8Array>): Record<string, Uint8Array>;
|
|
130
|
+
/** Bare, lowercased hostnames a manifest declares under `permissions.net.get`/`.post`, deduplicated. */
|
|
131
|
+
export declare function manifestNetHosts(manifest: BundleManifest): string[];
|
|
132
|
+
/**
|
|
133
|
+
* Consent unification (SECURITY-RELEVANT, GitHub issue #9 follow-up): a
|
|
134
|
+
* bundle's manifest `permissions.net` is DECLARED INTENT only. The static
|
|
135
|
+
* scan of the run's executable closure (`./script-requirements.ts`) is the
|
|
136
|
+
* ONLY source of the hostnames a run prompts for, for a bundle exactly as
|
|
137
|
+
* for a bare `.mk.md` document — the manifest's declaration never widens
|
|
138
|
+
* that prompt (a declared-but-unused host is never prompted for) and never
|
|
139
|
+
* narrows it (a host a script actually reaches is always prompted for,
|
|
140
|
+
* declared or not).
|
|
141
|
+
*
|
|
142
|
+
* A mismatch between the two is not silently absorbed: this pure function
|
|
143
|
+
* returns one diagnostics line per mismatched host, in both directions, for
|
|
144
|
+
* the host's diagnostics surface (docs/integration.md) — never the rendered
|
|
145
|
+
* page. Wording lives here, the one place both apps' adapters read it from,
|
|
146
|
+
* so it can never drift between VS Code's output channel and Obsidian's
|
|
147
|
+
* console/notice surface.
|
|
148
|
+
*
|
|
149
|
+
* Returns `[]` when the two sets agree (including when both are empty).
|
|
150
|
+
* Order: every declared-but-unused host first (in `declaredHosts` order),
|
|
151
|
+
* then every used-but-undeclared host (in `scannedHosts` order) — matching
|
|
152
|
+
* how `manifestNetHosts`/`extractRunRequirements` already dedupe and order
|
|
153
|
+
* their inputs, so this function does no case-folding or deduping of its
|
|
154
|
+
* own.
|
|
155
|
+
*/
|
|
156
|
+
export declare function netDeclarationDiagnostics(declaredHosts: readonly string[], scannedHosts: readonly string[]): string[];
|
|
157
|
+
/** The bundle-filesystem grants a manifest declares under `permissions.bundle` (`'read'` / `'write:.cache/'`), or `[]` when it declares none. */
|
|
158
|
+
export declare function manifestBundleFsGrants(manifest: BundleManifest): BundleFsGrant[];
|
|
159
|
+
/**
|
|
160
|
+
* Cap on a zip-form bundle's persisted `.cache/` state (extension storage,
|
|
161
|
+
* keyed by bundle identity — see `preview-panel.ts`'s adapters). Mirrors
|
|
162
|
+
* `run-flow.ts`'s `MAX_CACHE_SNAPSHOT_BYTES` for the unrelated `cache.get`
|
|
163
|
+
* Memento snapshot: real script-written cache files are small, so this
|
|
164
|
+
* exists purely to guarantee a runaway/adversarial bundle can never grow
|
|
165
|
+
* `workspaceState` without bound. Oversize state is DROPPED wholesale, not
|
|
166
|
+
* partially persisted — see that constant's doc comment for why a partial
|
|
167
|
+
* write is worse than none.
|
|
168
|
+
*/
|
|
169
|
+
export declare const MAX_PERSISTED_BUNDLE_CACHE_BYTES = 1000000;
|
|
170
|
+
/** A JSON-safe, base64-encoded form of a `path -> bytes` cache map, suitable for a `vscode.Memento` value. */
|
|
171
|
+
export type EncodedBundleCache = Record<string, string>;
|
|
172
|
+
/**
|
|
173
|
+
* Encodes `files` (a bundle run's `.cache/` output) for Memento storage, or
|
|
174
|
+
* `undefined` when it must be dropped instead — either it doesn't encode at
|
|
175
|
+
* all, or the encoded form exceeds `MAX_PERSISTED_BUNDLE_CACHE_BYTES`.
|
|
176
|
+
* Callers write `undefined` back to storage in that case, exactly like
|
|
177
|
+
* `run-flow.ts`'s `serializeCacheSnapshotIfSmallEnough`.
|
|
178
|
+
*/
|
|
179
|
+
export declare function encodeBundleCacheForStorage(files: Record<string, Uint8Array>): EncodedBundleCache | undefined;
|
|
180
|
+
/**
|
|
181
|
+
* Decodes a value read back from a `vscode.Memento` into a `path -> bytes`
|
|
182
|
+
* cache map — never trusts the stored shape further than "every value is a
|
|
183
|
+
* string that decodes as base64"; a corrupt/foreign/hand-edited value
|
|
184
|
+
* degrades to an empty map (no persisted cache), matching this whole
|
|
185
|
+
* module's fail-safe posture (`grant-flow.ts`'s `isStoredGrant` takes the
|
|
186
|
+
* same stance for its own Memento reads).
|
|
187
|
+
*/
|
|
188
|
+
export declare function decodeBundleCacheFromStorage(value: unknown): Record<string, Uint8Array>;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/** The three bundle-relative prefixes a run's snapshot ever collects — see this module's top doc comment. */
|
|
2
|
+
const SNAPSHOT_PREFIXES = ['scripts/', '.cache/', 'assets/'];
|
|
3
|
+
/**
|
|
4
|
+
* Total byte budget for one run's bundle snapshot (across `scripts/` +
|
|
5
|
+
* `.cache/` + `assets/` combined). Mirrors the same order of magnitude as
|
|
6
|
+
* `bundle-resolve.ts`'s `DEFAULT_MAX_EMBEDDED_ASSET_BYTES` (20MB) — a
|
|
7
|
+
* personal note bundle's scripts and cache are typically tiny; this budget
|
|
8
|
+
* exists so a hostile or oversized bundle degrades quietly (files beyond
|
|
9
|
+
* the cap are simply absent from the snapshot, exactly like a path the
|
|
10
|
+
* snapshot never collected) rather than exhausting worker/host memory or
|
|
11
|
+
* ballooning the `postMessage` structured-clone payload.
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_MAX_BUNDLE_SNAPSHOT_BYTES = 20 * 1024 * 1024;
|
|
14
|
+
/**
|
|
15
|
+
* Per-file cap applied on top of the total budget (C-1, PENTEST-REPORT
|
|
16
|
+
* follow-up): a single huge file could otherwise consume the ENTIRE total
|
|
17
|
+
* budget by itself, and — before this fix — would be read into memory in
|
|
18
|
+
* full before the total-budget check ever saw it. Sized the same as the
|
|
19
|
+
* total budget by default; a caller wanting a stricter per-file limit can
|
|
20
|
+
* override it independently via `maxFileBytes`.
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_MAX_BUNDLE_SNAPSHOT_FILE_BYTES = DEFAULT_MAX_BUNDLE_SNAPSHOT_BYTES;
|
|
23
|
+
/**
|
|
24
|
+
* Reads every file under `scripts/`, `.cache/`, and `assets/` out of
|
|
25
|
+
* `storage`, up to `maxTotalBytes` total. `storage.list()` already returns
|
|
26
|
+
* paths sorted, so which files get dropped once the budget is hit is
|
|
27
|
+
* deterministic rather than depending on iteration order.
|
|
28
|
+
*/
|
|
29
|
+
export async function buildBundleSnapshot(storage, options = {}) {
|
|
30
|
+
const maxTotalBytes = options.maxTotalBytes ?? DEFAULT_MAX_BUNDLE_SNAPSHOT_BYTES;
|
|
31
|
+
const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_BUNDLE_SNAPSHOT_FILE_BYTES;
|
|
32
|
+
const allPaths = await storage.list();
|
|
33
|
+
const eligible = allPaths.filter((path) => SNAPSHOT_PREFIXES.some((prefix) => path.startsWith(prefix)));
|
|
34
|
+
const files = {};
|
|
35
|
+
let total = 0;
|
|
36
|
+
let truncated = false;
|
|
37
|
+
for (const path of eligible) {
|
|
38
|
+
// C-1: consult `size()` BEFORE ever calling `read()` — a file whose
|
|
39
|
+
// declared size alone would blow the per-file or remaining total budget
|
|
40
|
+
// is skipped without materializing its bytes at all, which is the whole
|
|
41
|
+
// point: a host-side `readFile` of a multi-gigabyte file (directory
|
|
42
|
+
// form) would otherwise allocate that much in the extension host before
|
|
43
|
+
// any cap ever saw it. `size()` returning `undefined` (path vanished
|
|
44
|
+
// between `list()` and here, or the storage form has no size for it) is
|
|
45
|
+
// treated exactly like the file being absent: a quiet skip, never a
|
|
46
|
+
// fallback to reading it blind.
|
|
47
|
+
const size = await storage.size(path);
|
|
48
|
+
if (size === undefined)
|
|
49
|
+
continue;
|
|
50
|
+
if (size > maxFileBytes || total + size > maxTotalBytes) {
|
|
51
|
+
truncated = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const bytes = await storage.read(path);
|
|
55
|
+
if (bytes === undefined)
|
|
56
|
+
continue;
|
|
57
|
+
// Defense in depth: re-check the actual byte length against the budget.
|
|
58
|
+
// A well-behaved `BundleStorage` never disagrees between `size()` and
|
|
59
|
+
// `read()`, but the total-budget accounting below must never trust
|
|
60
|
+
// `size()` blindly if it ever did.
|
|
61
|
+
if (total + bytes.length > maxTotalBytes) {
|
|
62
|
+
truncated = true;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
total += bytes.length;
|
|
66
|
+
files[path] = bytes;
|
|
67
|
+
}
|
|
68
|
+
return { files, truncated };
|
|
69
|
+
}
|
|
70
|
+
const utf8Decoder = new TextDecoder('utf-8', { fatal: false });
|
|
71
|
+
/**
|
|
72
|
+
* F-1 fix: the `bundleModules` half of the grant closure `computeGrantKey`
|
|
73
|
+
* (`@markii/runtime`) hashes — resolved from `snapshot` (the same in-memory
|
|
74
|
+
* bundle snapshot a run's `ScriptView` is backed by, see this module's top
|
|
75
|
+
* doc comment) rather than left empty.
|
|
76
|
+
*
|
|
77
|
+
* Before this fix, `grant-flow.ts`'s `closureFrom` always passed
|
|
78
|
+
* `bundleModules: {}`, so a `src=scripts/etl.lua` block's `code` was `""`
|
|
79
|
+
* (@markii/core's `ScriptBlock` leaves it empty for a `src=` reference — the
|
|
80
|
+
* actual source lives in the referenced file) and the grant key never
|
|
81
|
+
* reflected that file's bytes at all: swapping the file's contents while
|
|
82
|
+
* the note's own text stayed byte-identical re-ran new code under an old,
|
|
83
|
+
* un-reprompted grant.
|
|
84
|
+
*
|
|
85
|
+
* Only `src=` targets are resolved here — `require()` (bundle-local or
|
|
86
|
+
* pack) stays unwired for the whole `.mkz` Run-path arc (see
|
|
87
|
+
* `grant-flow.ts`'s top doc comment), so a script's closure has no other
|
|
88
|
+
* bundle-local code to fold in yet. A `src=` path missing from `snapshot`
|
|
89
|
+
* (file deleted, or dropped by `buildBundleSnapshot`'s own budget) is
|
|
90
|
+
* simply omitted from the result — never thrown — so a note whose script
|
|
91
|
+
* references a missing file still gets a (now-narrower, but well-defined)
|
|
92
|
+
* grant key instead of failing the whole grant flow.
|
|
93
|
+
*/
|
|
94
|
+
export function bundleModulesFromSnapshot(scripts, snapshot) {
|
|
95
|
+
const modules = {};
|
|
96
|
+
for (const script of scripts) {
|
|
97
|
+
if (script.src === undefined)
|
|
98
|
+
continue;
|
|
99
|
+
const bytes = snapshot[script.src];
|
|
100
|
+
if (bytes === undefined)
|
|
101
|
+
continue;
|
|
102
|
+
modules[script.src] = utf8Decoder.decode(bytes);
|
|
103
|
+
}
|
|
104
|
+
return modules;
|
|
105
|
+
}
|
|
106
|
+
/** Every `.cache/`-prefixed entry in `files`, unchanged — the shape a worker's `RunResult.cacheOut` carries back and a host persists. */
|
|
107
|
+
export function cacheFilesFrom(files) {
|
|
108
|
+
const out = {};
|
|
109
|
+
for (const [path, bytes] of Object.entries(files)) {
|
|
110
|
+
if (path.startsWith('.cache/'))
|
|
111
|
+
out[path] = bytes;
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Overlays `persisted` (a prior run's `.cache/` output, read back from
|
|
117
|
+
* wherever the host keeps it) onto `base` (a freshly built snapshot),
|
|
118
|
+
* persisted entries winning on a path collision — the "seed the next run's
|
|
119
|
+
* snapshot from the persisted cache" half of the design. Used for the
|
|
120
|
+
* zip-form path, where the archive's own on-disk `.cache/` entries (if any)
|
|
121
|
+
* are stale the moment a run has written a persisted cache elsewhere (the
|
|
122
|
+
* host never rewrites the user's zip — see `docs` design comment).
|
|
123
|
+
*/
|
|
124
|
+
export function withPersistedCache(base, persisted) {
|
|
125
|
+
return { ...base, ...persisted };
|
|
126
|
+
}
|
|
127
|
+
/** Bare, lowercased hostnames a manifest declares under `permissions.net.get`/`.post`, deduplicated. */
|
|
128
|
+
export function manifestNetHosts(manifest) {
|
|
129
|
+
const get = manifest.permissions?.net?.get ?? [];
|
|
130
|
+
const post = manifest.permissions?.net?.post ?? [];
|
|
131
|
+
return [...new Set([...get, ...post].map((host) => host.toLowerCase()))];
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Consent unification (SECURITY-RELEVANT, GitHub issue #9 follow-up): a
|
|
135
|
+
* bundle's manifest `permissions.net` is DECLARED INTENT only. The static
|
|
136
|
+
* scan of the run's executable closure (`./script-requirements.ts`) is the
|
|
137
|
+
* ONLY source of the hostnames a run prompts for, for a bundle exactly as
|
|
138
|
+
* for a bare `.mk.md` document — the manifest's declaration never widens
|
|
139
|
+
* that prompt (a declared-but-unused host is never prompted for) and never
|
|
140
|
+
* narrows it (a host a script actually reaches is always prompted for,
|
|
141
|
+
* declared or not).
|
|
142
|
+
*
|
|
143
|
+
* A mismatch between the two is not silently absorbed: this pure function
|
|
144
|
+
* returns one diagnostics line per mismatched host, in both directions, for
|
|
145
|
+
* the host's diagnostics surface (docs/integration.md) — never the rendered
|
|
146
|
+
* page. Wording lives here, the one place both apps' adapters read it from,
|
|
147
|
+
* so it can never drift between VS Code's output channel and Obsidian's
|
|
148
|
+
* console/notice surface.
|
|
149
|
+
*
|
|
150
|
+
* Returns `[]` when the two sets agree (including when both are empty).
|
|
151
|
+
* Order: every declared-but-unused host first (in `declaredHosts` order),
|
|
152
|
+
* then every used-but-undeclared host (in `scannedHosts` order) — matching
|
|
153
|
+
* how `manifestNetHosts`/`extractRunRequirements` already dedupe and order
|
|
154
|
+
* their inputs, so this function does no case-folding or deduping of its
|
|
155
|
+
* own.
|
|
156
|
+
*/
|
|
157
|
+
export function netDeclarationDiagnostics(declaredHosts, scannedHosts) {
|
|
158
|
+
const declared = new Set(declaredHosts);
|
|
159
|
+
const scanned = new Set(scannedHosts);
|
|
160
|
+
const lines = [];
|
|
161
|
+
for (const host of declaredHosts) {
|
|
162
|
+
if (!scanned.has(host)) {
|
|
163
|
+
lines.push(`The manifest declares net access to ${host}. No script in this run uses that host.`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
for (const host of scannedHosts) {
|
|
167
|
+
if (!declared.has(host)) {
|
|
168
|
+
lines.push(`A script in this run uses net access to ${host}. The manifest does not declare that host.`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return lines;
|
|
172
|
+
}
|
|
173
|
+
/** The bundle-filesystem grants a manifest declares under `permissions.bundle` (`'read'` / `'write:.cache/'`), or `[]` when it declares none. */
|
|
174
|
+
export function manifestBundleFsGrants(manifest) {
|
|
175
|
+
return manifest.permissions?.bundle ?? [];
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Cap on a zip-form bundle's persisted `.cache/` state (extension storage,
|
|
179
|
+
* keyed by bundle identity — see `preview-panel.ts`'s adapters). Mirrors
|
|
180
|
+
* `run-flow.ts`'s `MAX_CACHE_SNAPSHOT_BYTES` for the unrelated `cache.get`
|
|
181
|
+
* Memento snapshot: real script-written cache files are small, so this
|
|
182
|
+
* exists purely to guarantee a runaway/adversarial bundle can never grow
|
|
183
|
+
* `workspaceState` without bound. Oversize state is DROPPED wholesale, not
|
|
184
|
+
* partially persisted — see that constant's doc comment for why a partial
|
|
185
|
+
* write is worse than none.
|
|
186
|
+
*/
|
|
187
|
+
export const MAX_PERSISTED_BUNDLE_CACHE_BYTES = 1_000_000;
|
|
188
|
+
/**
|
|
189
|
+
* Encodes `files` (a bundle run's `.cache/` output) for Memento storage, or
|
|
190
|
+
* `undefined` when it must be dropped instead — either it doesn't encode at
|
|
191
|
+
* all, or the encoded form exceeds `MAX_PERSISTED_BUNDLE_CACHE_BYTES`.
|
|
192
|
+
* Callers write `undefined` back to storage in that case, exactly like
|
|
193
|
+
* `run-flow.ts`'s `serializeCacheSnapshotIfSmallEnough`.
|
|
194
|
+
*/
|
|
195
|
+
export function encodeBundleCacheForStorage(files) {
|
|
196
|
+
const encoded = {};
|
|
197
|
+
let total = 0;
|
|
198
|
+
for (const [path, bytes] of Object.entries(files)) {
|
|
199
|
+
total += bytes.length;
|
|
200
|
+
if (total > MAX_PERSISTED_BUNDLE_CACHE_BYTES)
|
|
201
|
+
return undefined;
|
|
202
|
+
encoded[path] = Buffer.from(bytes).toString('base64');
|
|
203
|
+
}
|
|
204
|
+
return encoded;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Decodes a value read back from a `vscode.Memento` into a `path -> bytes`
|
|
208
|
+
* cache map — never trusts the stored shape further than "every value is a
|
|
209
|
+
* string that decodes as base64"; a corrupt/foreign/hand-edited value
|
|
210
|
+
* degrades to an empty map (no persisted cache), matching this whole
|
|
211
|
+
* module's fail-safe posture (`grant-flow.ts`'s `isStoredGrant` takes the
|
|
212
|
+
* same stance for its own Memento reads).
|
|
213
|
+
*/
|
|
214
|
+
export function decodeBundleCacheFromStorage(value) {
|
|
215
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
216
|
+
return {};
|
|
217
|
+
}
|
|
218
|
+
const out = {};
|
|
219
|
+
for (const [path, encoded] of Object.entries(value)) {
|
|
220
|
+
if (typeof encoded !== 'string')
|
|
221
|
+
continue;
|
|
222
|
+
try {
|
|
223
|
+
out[path] = new Uint8Array(Buffer.from(encoded, 'base64'));
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// Skip a value that isn't valid base64 rather than failing the whole
|
|
227
|
+
// decode — one corrupt entry shouldn't take down every other cached
|
|
228
|
+
// file.
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2's grant/prompt flow (docs: GitHub issue #1's locked design
|
|
3
|
+
* comment) — vscode-free by design, same split as `./run-host.ts` and
|
|
4
|
+
* `./script-requirements.ts`: this module decides WHAT to prompt and WHAT
|
|
5
|
+
* to persist, but never calls `vscode.window.showInformationMessage` or
|
|
6
|
+
* `context.workspaceState` directly. The extension-host adapter
|
|
7
|
+
* (`extension.ts`/`preview-panel.ts` — the only files allowed to import
|
|
8
|
+
* `vscode`) supplies the actual prompt UI and Memento, both behind the thin
|
|
9
|
+
* interfaces below, so this file stays unit-testable with plain fakes.
|
|
10
|
+
*
|
|
11
|
+
* ## Keying
|
|
12
|
+
*
|
|
13
|
+
* `computeGrantKey` (`@markii/runtime`) hashes the note's executable
|
|
14
|
+
* closure — that closure is its own script blocks
|
|
15
|
+
* (`RunRequirements.grantScripts`) plus, for a bundle-backed run, the
|
|
16
|
+
* resolved source text of every `src=`-referenced script file
|
|
17
|
+
* (`GrantFlowRequirements.bundleModules`, F-1 fix), with `vaultModules`/
|
|
18
|
+
* `packs` left empty: `require` (bundle-local or pack) stays unwired for
|
|
19
|
+
* the whole `.mkz` Run-path arc (GitHub issue #9's design comment), so a
|
|
20
|
+
* bundle-backed run's closure has no OTHER bundle-local code to fold in
|
|
21
|
+
* yet beyond its `src=` targets. Any edit to any script's code — inline or
|
|
22
|
+
* `src=`-referenced — changes the hash, which is what makes "any code
|
|
23
|
+
* change re-prompts" fall out of the keying scheme for free rather than
|
|
24
|
+
* needing its own diffing logic here.
|
|
25
|
+
*
|
|
26
|
+
* The bundle-fs capability (`permissions.bundle`) is out of this module
|
|
27
|
+
* entirely (consent unification, SECURITY-RELEVANT): it needs no
|
|
28
|
+
* user-facing prompt, so it is never part of `GrantFlowRequirements`, the
|
|
29
|
+
* stored grant, or the grant key. `./run-flow.ts` passes the manifest's
|
|
30
|
+
* declared bundle-fs grants straight through to `spawnRun` — see its own
|
|
31
|
+
* comment on why that is still safe (path-jail + the read-only tier for
|
|
32
|
+
* auto/scheduled, both unaffected by this module).
|
|
33
|
+
*
|
|
34
|
+
* ## Storage
|
|
35
|
+
*
|
|
36
|
+
* One `{key, allowedHosts}` record per document, keyed by the document's
|
|
37
|
+
* URI string, all held under one Memento key (`GRANTS_STORAGE_KEY`) as a
|
|
38
|
+
* plain map. A stored record whose `key` no longer matches the freshly
|
|
39
|
+
* computed one is a MISS — this module never trusts a stale record's
|
|
40
|
+
* `allowedHosts`, even partially; the whole grant re-derives from scratch.
|
|
41
|
+
* A record is only ever WRITTEN when `allowedHosts` ends up non-empty — a
|
|
42
|
+
* full decline is never persisted, so it simply re-prompts next time (see
|
|
43
|
+
* `runGrantFlow`'s own comment on this, C-3) — and `clearGrantForDocument`
|
|
44
|
+
* removes a document's record outright, for the `markii.resetScriptGrants`
|
|
45
|
+
* command.
|
|
46
|
+
*
|
|
47
|
+
* ## The prompt shapes
|
|
48
|
+
*
|
|
49
|
+
* - One prompt per statically-known host (`promptHost`), worded exactly
|
|
50
|
+
* "This note's scripts can send data to <host>. Allow?" — see
|
|
51
|
+
* `hostPromptMessage`. Used only while the distinct safe host count is at
|
|
52
|
+
* or under `MAX_HOST_PROMPTS`; above that, `promptManyHosts` (below) takes
|
|
53
|
+
* over instead so a note can never turn one Run press into a wall of
|
|
54
|
+
* modals (the PROMPT-STORM fix — see `MAX_HOST_PROMPTS`'s doc comment).
|
|
55
|
+
* - `promptManyHosts`, exactly once, in place of the whole per-host loop
|
|
56
|
+
* when there are more than `MAX_HOST_PROMPTS` distinct safe hosts: one
|
|
57
|
+
* all-or-nothing gate ("This note requests network access to many hosts
|
|
58
|
+
* (N). Allow all or deny all?", `manyHostsPromptMessage`) rather than N
|
|
59
|
+
* separate dialogs.
|
|
60
|
+
* - Exactly one extra prompt (`promptUnknownHosts`,
|
|
61
|
+
* `UNKNOWN_HOSTS_PROMPT_MESSAGE`) when the closure could reach hosts that
|
|
62
|
+
* cannot be listed in advance (`RunRequirements.hasUnknownHosts`, or a
|
|
63
|
+
* host string this module refuses to render — see `isSafeHostForPrompt`).
|
|
64
|
+
* Accepting it does NOT add anything to `allowedHosts` — there is no host
|
|
65
|
+
* name to grant — it is a consent gate: declining it clears every
|
|
66
|
+
* already-collected per-host answer too, since a script that can reach
|
|
67
|
+
* addresses it never named cannot be trusted to only use the hosts it DID
|
|
68
|
+
* name. A dynamically-built address a script hits at RUN TIME, whether or
|
|
69
|
+
* not this prompt was accepted, is never added to `allowedHosts` here (it
|
|
70
|
+
* is not known in advance); it fails at execution with the ordinary
|
|
71
|
+
* `'capability-denied'` kind, same as any other ungranted host.
|
|
72
|
+
*/
|
|
73
|
+
import { type GrantClosureScript } from '@markii/runtime';
|
|
74
|
+
/** The subset of `RunRequirements` (`./script-requirements.ts`) this flow needs — kept structural rather than importing the whole interface, so a fake in tests doesn't need to fabricate `scripts`. */
|
|
75
|
+
export interface GrantFlowRequirements {
|
|
76
|
+
readonly hosts: readonly string[];
|
|
77
|
+
readonly hasUnknownHosts: boolean;
|
|
78
|
+
readonly grantScripts: readonly GrantClosureScript[];
|
|
79
|
+
/**
|
|
80
|
+
* Bundle-relative `src=` script path -> source text, for every `src=`
|
|
81
|
+
* block among `grantScripts` whose file content the host has resolved
|
|
82
|
+
* (F-1 fix) — `./bundle-run.ts`'s `bundleModulesFromSnapshot`, run over
|
|
83
|
+
* the same in-memory bundle snapshot the run itself will use. Omitted or
|
|
84
|
+
* empty for a bare `.mk.md` document (which has no bundle to resolve
|
|
85
|
+
* `src=` targets from) or a bundle whose scripts have no `src=` blocks.
|
|
86
|
+
* Folded into `closureFrom`'s `GrantClosure.bundleModules` so
|
|
87
|
+
* `computeGrantKey` hashes the REFERENCED file's bytes, not just the
|
|
88
|
+
* (empty) `code` field a `src=` block carries in the note's own text —
|
|
89
|
+
* without this, swapping a bundle script's contents while the note text
|
|
90
|
+
* stays byte-identical would silently re-run new code under an old,
|
|
91
|
+
* un-reprompted grant.
|
|
92
|
+
*/
|
|
93
|
+
readonly bundleModules?: Readonly<Record<string, string>>;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The `Thenable` shape `vscode.Memento.update` actually returns — restated
|
|
97
|
+
* here (rather than importing `vscode`, which this file must never do) so
|
|
98
|
+
* a real `vscode.Memento` is assignable to `GrantMemento` with no adapter
|
|
99
|
+
* class, and a plain `Promise`-returning fake satisfies it in tests too.
|
|
100
|
+
*/
|
|
101
|
+
export interface Thenable<T> {
|
|
102
|
+
then<TResult1 = T, TResult2 = never>(onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | undefined | null): PromiseLike<TResult1 | TResult2>;
|
|
103
|
+
}
|
|
104
|
+
/** The exact shape of `vscode.Memento` this module uses — `context.workspaceState` satisfies this directly. */
|
|
105
|
+
export interface GrantMemento {
|
|
106
|
+
get<T>(key: string): T | undefined;
|
|
107
|
+
get<T>(key: string, defaultValue: T): T;
|
|
108
|
+
update(key: string, value: unknown): Thenable<void>;
|
|
109
|
+
}
|
|
110
|
+
/** Prompts once for `host`, resolving `true` for Allow, `false` for Don't allow (or the prompt being dismissed). */
|
|
111
|
+
export type PromptHost = (host: string, declaredHosts: readonly string[]) => Promise<boolean>;
|
|
112
|
+
/** Prompts once for the "this note builds a network address dynamically" consent gate. */
|
|
113
|
+
export type PromptUnknownHosts = () => Promise<boolean>;
|
|
114
|
+
/** Prompts once for the consolidated "many hosts" gate (PROMPT-STORM fix) — resolves `true` for Allow all, `false` for Deny all. */
|
|
115
|
+
export type PromptManyHosts = (hostCount: number) => Promise<boolean>;
|
|
116
|
+
export interface GrantFlowOptions {
|
|
117
|
+
/** Stable identity for the note this run belongs to — `vscode.Uri.toString()` of the document, in the real adapter. */
|
|
118
|
+
documentKey: string;
|
|
119
|
+
requirements: GrantFlowRequirements;
|
|
120
|
+
memento: GrantMemento;
|
|
121
|
+
promptHost: PromptHost;
|
|
122
|
+
/**
|
|
123
|
+
* Hostnames the note's own scripts declare through their `permissions`
|
|
124
|
+
* fence attribute, for `hostPromptMessage` to show beside the host being
|
|
125
|
+
* asked about. Display only: what is actually prompted for, and what can
|
|
126
|
+
* be granted, comes from `requirements.hosts` (the literal-URL scan) and
|
|
127
|
+
* nothing else.
|
|
128
|
+
*/
|
|
129
|
+
declaredHosts?: readonly string[];
|
|
130
|
+
promptUnknownHosts: PromptUnknownHosts;
|
|
131
|
+
/** Consolidated gate used instead of the per-host loop once the distinct static host count exceeds `MAX_HOST_PROMPTS` — see that constant's doc comment. */
|
|
132
|
+
promptManyHosts: PromptManyHosts;
|
|
133
|
+
}
|
|
134
|
+
export interface GrantFlowResult {
|
|
135
|
+
/** The hostnames this run's `net.*` calls may reach — exactly what `spawnRun`'s `netAllowlist` (`./run-host.ts`) expects. */
|
|
136
|
+
allowedHosts: string[];
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Exact prompt wording — normative per the locked design comment. Exported
|
|
140
|
+
* so a test (and the adapter that renders it) both anchor on the same
|
|
141
|
+
* string.
|
|
142
|
+
*
|
|
143
|
+
* `declaredHosts` is what the note's own scripts declare through their
|
|
144
|
+
* `permissions` fence attribute (`./script-requirements.ts`'s
|
|
145
|
+
* `scriptDeclaredHosts`). It is DISPLAY-ONLY: it never changes what is
|
|
146
|
+
* prompted for or granted, only what the user reads before answering, so
|
|
147
|
+
* a note cannot talk its way into a host the literal-URL scan did not
|
|
148
|
+
* find. `host` is always the one host this prompt asks about. With no
|
|
149
|
+
* declaration the wording is unchanged, so a host that does not pass the
|
|
150
|
+
* list sees exactly the message it saw before.
|
|
151
|
+
*/
|
|
152
|
+
export declare function hostPromptMessage(host: string, declaredHosts?: readonly string[]): string;
|
|
153
|
+
/** Exact wording of the one extra "hosts can't be listed in advance" prompt. */
|
|
154
|
+
export declare const UNKNOWN_HOSTS_PROMPT_MESSAGE = "This note builds network addresses at run time, which can't be listed in advance and will be denied; only the hosts written directly in the note can be granted. Allow those?";
|
|
155
|
+
/** Button labels every prompt in this flow uses — both a plain "Allow" and a "Don't allow" refusal are always offered, never a bare dismiss-only message. */
|
|
156
|
+
export declare const ALLOW_LABEL = "Allow";
|
|
157
|
+
export declare const DONT_ALLOW_LABEL = "Don't allow";
|
|
158
|
+
/**
|
|
159
|
+
* PROMPT-STORM fix (report section 8, item 6, docs/archive/PENTEST-REPORT-2026-08-23.md):
|
|
160
|
+
* `runGrantFlow`'s per-host loop below is sequential and modal, one dialog
|
|
161
|
+
* per statically-extracted host. A hostile note with hundreds of literal
|
|
162
|
+
* `net.fetch_json("https://hostN/")` calls would otherwise re-open hundreds
|
|
163
|
+
* of modals on EVERY `markii.runScripts` press — and because a FULL decline
|
|
164
|
+
* is deliberately never persisted (C-3, so one mis-click can't permanently
|
|
165
|
+
* lock out network access), nothing about answering them once makes them
|
|
166
|
+
* stop recurring on the next Run press either. That makes the per-host loop
|
|
167
|
+
* itself a denial-of-service surface against the user's own attention, not
|
|
168
|
+
* just an annoyance.
|
|
169
|
+
*
|
|
170
|
+
* The guard: cap how many per-host prompts one run will ever show. When the
|
|
171
|
+
* distinct SAFE static host count is at or under this cap, the ordinary
|
|
172
|
+
* per-host flow runs exactly as before (this is overwhelmingly the common
|
|
173
|
+
* case — most notes touch a small, fixed set of APIs). Above the cap, the
|
|
174
|
+
* per-host loop is skipped entirely in favor of ONE consolidated prompt
|
|
175
|
+
* (`promptManyHosts`) that grants either every one of those hosts or none of
|
|
176
|
+
* them — still a single, honest, all-or-nothing consent decision, just sized
|
|
177
|
+
* to the actual scale of what's being asked, rather than a wall of modals a
|
|
178
|
+
* user would rationally start rubber-stamping. 10 is comfortably above what
|
|
179
|
+
* a legitimate dashboard-style note (docs/scripting.md's callout on
|
|
180
|
+
* dashboards being a first-class use case) is expected to reach while still
|
|
181
|
+
* catching a hostile host list before it becomes hundreds of dialogs.
|
|
182
|
+
*/
|
|
183
|
+
export declare const MAX_HOST_PROMPTS = 10;
|
|
184
|
+
/** Exact wording of the consolidated "many hosts" gate — normative, mirroring `hostPromptMessage`/`UNKNOWN_HOSTS_PROMPT_MESSAGE`'s pattern of an exported, test-anchored string. */
|
|
185
|
+
export declare function manyHostsPromptMessage(hostCount: number): string;
|
|
186
|
+
export declare function isSafeHostForPrompt(host: string): boolean;
|
|
187
|
+
/**
|
|
188
|
+
* Runs the whole grant flow for one Run press: computes the closure's grant
|
|
189
|
+
* key, reuses a matching stored grant with NO prompting at all, or (on a
|
|
190
|
+
* miss — first run or any code change) prompts fresh for every host and the
|
|
191
|
+
* unknown-hosts gate, persists the result, and returns the allowlist
|
|
192
|
+
* `spawnRun` should use.
|
|
193
|
+
*/
|
|
194
|
+
export declare function runGrantFlow(options: GrantFlowOptions): Promise<GrantFlowResult>;
|
|
195
|
+
/**
|
|
196
|
+
* The NON-INTERACTIVE half of `runGrantFlow`, for auto/scheduled runs
|
|
197
|
+
* (GitHub issue #11): resolves this note's effective net + bundle-fs
|
|
198
|
+
* allowlist from ONLY what is already persisted, and NEVER prompts, NEVER
|
|
199
|
+
* writes. It is exactly `runGrantFlow`'s stored-grant fast path in
|
|
200
|
+
* isolation.
|
|
201
|
+
*
|
|
202
|
+
* This is the code half of the security promise auto/scheduled execution
|
|
203
|
+
* makes (docs/security.md, docs/scripting.md): a run that was not triggered
|
|
204
|
+
* by a click must never open a modal on a timer, and must never widen what
|
|
205
|
+
* the note can reach beyond a grant the user already made by hand under the
|
|
206
|
+
* exact same executable closure. On ANY miss — no stored grant, a grant for
|
|
207
|
+
* different code (the key no longer matches), or a stored host that fails
|
|
208
|
+
* today's `isSafeHostForPrompt` re-check (N-6) — this returns an EMPTY
|
|
209
|
+
* allowlist rather than falling back to prompting. An ungranted host then
|
|
210
|
+
* fails at execution with the ordinary `'capability-denied'` kind, exactly
|
|
211
|
+
* as it would on a manual run the user declined, and the consuming
|
|
212
|
+
* component shows its quiet stale/empty state.
|
|
213
|
+
*
|
|
214
|
+
* The read-only tier (`tierForTrigger('auto'|'scheduled')` in
|
|
215
|
+
* `@markii/runtime`) is a SEPARATE, independent gate applied in the worker;
|
|
216
|
+
* this function governs only which hosts are reachable, that one governs
|
|
217
|
+
* which capabilities (no POST/PATCH/bundle-write) may run at all.
|
|
218
|
+
*/
|
|
219
|
+
export declare function resolveStoredGrant(options: {
|
|
220
|
+
documentKey: string;
|
|
221
|
+
requirements: GrantFlowRequirements;
|
|
222
|
+
memento: GrantMemento;
|
|
223
|
+
}): Promise<GrantFlowResult>;
|
|
224
|
+
/**
|
|
225
|
+
* Clears the persisted grant (if any) for `documentKey` — the
|
|
226
|
+
* `markii.resetScriptGrants` command's whole implementation (C-3): with no
|
|
227
|
+
* stored record, `runGrantFlow`'s next call for this document is
|
|
228
|
+
* indistinguishable from a first run, so it prompts fresh for every host
|
|
229
|
+
* again. A no-op, not an error, when there is nothing stored for this
|
|
230
|
+
* document.
|
|
231
|
+
*/
|
|
232
|
+
export declare function clearGrantForDocument(memento: GrantMemento, documentKey: string): Promise<void>;
|