@lapxo/bound 0.1.8 → 0.2.0
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/README.md +56 -20
- package/TARGET.bound +70 -27
- package/assets/wasm/guest.js +4 -0
- package/assets/wasm/manifest.json +27 -0
- package/assets/wasm/runtime.js +6 -0
- package/assets/wasm/runtime.wasm +0 -0
- package/dist/cli/accept.js +83 -59
- package/dist/cli/act-result.d.ts +9 -0
- package/dist/cli/act-result.js +68 -13
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.js +5 -2
- package/dist/cli/custodian.d.ts +1 -0
- package/dist/cli/custodian.js +29 -0
- package/dist/cli/native-descendant.d.ts +1 -0
- package/dist/cli/native-descendant.js +19 -0
- package/dist/cli/native.d.ts +1 -0
- package/dist/cli/native.js +60 -0
- package/dist/cli/observe.js +9 -2
- package/dist/cli/pass.js +9 -4
- package/dist/cli/place.d.ts +2 -0
- package/dist/cli/place.js +7 -4
- package/dist/cli/standing-result.js +7 -26
- package/dist/cli/verb.js +65 -9
- package/dist/engine/index.d.ts +11 -0
- package/dist/engine/index.js +9 -0
- package/dist/engine/object.d.ts +17 -0
- package/dist/engine/object.js +38 -0
- package/dist/fold/admission-history.js +2 -2
- package/dist/fold/claims.d.ts +4 -0
- package/dist/fold/claims.js +23 -3
- package/dist/fold/closed.d.ts +1 -1
- package/dist/fold/closed.js +36 -7
- package/dist/fold/digests.d.ts +1 -1
- package/dist/fold/digests.js +6 -4
- package/dist/fold/evidence.d.ts +1 -1
- package/dist/fold/kept.js +2 -1
- package/dist/fold/object.d.ts +3 -17
- package/dist/fold/object.js +2 -38
- package/dist/fold/observed.d.ts +1 -0
- package/dist/fold/observed.js +54 -9
- package/dist/fold/readers.d.ts +4 -3
- package/dist/fold/readers.js +18 -13
- package/dist/fold/resolved.js +12 -2
- package/dist/fold/signers.js +2 -2
- package/dist/host/adapters/modules/javascript.d.ts +2 -0
- package/dist/host/adapters/modules/javascript.js +8 -6
- package/dist/host/admitted-place.d.ts +5 -0
- package/dist/host/admitted-place.js +57 -0
- package/dist/host/capsule-process.d.ts +1 -1
- package/dist/host/capsule-process.js +4 -1
- package/dist/host/capsule.d.ts +4 -0
- package/dist/host/capsule.js +16 -2
- package/dist/host/cell-receipts.js +1 -1
- package/dist/host/contextual-place.d.ts +59 -0
- package/dist/host/contextual-place.js +77 -0
- package/dist/host/hash.d.ts +7 -0
- package/dist/host/hash.js +6 -0
- package/dist/host/io.d.ts +2 -0
- package/dist/host/io.js +2 -0
- package/dist/host/lifetime.js +61 -22
- package/dist/host/native/bounded-file.d.ts +8 -0
- package/dist/host/native/bounded-file.js +32 -0
- package/dist/host/native/command.d.ts +36 -0
- package/dist/host/native/command.js +91 -0
- package/dist/host/native/control-bridge-worker.d.ts +1 -0
- package/dist/host/native/control-bridge-worker.js +88 -0
- package/dist/host/native/control-bridge.d.ts +30 -0
- package/dist/host/native/control-bridge.js +110 -0
- package/dist/host/native/control-selection.d.ts +29 -0
- package/dist/host/native/control-selection.js +32 -0
- package/dist/host/native/custody.d.ts +5 -0
- package/dist/host/native/custody.js +91 -0
- package/dist/host/native/deadline.d.ts +12 -0
- package/dist/host/native/deadline.js +28 -0
- package/dist/host/native/descendants.d.ts +1 -0
- package/dist/host/native/descendants.js +528 -0
- package/dist/host/native/generated-custodian.d.ts +122 -0
- package/dist/host/native/generated-custodian.js +267 -0
- package/dist/host/native/generated.d.ts +217 -0
- package/dist/host/native/generated.js +379 -0
- package/dist/host/native/image.d.ts +36 -0
- package/dist/host/native/image.js +45 -0
- package/dist/host/native/jobs.d.ts +75 -0
- package/dist/host/native/jobs.js +148 -0
- package/dist/host/native/loader.d.ts +80 -0
- package/dist/host/native/loader.js +269 -0
- package/dist/host/native/materialize.d.ts +22 -0
- package/dist/host/native/materialize.js +175 -0
- package/dist/host/native/module-closure.d.ts +27 -0
- package/dist/host/native/module-closure.js +99 -0
- package/dist/host/native/module-syntax.d.ts +8 -0
- package/dist/host/native/module-syntax.js +63 -0
- package/dist/host/native/native-binary.d.ts +54 -0
- package/dist/host/native/native-binary.js +115 -0
- package/dist/host/native/native-program.d.ts +56 -0
- package/dist/host/native/native-program.js +72 -0
- package/dist/host/native/node-jobs.d.ts +84 -0
- package/dist/host/native/node-jobs.js +187 -0
- package/dist/host/native/node-launch.d.ts +72 -0
- package/dist/host/native/node-launch.js +159 -0
- package/dist/host/native/preload.d.ts +1 -0
- package/dist/host/native/preload.js +45 -0
- package/dist/host/native/reading.d.ts +91 -0
- package/dist/host/native/reading.js +249 -0
- package/dist/host/native/receipt-environment.d.ts +3 -0
- package/dist/host/native/receipt-environment.js +41 -0
- package/dist/host/native/references.d.ts +5 -0
- package/dist/host/native/references.js +30 -0
- package/dist/host/native/root.d.ts +17 -0
- package/dist/host/native/root.js +37 -0
- package/dist/host/native/route.d.ts +66 -0
- package/dist/host/native/route.js +216 -0
- package/dist/host/native/runtime.d.ts +4 -0
- package/dist/host/native/runtime.js +9 -0
- package/dist/host/native-reader-context.d.ts +3 -0
- package/dist/host/native-reader-context.js +8 -0
- package/dist/host/objects.d.ts +2 -1
- package/dist/host/objects.js +24 -4
- package/dist/host/place-clock-selection.d.ts +55 -0
- package/dist/host/place-clock-selection.js +124 -0
- package/dist/host/ports/local-act.d.ts +10 -0
- package/dist/host/ports/local-act.js +78 -6
- package/dist/host/ports/place-act.d.ts +46 -0
- package/dist/host/ports/place-act.js +169 -0
- package/dist/host/process-lifetime.js +8 -3
- package/dist/host/provide.d.ts +25 -0
- package/dist/host/provide.js +115 -13
- package/dist/host/reading-closures.d.ts +17 -1
- package/dist/host/reading-closures.js +98 -18
- package/dist/host/reading-context.d.ts +29 -0
- package/dist/host/reading-context.js +53 -0
- package/dist/host/reference-authenticated.d.ts +28 -0
- package/dist/host/reference-authenticated.js +84 -0
- package/dist/host/reference-native.d.ts +10 -0
- package/dist/host/reference-native.js +50 -0
- package/dist/host/reference-profile.d.ts +23 -0
- package/dist/host/reference-profile.js +62 -0
- package/dist/host/reference.d.ts +6 -0
- package/dist/host/reference.js +24 -0
- package/dist/host/root-clock-source.d.ts +85 -0
- package/dist/host/root-clock-source.js +305 -0
- package/dist/host/selected-capsules.js +3 -2
- package/dist/host/selected-topos.d.ts +1 -1
- package/dist/host/selected-topos.js +9 -1
- package/dist/land/act.d.ts +26 -0
- package/dist/land/act.js +96 -3
- package/dist/land/ledger.d.ts +4 -0
- package/dist/land/ledger.js +20 -2
- package/dist/observe/regions.d.ts +4 -0
- package/dist/observe/regions.js +109 -1
- package/dist/observe/runner.d.ts +4 -0
- package/dist/observe/runner.js +9 -2
- package/dist/wasm/adapter.d.ts +51 -0
- package/dist/wasm/adapter.js +180 -0
- package/dist/wasm/node-worker.d.ts +1 -0
- package/dist/wasm/node-worker.js +13 -0
- package/dist/wasm/object.d.ts +8 -0
- package/dist/wasm/object.js +76 -0
- package/dist/wasm/worker.d.ts +1 -0
- package/dist/wasm/worker.js +7 -0
- package/docs/contracts/authenticated-reference.publication.json +25 -0
- package/docs/contracts/cli.md +13 -0
- package/docs/contracts/engine-object-projection.publication.json +25 -0
- package/docs/contracts/engine.md +39 -0
- package/docs/contracts/native-publication.inventory.json +8 -0
- package/docs/contracts/optional-wasm-adapter.publication.json +25 -0
- package/docs/contracts/publication-additions.inventory.json +8 -0
- package/docs/contracts/reference-port.md +33 -0
- package/docs/contracts/reference-port.publication.json +25 -0
- package/licenses/wasm/bound.txt +21 -0
- package/licenses/wasm/fast-text-encoding.txt +201 -0
- package/licenses/wasm/jitl-quickjs-ffi-types.txt +21 -0
- package/licenses/wasm/jitl-quickjs-wasmfile-release-sync.txt +47 -0
- package/licenses/wasm/obligations.txt +21 -0
- package/licenses/wasm/quickjs-emscripten-core.txt +21 -0
- package/licenses/wasm/topos.txt +21 -0
- package/package.json +94 -4
- package/receipts.bound +7672 -573
- package/samples/authenticated-reference/no.json +31 -0
- package/samples/authenticated-reference/yes/editorial-state/TARGET.bound +57 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/arrival.json +1 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/coverage.json +1 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/era.json +1 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/order.json +1 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/signature.json +1 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/transport.arrival.json +1 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/transport.coverage.json +1 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/transport.era.json +1 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/transport.order.json +1 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/transport.signature.json +1 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/transport.yes.json +1 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/yes.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/TARGET.bound +57 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/arrival.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/coverage.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/era.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/order.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/signature.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/transport.arrival.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/transport.coverage.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/transport.era.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/transport.order.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/transport.signature.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/transport.yes.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/yes.json +1 -0
- package/samples/authenticated-reference/yes.json +19 -0
- package/samples/engine-alphabet/TARGET.bound +29 -0
- package/samples/engine-alphabet/inputs/sample.json +323 -0
- package/samples/engine-alphabet/package.json +1 -0
- package/samples/engine-alphabet/reader.js +1457 -0
- package/samples/engine-alphabet/receipts.bound +12 -0
- package/samples/engine-interval/TARGET.bound +29 -0
- package/samples/engine-interval/inputs/sample.json +277 -0
- package/samples/engine-interval/package.json +1 -0
- package/samples/engine-interval/reader.js +1457 -0
- package/samples/engine-interval/receipts.bound +12 -0
- package/samples/no/engine-order/TARGET.bound +29 -0
- package/samples/no/engine-order/inputs/sample.json +277 -0
- package/samples/no/engine-order/package.json +1 -0
- package/samples/no/engine-order/reader.js +1457 -0
- package/samples/reference-port/no/catalog/standing.json +1 -0
- package/samples/reference-port/no/catalog/transport.json +1 -0
- package/samples/reference-port/no/route-notes/standing.json +1 -0
- package/samples/reference-port/no/route-notes/transport.json +1 -0
- package/samples/reference-port/yes/catalog/TARGET.bound +33 -0
- package/samples/reference-port/yes/catalog/standing.json +1 -0
- package/samples/reference-port/yes/catalog/transport.json +1 -0
- package/samples/reference-port/yes/route-notes/TARGET.bound +33 -0
- package/samples/reference-port/yes/route-notes/standing.json +1 -0
- package/samples/reference-port/yes/route-notes/transport.json +1 -0
- package/samples/wasm/editorial-notes/TARGET.bound +65 -0
- package/samples/wasm/editorial-notes/esm.bound +59 -0
- package/samples/wasm/editorial-notes/no-deadline.bound +65 -0
- package/samples/wasm/editorial-notes/no-memory.bound +65 -0
- package/samples/wasm/editorial-notes/no-missing-limit.bound +64 -0
- package/samples/wasm/editorial-notes/receipts.bound +1 -0
- package/samples/wasm/water-level/TARGET.bound +65 -0
- package/samples/wasm/water-level/esm.bound +59 -0
- package/samples/wasm/water-level/no-deadline.bound +65 -0
- package/samples/wasm/water-level/no-memory.bound +65 -0
- package/samples/wasm/water-level/no-missing-limit.bound +64 -0
- package/samples/wasm/water-level/receipts.bound +1 -0
- package/vectors/engine-object.cli.json +548 -0
- package/vectors/native/authenticated-reference-formation.vector.json +53 -0
- package/vectors/native/authenticated-reference.cli.json +157 -0
- package/vectors/native/authenticated-reference.vector.json +440 -0
- package/vectors/reference-port.cli.json +688 -0
- package/vectors/wasm-object.cli.json +452 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { isSha256 } from "../hash.js";
|
|
2
|
+
import { sha256 as sha } from "../hash.js";
|
|
3
|
+
import { nodeLaunch } from "./node-launch.js";
|
|
4
|
+
/** Content producer for a separately Root-authorized finite native program.
|
|
5
|
+
* Outputs are provenance, not grants. No arbitrary-JS confinement is claimed. */
|
|
6
|
+
import { openSync, fstatSync, readSync, closeSync, realpathSync } from 'node:fs';
|
|
7
|
+
import { resolve, dirname, isAbsolute } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { builtinModules, createRequire } from 'node:module';
|
|
10
|
+
import { moduleImportSyntax } from "./module-syntax.js";
|
|
11
|
+
import { authenticateSignedPort, canonicalJson } from "./reading.js";
|
|
12
|
+
const fail = (why) => { throw Error('REFUSE·producer ' + why); };
|
|
13
|
+
const digest = isSha256;
|
|
14
|
+
const contexts = new WeakMap();
|
|
15
|
+
function state(handle) { const s = contexts.get(handle); if (!s)
|
|
16
|
+
return fail('unknown Root context'); return s; }
|
|
17
|
+
function charge(s, work, bytes = 0, members = 0) { if (s.work + work > s.profile.limits.work)
|
|
18
|
+
fail('work exhausted'); if (s.bytes + bytes > s.profile.limits.bytes)
|
|
19
|
+
fail('derived bytes exhausted'); if (s.members + members > s.profile.limits.members)
|
|
20
|
+
fail('derived members exhausted'); s.work += work; s.bytes += bytes; s.members += members; }
|
|
21
|
+
/** Call inside the parent's real authority guard BEFORE emission/work. Signature
|
|
22
|
+
* and current eligibility are shared with the Root port constructor, unchanged. */
|
|
23
|
+
export function createGeneratedProducer(root, coordinate, parent, implementationBytes) {
|
|
24
|
+
const auth = authenticateSignedPort(root, coordinate), profile = auth.profile;
|
|
25
|
+
if (!profile || Object.keys(profile).sort().join(',') !== 'coordinate,environmentNames,implementation,interpretations,limits,profile,selector' || profile.profile !== 'host-generated-executable@3' || profile.coordinate !== coordinate || Object.keys(profile.limits ?? {}).sort().join(',') !== 'argvBytes,argvItems,bytes,members,work' || Object.values(profile.limits).some(v => !Number.isSafeInteger(v) || v < 0))
|
|
26
|
+
fail('capability/budget absent or unknown');
|
|
27
|
+
if (!Array.isArray(profile.interpretations) || profile.interpretations.length > 3 || profile.interpretations.some(p => !['node-package-json@1', 'node-ambiguous-javascript@1', 'host-file-emission@1'].includes(p)) || new Set(profile.interpretations).size !== profile.interpretations.length)
|
|
28
|
+
fail('interpretation declaration absent/ambiguous');
|
|
29
|
+
if (!Array.isArray(profile.environmentNames) || profile.environmentNames.length > profile.limits.argvItems || profile.environmentNames.some(n => typeof n !== 'string' || !n.length || n.length > profile.limits.argvBytes || /[=*\0]/.test(n)) || new Set(profile.environmentNames).size !== profile.environmentNames.length || Buffer.byteLength(canonicalJson(profile.environmentNames)) > profile.limits.argvBytes)
|
|
30
|
+
fail('derived environment declaration absent/ambiguous/over budget');
|
|
31
|
+
if (Object.keys(profile.selector ?? {}).sort().join(',') !== 'binding,implementation,port' || canonicalJson(profile.selector) !== canonicalJson({ port: parent.port, binding: parent.binding, implementation: parent.implementation }))
|
|
32
|
+
fail('producer selector differs from parent');
|
|
33
|
+
if (!digest(parent.identity) || !digest(parent.instrument) || !digest(parent.policy) || !digest(parent.authority) || !digest(parent.implementation) || !isAbsolute(parent.cwd) || !isAbsolute(parent.runtime) || !Number.isSafeInteger(parent.timeoutMs) || parent.timeoutMs < 1 || !Number.isSafeInteger(parent.responseBytes) || parent.responseBytes < 1)
|
|
34
|
+
fail('parent context unknown');
|
|
35
|
+
if (!Array.isArray(parent.argv) || !Array.isArray(parent.inputs) || !Array.isArray(parent.environment) || [parent.argv, parent.inputs, parent.environment].some(a => a.length > root.budget.bytes))
|
|
36
|
+
fail('parent metadata budget');
|
|
37
|
+
if (profile.limits.bytes > parent.responseBytes || profile.limits.argvBytes > parent.responseBytes || implementationBytes.byteLength > root.budget.bytes)
|
|
38
|
+
fail('producer exceeds parent byte resource');
|
|
39
|
+
if (sha(implementationBytes) !== profile.implementation)
|
|
40
|
+
fail('producer implementation bytes differ');
|
|
41
|
+
if (parent.argv.length > profile.limits.argvItems || parent.argv.some(x => typeof x !== 'string' || x.length > profile.limits.argvBytes) || Buffer.byteLength(canonicalJson(parent.argv)) > profile.limits.argvBytes || parent.inputs.some(p => p.length !== 2 || typeof p[0] !== 'string' || !digest(p[1])) || new Set(parent.inputs.map(p => p[0])).size !== parent.inputs.length || parent.environment.some(p => p.length !== 2 || typeof p[0] !== 'string' || typeof p[1] !== 'string') || new Set(parent.environment.map(p => p[0])).size !== parent.environment.length)
|
|
42
|
+
fail('parent inputs/argv/environment differ');
|
|
43
|
+
const copy = JSON.parse(canonicalJson(parent)), handle = Object.freeze({ identity: sha(canonicalJson([auth.line, copy])) });
|
|
44
|
+
contexts.set(handle, { candidates: new Map(), emissionInvocations: new Map(), emissions: new Map(), emissionDigests: new Map(), profile, parent: copy, members: 0, bytes: 0, work: 0, recordBytes: 0, retained: new Map(), executableDigests: new Set(), paths: new Map(), records: new Map(), interpretations: new Map() });
|
|
45
|
+
return handle;
|
|
46
|
+
}
|
|
47
|
+
function checkedInvocation(s, input) { if (input && Array.isArray(input.environment)) {
|
|
48
|
+
const unknown = input.environment.filter(p => Array.isArray(p) && typeof p[0] === 'string' && !s.parent.environment.some(q => q[0] === p[0]) && !s.profile.environmentNames.includes(p[0])).map(p => p[0]);
|
|
49
|
+
if (unknown.length)
|
|
50
|
+
fail('descendant environment undeclared ' + unknown.slice(0, 16).join(','));
|
|
51
|
+
} if (input?.runtime !== s.parent.runtime)
|
|
52
|
+
fail('descendant runtime differs'); if (!Number.isSafeInteger(input?.timeoutMs) || input.timeoutMs < 1 || input.timeoutMs > s.parent.timeoutMs)
|
|
53
|
+
fail('descendant time outside parent'); if (!Number.isSafeInteger(input?.responseBytes) || input.responseBytes < 1 || input.responseBytes > s.parent.responseBytes)
|
|
54
|
+
fail('descendant response bytes outside parent'); if (!input || typeof input.cwd !== 'string' || !Array.isArray(input.argv) || !Array.isArray(input.environment) || input.environment.length > s.profile.limits.argvItems || input.argv.some(x => typeof x !== 'string' || x.length > s.profile.limits.argvBytes) || input.environment.some(p => !Array.isArray(p) || p.length !== 2 || p.some(x => typeof x !== 'string' || x.length > s.profile.limits.argvBytes)) || !isAbsolute(input.cwd) || input.runtime !== s.parent.runtime || !Number.isSafeInteger(input.timeoutMs) || input.timeoutMs < 1 || input.timeoutMs > s.parent.timeoutMs || !Number.isSafeInteger(input.responseBytes) || input.responseBytes < 1 || input.responseBytes > s.parent.responseBytes || input.argv.length > s.profile.limits.argvItems || Buffer.byteLength(canonicalJson([input.argv, input.environment])) > s.profile.limits.argvBytes || input.argv.some(x => typeof x !== 'string') || input.environment.some(p => p.length !== 2 || typeof p[0] !== 'string' || typeof p[1] !== 'string') || new Set(input.environment.map(p => p[0])).size !== input.environment.length || input.environment.some(p => !s.parent.environment.some(q => q[0] === p[0]) && !s.profile.environmentNames.includes(p[0])))
|
|
55
|
+
fail('descendant invocation context missing/over budget'); return JSON.parse(canonicalJson(input)); }
|
|
56
|
+
export function validateDescendantInvocation(handle, input) { return checkedInvocation(state(handle), input); }
|
|
57
|
+
export function producerTransport(handle) { const s = state(handle); return Object.freeze({ coordinate: s.profile.coordinate, implementation: s.profile.implementation, identity: handle.identity, parent: s.parent.identity, timeoutMs: s.parent.timeoutMs, responseBytes: Math.min(s.parent.responseBytes, s.profile.limits.argvBytes), limits: { ...s.profile.limits }, interpretations: [...s.profile.interpretations] }); }
|
|
58
|
+
function physical(path) { try {
|
|
59
|
+
return realpathSync(path);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return fail('member path absent ' + path);
|
|
63
|
+
} }
|
|
64
|
+
export function readBounded(path, limit) { let fd; try {
|
|
65
|
+
fd = openSync(path, 'r');
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return fail('executable member absent ' + path);
|
|
69
|
+
} try {
|
|
70
|
+
const stat = fstatSync(fd);
|
|
71
|
+
if (!stat.isFile() || stat.size > limit)
|
|
72
|
+
fail('member exceeds remaining bytes ' + path);
|
|
73
|
+
const bytes = Buffer.alloc(stat.size), tail = Buffer.alloc(1);
|
|
74
|
+
let got = 0;
|
|
75
|
+
while (got < bytes.length) {
|
|
76
|
+
const n = readSync(fd, bytes, got, bytes.length - got, got);
|
|
77
|
+
if (!n)
|
|
78
|
+
break;
|
|
79
|
+
got += n;
|
|
80
|
+
}
|
|
81
|
+
if (got !== bytes.length || readSync(fd, tail, 0, 1, bytes.length) !== 0)
|
|
82
|
+
fail('member changed during bounded read ' + path);
|
|
83
|
+
return bytes;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
closeSync(fd);
|
|
87
|
+
} }
|
|
88
|
+
function remember(s, handle, body) { const serialized = canonicalJson([s.parent.identity, handle.identity, body]), identity = sha(serialized), held = s.records.get(identity); if (held)
|
|
89
|
+
return held; const size = Buffer.byteLength(serialized); if (size > s.parent.responseBytes - s.recordBytes)
|
|
90
|
+
fail('retained context bytes exhausted'); const record = { kind: 'derived-executable/1', parent: s.parent.identity, producer: handle.identity, ...body, identity: sha(serialized) }; s.recordBytes += size; s.records.set(record.identity, record); return Object.freeze(record); }
|
|
91
|
+
export function assertProducerInterpretation(handle, name) { if (!state(handle).profile.interpretations.includes(name))
|
|
92
|
+
fail('interpretation not declared ' + name); }
|
|
93
|
+
/** Read work is paid each time; retained code bytes and member count are paid
|
|
94
|
+
* once by digest. An alias earns no new origin and still needs actual rehash. */
|
|
95
|
+
function retainedFile(s, path, executable) {
|
|
96
|
+
if (executable && s.members >= s.profile.limits.members && !s.executableDigests.size)
|
|
97
|
+
fail('derived members exhausted');
|
|
98
|
+
charge(s, 1);
|
|
99
|
+
const actual = physical(path), held = s.paths.get(actual), remaining = s.profile.limits.bytes - s.bytes;
|
|
100
|
+
// Before allocation, bound verification by an existing retained length or
|
|
101
|
+
// the remaining new-content allowance. No unknown-size/read fallback.
|
|
102
|
+
const maxRetained = Math.max(0, ...s.retained.values()), bytes = readBounded(actual, held?.bytes ?? Math.max(remaining, maxRetained)), digest = sha(bytes);
|
|
103
|
+
charge(s, 1);
|
|
104
|
+
if (held && (held.digest !== digest || held.bytes !== bytes.length))
|
|
105
|
+
fail('retained executable bytes differ ' + actual);
|
|
106
|
+
retainContent(s, digest, bytes.length, executable);
|
|
107
|
+
s.paths.set(actual, { digest, bytes: bytes.length });
|
|
108
|
+
return { actual, bytes, digest };
|
|
109
|
+
}
|
|
110
|
+
function retainContent(s, digest, bytes, executable) { const newBytes = s.retained.has(digest) ? 0 : bytes, newMember = executable && !s.executableDigests.has(digest) ? 1 : 0; charge(s, 0, newBytes, newMember); if (!s.retained.has(digest))
|
|
111
|
+
s.retained.set(digest, bytes); if (executable)
|
|
112
|
+
s.executableDigests.add(digest); }
|
|
113
|
+
export function sealGeneratedFile(handle, path, mode, invocation, interpretation) { const s = state(handle); if (interpretation !== undefined) {
|
|
114
|
+
if (interpretation !== 'node-ambiguous-javascript@1')
|
|
115
|
+
fail('executable interpretation unknown');
|
|
116
|
+
assertProducerInterpretation(handle, interpretation);
|
|
117
|
+
} if (!isAbsolute(path) || !['module', 'commonjs'].includes(mode))
|
|
118
|
+
fail('unknown executable file/mode'); const checked = checkedInvocation(s, invocation), held = retainedFile(s, path, true); return remember(s, handle, { target: { kind: 'file', path: held.actual }, mode, ...(interpretation ? { interpretation } : {}), invocation: checked, digest: held.digest, byteLength: held.bytes.byteLength }); }
|
|
119
|
+
/** Preserve the four consumed native eval forms exactly; no rewriting/default
|
|
120
|
+
* flags/option blacklist. Prefix is the exact Root-selected inherited preload. */
|
|
121
|
+
export function sealNativeEval(handle, actualArgv, rootPrefix, invocation) { const s = state(handle); const checked = checkedInvocation(s, invocation); charge(s, 1); if (actualArgv.length > s.profile.limits.argvItems || Buffer.byteLength(canonicalJson(actualArgv)) > s.profile.limits.argvBytes || actualArgv.some(x => typeof x !== 'string') || rootPrefix.some((x, i) => actualArgv[i] !== x))
|
|
122
|
+
fail('eval argv exceeds/changes Root binding'); const argv = actualArgv.slice(rootPrefix.length); const launch = nodeLaunch(argv); if (launch.kind !== 'eval' || launch.imports.length)
|
|
123
|
+
return fail('unsupported native eval argv/preload'); const mode = launch.mode, text = launch.text; if (Buffer.from(text, 'utf8').toString('utf8') !== text)
|
|
124
|
+
fail('eval text is not scalar UTF-8'); const bytes = Buffer.from(text, 'utf8'); charge(s, 1); retainContent(s, sha(bytes), bytes.length, true); return remember(s, handle, { target: { kind: 'eval', cwd: checked.cwd, argv: [...actualArgv], text }, mode, invocation: checked, digest: sha(bytes), byteLength: bytes.length }); }
|
|
125
|
+
/** Interpretive data is provenance, never an executable or an authority grant.
|
|
126
|
+
* The finite interpretation is Root-signed before bounded IO/JSON work. */
|
|
127
|
+
export function sealInterpretation(handle, path, interpretation, invocation) { const s = state(handle); if (!isAbsolute(path) || !s.profile.interpretations.includes(interpretation) || interpretation !== 'node-package-json@1')
|
|
128
|
+
fail('interpretation not declared'); const checked = checkedInvocation(s, invocation); charge(s, 1); const actual = physical(path), bytes = readBounded(actual, Math.max(s.profile.limits.bytes - s.bytes, ...s.retained.values(), 0)); charge(s, 1); const candidate = s.candidates.get(actual); if (candidate?.reason)
|
|
129
|
+
fail('required emission unmeasured ' + actual + ' ' + candidate.reason); if (candidate) {
|
|
130
|
+
if (candidate.digest !== sha(bytes))
|
|
131
|
+
fail('observed emission bytes differ ' + actual);
|
|
132
|
+
if (!s.emissionDigests.has(candidate.digest))
|
|
133
|
+
recordProducerEmissions(handle, [{ path: actual, digest: candidate.digest }], candidate.invocation);
|
|
134
|
+
} let json; try {
|
|
135
|
+
json = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return fail('package interpretation JSON invalid');
|
|
139
|
+
} if (!json || typeof json !== 'object' || Array.isArray(json) || ('type' in json && json.type !== 'module' && json.type !== 'commonjs'))
|
|
140
|
+
fail('package interpretation type invalid'); retainContent(s, sha(bytes), bytes.byteLength, false); const body = { path: actual, digest: sha(bytes), interpretation, invocation: checked, byteLength: bytes.byteLength }, serialized = canonicalJson([s.parent.identity, handle.identity, body]), identity = sha(serialized), held = s.interpretations.get(identity); if (held)
|
|
141
|
+
return held; const size = Buffer.byteLength(serialized); if (size > s.parent.responseBytes - s.recordBytes)
|
|
142
|
+
fail('retained context bytes exhausted'); const record = { kind: 'derived-interpretation/1', parent: s.parent.identity, producer: handle.identity, ...body, identity: sha(serialized) }; s.recordBytes += size; s.interpretations.set(record.identity, record); return Object.freeze(record); }
|
|
143
|
+
/** Metadata from the fixed observer is only a candidate. No file is opened,
|
|
144
|
+
* no emission accepted, and no execution permission follows from registration. */
|
|
145
|
+
export function registerProducerCandidates(handle, groups) {
|
|
146
|
+
const s = state(handle);
|
|
147
|
+
assertProducerInterpretation(handle, 'host-file-emission@1');
|
|
148
|
+
if (!Array.isArray(groups))
|
|
149
|
+
fail('candidate inventory absent');
|
|
150
|
+
const updates = new Map(), invocations = new Map();
|
|
151
|
+
let bytes = s.recordBytes, work = 0;
|
|
152
|
+
for (const group of groups) {
|
|
153
|
+
const call = checkedInvocation(s, group.invocation), text = canonicalJson(call), id = sha(text);
|
|
154
|
+
if (!Array.isArray(group.outputs))
|
|
155
|
+
fail('candidate outputs absent');
|
|
156
|
+
const existing = s.emissionInvocations.get(id) ?? invocations.get(id), invocation = existing ?? Object.freeze(call);
|
|
157
|
+
if (!existing) {
|
|
158
|
+
bytes += Buffer.byteLength(text);
|
|
159
|
+
invocations.set(id, invocation);
|
|
160
|
+
}
|
|
161
|
+
for (const o of group.outputs) {
|
|
162
|
+
if (!o || typeof o.path !== 'string' || !isAbsolute(o.path) || updates.has(o.path) || !((digest(o.digest) && o.reason === undefined) || (o.digest === undefined && typeof o.reason === 'string' && o.reason.length > 0)))
|
|
163
|
+
fail('candidate output absent/ambiguous');
|
|
164
|
+
const size = Buffer.byteLength(canonicalJson([o.path, o.digest ?? null, o.reason ?? null, id])), prior = s.candidates.get(o.path);
|
|
165
|
+
bytes += size - (prior?.size ?? 0);
|
|
166
|
+
updates.set(o.path, { path: o.path, ...(o.digest === undefined ? { reason: o.reason } : { digest: o.digest }), invocation, size });
|
|
167
|
+
work++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (bytes > s.parent.responseBytes)
|
|
171
|
+
fail('retained candidate bytes exhausted');
|
|
172
|
+
charge(s, work);
|
|
173
|
+
s.recordBytes = bytes;
|
|
174
|
+
for (const [id, call] of invocations)
|
|
175
|
+
s.emissionInvocations.set(id, call);
|
|
176
|
+
for (const [path, row] of updates)
|
|
177
|
+
s.candidates.set(path, row);
|
|
178
|
+
}
|
|
179
|
+
/** Root-selected write observer records output provenance, not execution
|
|
180
|
+
* authority. Bytes are bounded/rechecked by the same actual producer custody. */
|
|
181
|
+
function emissionRecord(s, handle, actual, digest, byteLength, checked) {
|
|
182
|
+
const invocationText = canonicalJson(checked), invocationIdentity = sha(invocationText), existing = s.emissionInvocations.get(invocationIdentity), invocation = existing ?? Object.freeze(checked);
|
|
183
|
+
const body = { path: actual, digest, invocationIdentity, byteLength }, serialized = canonicalJson([s.parent.identity, handle.identity, body]), identity = sha(serialized), held = s.emissions.get(actual);
|
|
184
|
+
if (held?.identity === identity)
|
|
185
|
+
return held;
|
|
186
|
+
const size = Buffer.byteLength(serialized) + (existing ? 0 : Buffer.byteLength(invocationText));
|
|
187
|
+
if (size > s.parent.responseBytes - s.recordBytes)
|
|
188
|
+
fail('retained context bytes exhausted');
|
|
189
|
+
const record = Object.freeze({ kind: 'producer-emission/1', parent: s.parent.identity, producer: handle.identity, ...body, invocation, identity });
|
|
190
|
+
s.recordBytes += size;
|
|
191
|
+
if (!existing)
|
|
192
|
+
s.emissionInvocations.set(invocationIdentity, invocation);
|
|
193
|
+
s.emissions.set(actual, record);
|
|
194
|
+
s.emissionDigests.set(digest, record);
|
|
195
|
+
return record;
|
|
196
|
+
}
|
|
197
|
+
export function recordProducerEmission(handle, path, invocation) {
|
|
198
|
+
const s = state(handle);
|
|
199
|
+
assertProducerInterpretation(handle, 'host-file-emission@1');
|
|
200
|
+
if (!isAbsolute(path))
|
|
201
|
+
fail('emission path absent');
|
|
202
|
+
const checked = checkedInvocation(s, invocation);
|
|
203
|
+
charge(s, 1);
|
|
204
|
+
const actual = physical(path), bytes = readBounded(actual, s.parent.responseBytes), digest = sha(bytes);
|
|
205
|
+
charge(s, 1);
|
|
206
|
+
return emissionRecord(s, handle, actual, digest, bytes.length, checked);
|
|
207
|
+
}
|
|
208
|
+
/** All observed outputs must match before any provenance is accepted. One
|
|
209
|
+
* invocation context is committed once, with no code grant or quota widening. */
|
|
210
|
+
export function recordProducerEmissions(handle, outputs, invocation) {
|
|
211
|
+
const s = state(handle);
|
|
212
|
+
assertProducerInterpretation(handle, 'host-file-emission@1');
|
|
213
|
+
const checked = checkedInvocation(s, invocation);
|
|
214
|
+
if (!Array.isArray(outputs) || !outputs.length || outputs.some(o => !o || typeof o.path !== 'string' || !isAbsolute(o.path) || !digest(o.digest)) || new Set(outputs.map(o => o.path)).size !== outputs.length)
|
|
215
|
+
fail('emission batch absent/ambiguous');
|
|
216
|
+
charge(s, 1 + outputs.length * 2);
|
|
217
|
+
const verified = outputs.map(o => { const path = physical(o.path), bytes = readBounded(path, s.parent.responseBytes); if (sha(bytes) !== o.digest)
|
|
218
|
+
fail('observed emission bytes differ ' + path); return { path, digest: o.digest, bytes: bytes.length }; });
|
|
219
|
+
if (new Set(verified.map(o => o.path)).size !== verified.length)
|
|
220
|
+
fail('emission batch physical coordinates ambiguous');
|
|
221
|
+
// Preflight total retained context resource; transaction has no partial green.
|
|
222
|
+
const invocationText = canonicalJson(checked), invocationIdentity = sha(invocationText);
|
|
223
|
+
let size = s.emissionInvocations.has(invocationIdentity) ? 0 : Buffer.byteLength(invocationText);
|
|
224
|
+
for (const o of verified) {
|
|
225
|
+
const body = { path: o.path, digest: o.digest, invocationIdentity, byteLength: o.bytes }, serialized = canonicalJson([s.parent.identity, handle.identity, body]);
|
|
226
|
+
if (s.emissions.get(o.path)?.identity !== sha(serialized))
|
|
227
|
+
size += Buffer.byteLength(serialized);
|
|
228
|
+
}
|
|
229
|
+
if (size > s.parent.responseBytes - s.recordBytes)
|
|
230
|
+
fail('retained context bytes exhausted');
|
|
231
|
+
const records = verified.map(o => emissionRecord(s, handle, o.path, o.digest, o.bytes, checked)), rows = records.map(({ path, digest, identity, byteLength }) => ({ path, digest, identity, byteLength })), body = { kind: 'producer-emission-batch/1', parent: s.parent.identity, producer: handle.identity, invocation: checked, invocationIdentity, outputs: rows };
|
|
232
|
+
return { ...body, identity: sha(canonicalJson(body)) };
|
|
233
|
+
}
|
|
234
|
+
/** Verify the ACTUAL body/argv again before execution. A record's existence
|
|
235
|
+
* alone authorizes nothing and cannot be moved to another parent context. */
|
|
236
|
+
export function verifyDerived(handle, record, actual) { const s = state(handle); charge(s, 1); const held = s.records.get(record.identity); if (!held || canonicalJson(held) !== canonicalJson(record) || record.parent !== s.parent.identity || record.producer !== handle.identity)
|
|
237
|
+
fail('unknown/substituted derived context'); let bytes; if (record.target.kind === 'file') {
|
|
238
|
+
if (!actual.path || physical(actual.path) !== record.target.path)
|
|
239
|
+
fail('derived target route differs');
|
|
240
|
+
bytes = readBounded(record.target.path, record.byteLength);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
if (!actual.argv || canonicalJson(actual.argv) !== canonicalJson(record.target.argv))
|
|
244
|
+
fail('actual eval argv differs');
|
|
245
|
+
bytes = Buffer.from(record.target.text, 'utf8');
|
|
246
|
+
} if (sha(bytes) !== record.digest || bytes.byteLength !== record.byteLength)
|
|
247
|
+
fail('derived executable bytes differ'); return bytes; }
|
|
248
|
+
/** Literal closure against Root-sealed targets/libraries BEFORE entry. No
|
|
249
|
+
* resolver or verifier is selected by generated code. Unknown computed imports
|
|
250
|
+
* or escaped require remain named refusals, not an empty graph. Root's actual
|
|
251
|
+
* runtime hook must still verify its resolved target before handing it back. */
|
|
252
|
+
/** The descriptor is fixed once per actual producer. Frozen descriptor metadata
|
|
253
|
+
* cannot silently change between entries; actual target bytes are never cached. */
|
|
254
|
+
function snapshotIndex(s, snapshot, nativeImplementations) {
|
|
255
|
+
if (s.index) {
|
|
256
|
+
if (s.index.snapshot !== snapshot || nativeImplementations.size !== s.index.nativeImplementations.size || [...nativeImplementations].some(d => !s.index.nativeImplementations.has(d)))
|
|
257
|
+
fail('Root snapshot changed within producer');
|
|
258
|
+
return s.index;
|
|
259
|
+
}
|
|
260
|
+
if (!digest(snapshot.platform) || !Number.isSafeInteger(snapshot.budget?.members) || snapshot.budget.members < 1 || !Number.isSafeInteger(snapshot.budget.bytes) || snapshot.budget.bytes < 1 || snapshot.members.length > snapshot.budget.members || snapshot.libraries.length > snapshot.budget.members)
|
|
261
|
+
fail('literal snapshot budget/context absent');
|
|
262
|
+
const files = new Map(), digests = new Set();
|
|
263
|
+
for (const m of snapshot.members) {
|
|
264
|
+
charge(s, 1);
|
|
265
|
+
if (!digest(m.digest))
|
|
266
|
+
fail('Root member digest absent');
|
|
267
|
+
const path = physical(m.path);
|
|
268
|
+
if (files.has(path))
|
|
269
|
+
fail('ambiguous Root member');
|
|
270
|
+
files.set(path, m.digest);
|
|
271
|
+
digests.add(m.digest);
|
|
272
|
+
Object.freeze(m);
|
|
273
|
+
}
|
|
274
|
+
if ([...nativeImplementations].some(d => !digests.has(d)))
|
|
275
|
+
fail('native implementation outside authenticated snapshot');
|
|
276
|
+
const libs = new Map();
|
|
277
|
+
for (const l of snapshot.libraries) {
|
|
278
|
+
charge(s, 1);
|
|
279
|
+
if (libs.has(l.name))
|
|
280
|
+
fail('ambiguous Root library');
|
|
281
|
+
libs.set(l.name, l.manifest);
|
|
282
|
+
Object.freeze(l);
|
|
283
|
+
}
|
|
284
|
+
const native = new Set();
|
|
285
|
+
for (const n of snapshot.builtins) {
|
|
286
|
+
charge(s, 1);
|
|
287
|
+
native.add(n.startsWith('node:') ? n : 'node:' + n);
|
|
288
|
+
}
|
|
289
|
+
Object.freeze(snapshot.budget);
|
|
290
|
+
Object.freeze(snapshot.members);
|
|
291
|
+
Object.freeze(snapshot.libraries);
|
|
292
|
+
Object.freeze(snapshot.builtins);
|
|
293
|
+
Object.freeze(snapshot);
|
|
294
|
+
const index = { snapshot, files, digests, libs, native, nativeImplementations: new Set(nativeImplementations), closures: new Map() };
|
|
295
|
+
s.index = index;
|
|
296
|
+
return index;
|
|
297
|
+
}
|
|
298
|
+
export function gateDerived(handle, record, actual, snapshot, nativeImplementations = new Set()) {
|
|
299
|
+
const s = state(handle), bytes = verifyDerived(handle, record, actual), index = snapshotIndex(s, snapshot, nativeImplementations), { files, digests, libs, native } = index, edges = [], targets = new Map();
|
|
300
|
+
const path = record.target.kind === 'file' ? record.target.path : resolve(record.target.cwd, record.mode === 'module' ? '[eval1].js' : '[eval].cjs'), key = canonicalJson([record.digest, path, record.mode]), held = index.closures.get(key);
|
|
301
|
+
const readSizes = new Map();
|
|
302
|
+
let staticReadBytes = 0;
|
|
303
|
+
const verify = (path) => { const real = physical(path), expected = files.get(real); charge(s, 1); const remaining = snapshot.budget.bytes - staticReadBytes + (readSizes.get(real) ?? 0); const bytes = readBounded(real, remaining); if (!readSizes.has(real)) {
|
|
304
|
+
staticReadBytes += bytes.length;
|
|
305
|
+
readSizes.set(real, bytes.length);
|
|
306
|
+
} const actualDigest = sha(bytes); if (expected && actualDigest !== expected)
|
|
307
|
+
fail('Root target bytes differ ' + path); if (!expected && !digests.has(actualDigest)) {
|
|
308
|
+
let emitted = s.emissionDigests.get(actualDigest);
|
|
309
|
+
if (!emitted) {
|
|
310
|
+
const candidate = s.candidates.get(real);
|
|
311
|
+
if (candidate?.reason)
|
|
312
|
+
fail('required emission unmeasured ' + real + ' ' + candidate.reason);
|
|
313
|
+
if (candidate) {
|
|
314
|
+
if (candidate.digest !== actualDigest)
|
|
315
|
+
fail('observed emission bytes differ ' + real);
|
|
316
|
+
recordProducerEmissions(handle, [{ path: real, digest: actualDigest }], candidate.invocation);
|
|
317
|
+
emitted = s.emissionDigests.get(actualDigest);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (!emitted || emitted.parent !== s.parent.identity || emitted.producer !== handle.identity || emitted.byteLength !== bytes.length)
|
|
321
|
+
fail('literal target outside Root snapshot or producer emissions ' + path);
|
|
322
|
+
retainContent(s, actualDigest, bytes.length, !real.endsWith('.json'));
|
|
323
|
+
} targets.set(real, { path: real, digest: actualDigest, bytes: bytes.length }); return { real, bytes, digest: actualDigest }; };
|
|
324
|
+
if (held) {
|
|
325
|
+
for (const target of held.targets) {
|
|
326
|
+
const current = verify(target.path);
|
|
327
|
+
if (current.digest !== target.digest || current.bytes.length !== target.bytes)
|
|
328
|
+
fail('literal target bytes differ ' + target.path);
|
|
329
|
+
}
|
|
330
|
+
return held.edges.map(e => ({ ...e }));
|
|
331
|
+
}
|
|
332
|
+
edges.push(...traverseGeneratedClosure({ source: bytes, path, mode: record.mode, libs, native, platform: snapshot.platform, nativeImplementations, verify, step: () => charge(s, 1) }));
|
|
333
|
+
index.closures.set(key, { edges: edges.map(e => Object.freeze({ ...e })), targets: [...targets.values()].map(t => Object.freeze({ ...t })) });
|
|
334
|
+
return edges;
|
|
335
|
+
}
|
|
336
|
+
export function chargeProducerRequest(handle) { charge(state(handle), 1); }
|
|
337
|
+
export function producerUsage(handle) { const s = state(handle); return Object.freeze({ members: s.members, bytes: s.bytes, work: s.work, retainedContents: s.retained.size, codeBytes: [...s.executableDigests].reduce((n, d) => n + (s.retained.get(d) ?? 0), 0), limits: { ...s.profile.limits } }); }
|
|
338
|
+
/** Shared host traversal. The reader callback supplies verification; traversal
|
|
339
|
+
* itself introduces no provenance or execution authority. */
|
|
340
|
+
export function traverseGeneratedClosure(input) {
|
|
341
|
+
const { libs, native, verify, nativeImplementations } = input, edges = [], seen = new Set();
|
|
342
|
+
const walk = (source, path, mode) => { if (seen.has(path))
|
|
343
|
+
return; seen.add(path); input.step(); const text = new TextDecoder('utf-8', { fatal: true }).decode(source), syntax = moduleImportSyntax(text, path); if (syntax.unresolved.length)
|
|
344
|
+
fail('literal closure unresolved ' + path + ' ' + syntax.unresolved.join(' · ')); for (const specifier of syntax.imports) {
|
|
345
|
+
const builtin = specifier.startsWith('node:') ? specifier : 'node:' + specifier;
|
|
346
|
+
if (builtinModules.includes(specifier) || specifier.startsWith('node:')) {
|
|
347
|
+
if (!native.has(builtin))
|
|
348
|
+
fail('native edge undeclared ' + specifier);
|
|
349
|
+
edges.push({ target: builtin, platform: input.platform });
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
let target;
|
|
353
|
+
if (specifier.startsWith('file:'))
|
|
354
|
+
target = fileURLToPath(specifier);
|
|
355
|
+
else if (isAbsolute(specifier))
|
|
356
|
+
target = specifier;
|
|
357
|
+
else if (specifier.startsWith('.'))
|
|
358
|
+
target = mode === 'commonjs' ? createRequire(path).resolve(specifier) : resolve(dirname(path), specifier);
|
|
359
|
+
else {
|
|
360
|
+
const name = specifier.startsWith('@') ? specifier.split('/').slice(0, 2).join('/') : specifier.split('/')[0], manifest = libs.get(name);
|
|
361
|
+
if (!manifest)
|
|
362
|
+
fail('literal library absent ' + name);
|
|
363
|
+
const pkg = verify(manifest);
|
|
364
|
+
const data = JSON.parse(Buffer.from(pkg.bytes).toString('utf8')), key = specifier === name ? '.' : './' + specifier.slice(name.length + 1), exported = typeof data.exports === 'string' && key === '.' ? data.exports : data.exports?.[key];
|
|
365
|
+
if (mode === 'commonjs')
|
|
366
|
+
target = createRequire(manifest).resolve(specifier);
|
|
367
|
+
else if (typeof exported === 'string')
|
|
368
|
+
target = resolve(dirname(manifest), exported);
|
|
369
|
+
else
|
|
370
|
+
return fail('ESM library export needs exact native selection ' + specifier);
|
|
371
|
+
}
|
|
372
|
+
const member = verify(target);
|
|
373
|
+
edges.push({ target: member.real, digest: member.digest });
|
|
374
|
+
if (!member.real.endsWith('.json') && !nativeImplementations.has(member.digest))
|
|
375
|
+
walk(member.bytes, member.real, mode);
|
|
376
|
+
} };
|
|
377
|
+
walk(input.source, input.path, input.mode);
|
|
378
|
+
return edges;
|
|
379
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Declaration } from './loader.ts';
|
|
2
|
+
export interface LogicalMember {
|
|
3
|
+
readonly coordinate: string;
|
|
4
|
+
readonly digest: string;
|
|
5
|
+
readonly bytes: number;
|
|
6
|
+
}
|
|
7
|
+
export interface LogicalLoader {
|
|
8
|
+
readonly members: readonly LogicalMember[];
|
|
9
|
+
readonly libraries: readonly {
|
|
10
|
+
name: string;
|
|
11
|
+
manifest: string;
|
|
12
|
+
}[];
|
|
13
|
+
readonly builtins: readonly string[];
|
|
14
|
+
readonly entries: readonly string[];
|
|
15
|
+
readonly platform: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ImageMount {
|
|
18
|
+
readonly coordinate: string;
|
|
19
|
+
readonly path: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ImageBudget {
|
|
22
|
+
readonly members: number;
|
|
23
|
+
readonly bytes: number;
|
|
24
|
+
}
|
|
25
|
+
/** Identifies the admitted Node/OS ABI condition, not a seal of backing libraries
|
|
26
|
+
* or portability proof. The signed image must explicitly name this condition.
|
|
27
|
+
*/
|
|
28
|
+
export declare const nodePlatformIdentity: () => string;
|
|
29
|
+
/** Mount only signed logical references at exact bytes. Physical locations are
|
|
30
|
+
* an execution envelope and never a source of implementation identity.
|
|
31
|
+
*/
|
|
32
|
+
export declare function mountNativeImage(loader: LogicalLoader, mounts: readonly ImageMount[], entry: string, budget: ImageBudget): {
|
|
33
|
+
identity: string;
|
|
34
|
+
declaration: Declaration;
|
|
35
|
+
entries: readonly string[];
|
|
36
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { sha256 as digest } from "../hash.js";
|
|
2
|
+
import { isAbsolute } from 'node:path';
|
|
3
|
+
import { realpathSync } from 'node:fs';
|
|
4
|
+
import { release } from 'node:os';
|
|
5
|
+
import { readBoundedBatch } from "./bounded-file.js";
|
|
6
|
+
const fail = (why) => { throw Error('REFUSE·native image ' + why); };
|
|
7
|
+
const coordinate = (name) => typeof name === 'string' && name !== '' && !isAbsolute(name) && !name.includes('\0') && !name.includes('\\') && !name.split('/').some(step => !step || step === '.' || step === '..');
|
|
8
|
+
/** Identifies the admitted Node/OS ABI condition, not a seal of backing libraries
|
|
9
|
+
* or portability proof. The signed image must explicitly name this condition.
|
|
10
|
+
*/
|
|
11
|
+
export const nodePlatformIdentity = () => digest(JSON.stringify(['node-abi/1', process.platform, process.arch, process.versions, release()]));
|
|
12
|
+
/** Mount only signed logical references at exact bytes. Physical locations are
|
|
13
|
+
* an execution envelope and never a source of implementation identity.
|
|
14
|
+
*/
|
|
15
|
+
export function mountNativeImage(loader, mounts, entry, budget) {
|
|
16
|
+
if (!budget || !Number.isSafeInteger(budget.members) || budget.members < 1 || !Number.isSafeInteger(budget.bytes) || budget.bytes < 1)
|
|
17
|
+
fail('missing input resources');
|
|
18
|
+
if (!loader || !Array.isArray(loader.members) || !Array.isArray(loader.libraries) || !Array.isArray(loader.builtins) || !Array.isArray(loader.entries) || loader.members.length > budget.members || mounts.length !== loader.members.length || loader.libraries.length > budget.members || loader.builtins.length > budget.members || loader.entries.length > budget.members)
|
|
19
|
+
fail('input member resources');
|
|
20
|
+
if (loader.platform !== nodePlatformIdentity())
|
|
21
|
+
fail('declared Node platform differs');
|
|
22
|
+
const named = new Map();
|
|
23
|
+
let total = 0;
|
|
24
|
+
for (const member of loader.members) {
|
|
25
|
+
if (!member || !coordinate(member.coordinate) || named.has(member.coordinate) || !/^sha256:[0-9a-f]{64}$/.test(member.digest) || !Number.isSafeInteger(member.bytes) || member.bytes < 0 || (total += member.bytes) > budget.bytes)
|
|
26
|
+
fail('missing/duplicate/oversized member');
|
|
27
|
+
named.set(member.coordinate, member);
|
|
28
|
+
}
|
|
29
|
+
const paths = new Map();
|
|
30
|
+
for (const mount of mounts) {
|
|
31
|
+
if (!coordinate(mount.coordinate) || !named.has(mount.coordinate) || paths.has(mount.coordinate) || !isAbsolute(mount.path))
|
|
32
|
+
fail('unhanded or ambiguous mount');
|
|
33
|
+
paths.set(mount.coordinate, realpathSync(mount.path));
|
|
34
|
+
}
|
|
35
|
+
if (!coordinate(entry) || !loader.entries.includes(entry) || new Set(loader.entries).size !== loader.entries.length || loader.entries.some(e => !named.has(e)))
|
|
36
|
+
fail('entry outside signed image');
|
|
37
|
+
const data = readBoundedBatch(loader.members.map(m => ({ path: paths.get(m.coordinate), coordinate: m.coordinate })), budget.bytes);
|
|
38
|
+
loader.members.forEach((member, index) => { if (data[index].length !== member.bytes || digest(data[index]) !== member.digest)
|
|
39
|
+
fail('actual bytes differ ' + member.coordinate); });
|
|
40
|
+
const declarations = new Set();
|
|
41
|
+
const libraries = loader.libraries.map(lib => { if (!lib || typeof lib.name !== 'string' || declarations.has(lib.name) || !named.has(lib.manifest))
|
|
42
|
+
return fail('unknown or duplicate library'); declarations.add(lib.name); return { name: lib.name, manifest: paths.get(lib.manifest), coordinate: lib.manifest }; });
|
|
43
|
+
const identity = digest(JSON.stringify(['logical-native-image/1', loader]));
|
|
44
|
+
return { identity, declaration: { members: loader.members.map(m => ({ path: paths.get(m.coordinate), digest: m.digest, bytes: m.bytes })), libraries, builtins: [...loader.builtins], entry: paths.get(entry), platform: loader.platform }, entries: loader.entries.map(e => paths.get(e)) };
|
|
45
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** Pure prospective host handoff planner; no execution or authority discovery. */
|
|
2
|
+
export interface Budget {
|
|
3
|
+
jobs: number;
|
|
4
|
+
members: number;
|
|
5
|
+
edges: number;
|
|
6
|
+
bytes: number;
|
|
7
|
+
payloadBytes: number;
|
|
8
|
+
env: number;
|
|
9
|
+
}
|
|
10
|
+
export interface Member {
|
|
11
|
+
coordinate: string;
|
|
12
|
+
digest: string;
|
|
13
|
+
bytes: Uint8Array;
|
|
14
|
+
}
|
|
15
|
+
export interface Load {
|
|
16
|
+
from: string;
|
|
17
|
+
kind: 'module' | 'worker' | 'program';
|
|
18
|
+
target: string | {
|
|
19
|
+
computed: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export interface Binding {
|
|
23
|
+
id: string;
|
|
24
|
+
kind: 'process' | 'worker';
|
|
25
|
+
places: readonly string[];
|
|
26
|
+
program: string;
|
|
27
|
+
interpreter?: string;
|
|
28
|
+
entry?: string;
|
|
29
|
+
args: readonly string[];
|
|
30
|
+
inputs: readonly string[];
|
|
31
|
+
environment: readonly (readonly [string, string])[];
|
|
32
|
+
limits: {
|
|
33
|
+
timeMs: number;
|
|
34
|
+
responseBytes: number;
|
|
35
|
+
slots: number;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Constructed by the instrument from admitted configuration and verified bytes,
|
|
39
|
+
* never accepted as a world packet or accompanied by a packet-selected verifier. */
|
|
40
|
+
export interface Handoff {
|
|
41
|
+
place: string;
|
|
42
|
+
instrument: string;
|
|
43
|
+
policy: string;
|
|
44
|
+
authoritySnapshot: string;
|
|
45
|
+
budget: Budget;
|
|
46
|
+
members: readonly Member[];
|
|
47
|
+
loads: readonly Load[];
|
|
48
|
+
bindings: readonly Binding[];
|
|
49
|
+
}
|
|
50
|
+
export interface Proposal {
|
|
51
|
+
id: string;
|
|
52
|
+
payload: string;
|
|
53
|
+
}
|
|
54
|
+
export interface Launch {
|
|
55
|
+
id: string;
|
|
56
|
+
kind: Binding['kind'];
|
|
57
|
+
identity: string;
|
|
58
|
+
place: string;
|
|
59
|
+
program: readonly [string, string];
|
|
60
|
+
interpreter?: readonly [string, string];
|
|
61
|
+
entry?: readonly [string, string];
|
|
62
|
+
args: readonly string[];
|
|
63
|
+
payload: Uint8Array;
|
|
64
|
+
environment: readonly (readonly [string, string])[];
|
|
65
|
+
limits: Binding['limits'];
|
|
66
|
+
edges: readonly {
|
|
67
|
+
from: string;
|
|
68
|
+
kind: string;
|
|
69
|
+
target: string;
|
|
70
|
+
digest: string;
|
|
71
|
+
}[];
|
|
72
|
+
}
|
|
73
|
+
/** digest is selected by the instrument; it is a pure naming implementation,
|
|
74
|
+
* not a callback that answers whether untrusted bytes/authority are "verified". */
|
|
75
|
+
export declare function planJobs(host: Handoff, framed: Uint8Array, digest: (bytes: Uint8Array) => string): readonly Launch[];
|