@lapxo/bound 0.1.7 → 0.1.9
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 +27 -27
- 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 +6 -1
- package/dist/cli/standing-result.js +7 -26
- package/dist/cli/takes.js +5 -2
- package/dist/cli/verb.js +24 -5
- 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 +16 -4
- package/dist/fold/digests.d.ts +1 -1
- package/dist/fold/digests.js +3 -2
- package/dist/fold/evidence.d.ts +1 -1
- package/dist/fold/kept.js +2 -1
- 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 +11 -1
- 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 +54 -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/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/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 +15 -1
- package/dist/host/reading-closures.js +88 -6
- package/dist/host/reading-context.d.ts +29 -0
- package/dist/host/reading-context.js +53 -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/judge/take.d.ts +2 -0
- package/dist/judge/take.js +1 -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/runner.d.ts +4 -0
- package/dist/observe/runner.js +9 -2
- package/docs/contracts/cli.md +13 -0
- package/package.json +2 -2
- package/receipts.bound +607 -567
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { retainedProjection } from "./reading.js";
|
|
2
|
+
import { fieldNames } from "./reading.js";
|
|
3
|
+
import { sha256 as sha } from "../hash.js";
|
|
4
|
+
/** A program alias is a Root selection, never PATH.
|
|
5
|
+
* This verifies a finite implementation before entry; it executes nothing.
|
|
6
|
+
* The runtime hook must still verify each reached member before loading it. */
|
|
7
|
+
import { isAbsolute, resolve, relative } from 'node:path';
|
|
8
|
+
import { realpathSync } from 'node:fs';
|
|
9
|
+
import { authenticateSignedPort, nativeParentInputBytes, canonicalJson } from "./reading.js";
|
|
10
|
+
import { readBounded } from "./generated.js";
|
|
11
|
+
const fail = (why) => { throw Error('REFUSE·native program ' + why); };
|
|
12
|
+
const keys = fieldNames;
|
|
13
|
+
const verified = new WeakMap();
|
|
14
|
+
export function verifyNativeProgram(root, coordinate, parent, runtimeBinding) {
|
|
15
|
+
const authenticated = authenticateSignedPort(root, coordinate), p = authenticated.profile;
|
|
16
|
+
if (!p || keys(p) !== 'alias,condition,coordinate,entry,interpreter,limits,members,profile,root,selector' || p.profile !== 'host-native-program@1' || p.coordinate !== coordinate || typeof p.alias !== 'string' || !p.alias.length || p.alias.includes('\0') || typeof p.condition !== 'string' || !p.condition.length)
|
|
17
|
+
fail('selection absent/unknown');
|
|
18
|
+
if (keys(p.selector ?? {}) !== 'binding,implementation,port' || canonicalJson(p.selector) !== canonicalJson({ port: parent.port, binding: parent.binding, implementation: parent.implementation }))
|
|
19
|
+
fail('selection differs from parent');
|
|
20
|
+
if (keys(p.limits ?? {}) !== 'bytes,members' || !Number.isSafeInteger(p.limits.members) || p.limits.members < 1 || !Number.isSafeInteger(p.limits.bytes) || p.limits.bytes < 1 || p.limits.bytes > nativeParentInputBytes(root, parent))
|
|
21
|
+
fail('implementation resource absent/exceeds Root');
|
|
22
|
+
if ([parent.identity, parent.instrument, parent.policy, parent.authority, parent.implementation].some(d => typeof d !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(d)))
|
|
23
|
+
fail('parent identity absent');
|
|
24
|
+
if (!Array.isArray(p.members) || p.members.length > p.limits.members || typeof p.root !== 'string' || isAbsolute(p.root) || p.root.split('/').some(s => !s || s === '.' || s === '..') || !p.interpreter || typeof p.interpreter.path !== 'string' || isAbsolute(p.interpreter.path) || p.interpreter.path !== runtimeBinding.coordinate || keys(p.interpreter) !== 'bytes,digest,path' || !Number.isSafeInteger(p.interpreter.bytes) || p.interpreter.bytes < 1)
|
|
25
|
+
fail('interpreter/implementation context absent');
|
|
26
|
+
// This binding comes from the private prepared host image, not the program's
|
|
27
|
+
// declaration. Matching a path alone cannot identify an implementation.
|
|
28
|
+
if (!runtimeBinding || runtimeBinding.path !== parent.runtime || runtimeBinding.digest !== p.interpreter.digest || !parent.inputs.some(p => p[1] === runtimeBinding.digest))
|
|
29
|
+
fail('interpreter differs from parent image');
|
|
30
|
+
let bytes = p.interpreter.bytes;
|
|
31
|
+
if (bytes > p.limits.bytes)
|
|
32
|
+
fail('interpreter byte resource');
|
|
33
|
+
const coordinates = new Set();
|
|
34
|
+
for (const m of p.members) {
|
|
35
|
+
if (!m || keys(m) !== 'bytes,coordinate,digest' || typeof m.coordinate !== 'string' || !m.coordinate.length || isAbsolute(m.coordinate) || m.coordinate.includes('\\') || m.coordinate.split('/').some(x => !x || x === '.' || x === '..') || coordinates.has(m.coordinate) || !/^sha256:[0-9a-f]{64}$/.test(m.digest) || !Number.isSafeInteger(m.bytes) || m.bytes < 0)
|
|
36
|
+
fail('member identity ambiguous');
|
|
37
|
+
coordinates.add(m.coordinate);
|
|
38
|
+
bytes += m.bytes;
|
|
39
|
+
if (bytes > p.limits.bytes)
|
|
40
|
+
fail('implementation byte resource');
|
|
41
|
+
}
|
|
42
|
+
// All declarations and aggregate resources are checked before reading files.
|
|
43
|
+
if (!coordinates.has(p.entry) || !/^sha256:[0-9a-f]{64}$/.test(p.interpreter.digest))
|
|
44
|
+
fail('entry/interpreter digest absent');
|
|
45
|
+
const physical = (path) => { try {
|
|
46
|
+
return realpathSync(path);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return fail('selected member absent ' + path);
|
|
50
|
+
} };
|
|
51
|
+
const entryMount = runtimeBinding.members.find(m => m.coordinate === p.root + '/' + p.entry);
|
|
52
|
+
if (!entryMount)
|
|
53
|
+
return fail('entry mount absent from host image');
|
|
54
|
+
const levels = p.entry.split('/').length;
|
|
55
|
+
let physicalRoot = physical(entryMount.path);
|
|
56
|
+
for (let i = 0; i < levels; i++)
|
|
57
|
+
physicalRoot = resolve(physicalRoot, '..');
|
|
58
|
+
const interpreter = physical(runtimeBinding.path);
|
|
59
|
+
const interpreterBytes = readBounded(interpreter, p.interpreter.bytes);
|
|
60
|
+
if (interpreterBytes.length !== p.interpreter.bytes || sha(interpreterBytes) !== p.interpreter.digest)
|
|
61
|
+
fail('interpreter bytes differ');
|
|
62
|
+
const members = p.members.map(m => { const mounted = runtimeBinding.members.find(one => one.coordinate === p.root + '/' + m.coordinate); if (!mounted)
|
|
63
|
+
return fail('member absent from actual host image ' + m.coordinate); const path = physical(mounted.path), r = relative(physicalRoot, path); if (r === '..' || r.startsWith('../') || isAbsolute(r))
|
|
64
|
+
fail('member outside selected implementation'); const b = readBounded(path, m.bytes); if (b.length !== m.bytes || sha(b) !== m.digest)
|
|
65
|
+
fail('member bytes differ ' + m.coordinate); return { path, digest: m.digest }; });
|
|
66
|
+
const handle = Object.freeze({ identity: sha(canonicalJson([authenticated.line, parent.identity])) });
|
|
67
|
+
verified.set(handle, { alias: p.alias, interpreter, interpreterDigest: p.interpreter.digest, entry: physical(resolve(physicalRoot, p.entry)), root: physicalRoot, members, condition: p.condition });
|
|
68
|
+
return handle;
|
|
69
|
+
}
|
|
70
|
+
/** Only the guarded instrument can obtain this from a verified opaque handle.
|
|
71
|
+
* Put it in the signed native image; never accept it from a world proposal. */
|
|
72
|
+
export const nativeProgramProjection = retainedProjection(verified, fail);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { CommandPort } from '@lapxo/topos/contract';
|
|
2
|
+
import type { Declaration } from './loader.ts';
|
|
3
|
+
import { type Handoff } from './jobs.ts';
|
|
4
|
+
export interface Location {
|
|
5
|
+
member: string;
|
|
6
|
+
path: string;
|
|
7
|
+
kind: 'binary' | 'module' | 'data';
|
|
8
|
+
}
|
|
9
|
+
export interface NodeImage {
|
|
10
|
+
cwd: string;
|
|
11
|
+
condition: string;
|
|
12
|
+
runtime: string;
|
|
13
|
+
preload?: string;
|
|
14
|
+
nativePreloadMembers: readonly string[];
|
|
15
|
+
commandPort?: CommandPort;
|
|
16
|
+
hostLoader?: {
|
|
17
|
+
declaration: Declaration;
|
|
18
|
+
entries: readonly string[];
|
|
19
|
+
preload: string;
|
|
20
|
+
};
|
|
21
|
+
locations: readonly Location[];
|
|
22
|
+
}
|
|
23
|
+
/** Observation of a completed inner command. A nonzero exit is data, never
|
|
24
|
+
* silently converted into effect success; its meaning belongs to the reader. */
|
|
25
|
+
export interface CapturedCommand {
|
|
26
|
+
exitStatus: number;
|
|
27
|
+
stdout: string;
|
|
28
|
+
stderr: string;
|
|
29
|
+
}
|
|
30
|
+
export interface PreparedNodeImage {
|
|
31
|
+
readonly identity: string;
|
|
32
|
+
}
|
|
33
|
+
/** Image/runtime condition and locations are instrument-admitted inputs, selected
|
|
34
|
+
* before requests. Condition includes trusted Node builtins/platform libraries;
|
|
35
|
+
* a binary's file digest alone does not seal every dynamic platform library. */
|
|
36
|
+
export declare function prepareNodeImage(input: Handoff, inputImage: NodeImage): PreparedNodeImage;
|
|
37
|
+
/** Exact expectations from the same opaque host image used by execution. */
|
|
38
|
+
export declare function plannedJobs(handle: PreparedNodeImage, request: Uint8Array): readonly {
|
|
39
|
+
id: string;
|
|
40
|
+
identity: string;
|
|
41
|
+
}[];
|
|
42
|
+
/** Instrument-only producer lineage from the SAME private planned job used by
|
|
43
|
+
* executeJobs. No fixture may invent an identity or reset shared custody. */
|
|
44
|
+
export interface ParentExecution {
|
|
45
|
+
identity: string;
|
|
46
|
+
port: string;
|
|
47
|
+
binding: string;
|
|
48
|
+
implementation: string;
|
|
49
|
+
instrument: string;
|
|
50
|
+
policy: string;
|
|
51
|
+
authority: string;
|
|
52
|
+
inputs: readonly (readonly [string, string])[];
|
|
53
|
+
environment: readonly (readonly [string, string])[];
|
|
54
|
+
argv: readonly string[];
|
|
55
|
+
cwd: string;
|
|
56
|
+
runtime: string;
|
|
57
|
+
timeoutMs: number;
|
|
58
|
+
responseBytes: number;
|
|
59
|
+
}
|
|
60
|
+
export declare function plannedParentExecutions(handle: PreparedNodeImage, request: Uint8Array): readonly ParentExecution[];
|
|
61
|
+
/** Root-owned execution resource, constructed from authenticated producer selection
|
|
62
|
+
* under the authority guard. This is not a packet verifier or world callback. */
|
|
63
|
+
export interface RootJobCustody {
|
|
64
|
+
open(parent: ParentExecution): Promise<{
|
|
65
|
+
assertClosed(): void;
|
|
66
|
+
close(): Promise<void>;
|
|
67
|
+
descendant?: unknown;
|
|
68
|
+
}>;
|
|
69
|
+
}
|
|
70
|
+
/** No partial member escapes a failed batch. Uses the existing bounded async
|
|
71
|
+
* child-group executor; worker launcher is itself an exact selected module. */
|
|
72
|
+
export declare function executeJobs(handle: PreparedNodeImage, request: Uint8Array, options: {
|
|
73
|
+
phase: 'act' | 'selection' | 'help' | 'check' | 'preview' | 'sign';
|
|
74
|
+
signal?: AbortSignal;
|
|
75
|
+
trace?: (event: {
|
|
76
|
+
id: string;
|
|
77
|
+
phase: 'start' | 'end';
|
|
78
|
+
}) => void;
|
|
79
|
+
custody?: RootJobCustody;
|
|
80
|
+
}): Promise<readonly {
|
|
81
|
+
id: string;
|
|
82
|
+
identity: string;
|
|
83
|
+
responses: readonly CapturedCommand[];
|
|
84
|
+
}[]>;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { sha256 as digest } from "../hash.js";
|
|
2
|
+
/** Host-only controlled Node adapter; pure planning remains in planner.ts. */
|
|
3
|
+
import { realpathSync } from 'node:fs';
|
|
4
|
+
import { isAbsolute } from 'node:path';
|
|
5
|
+
import { readBoundedFile } from "./bounded-file.js";
|
|
6
|
+
import { executeLot } from "./command.js";
|
|
7
|
+
import { moduleClosure } from "./module-closure.js";
|
|
8
|
+
import { planJobs } from "./jobs.js";
|
|
9
|
+
const refuse = (why) => { throw Error('REFUSE·job adapter ' + why); };
|
|
10
|
+
const fileDigest = (path, limit) => { try {
|
|
11
|
+
return digest(readBoundedFile(path, limit, path));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return refuse('image member absent ' + path);
|
|
15
|
+
} };
|
|
16
|
+
const capturedCommand = (value) => { if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
17
|
+
return false; const v = value; return Object.keys(v).sort().join(',') === 'exitStatus,stderr,stdout' && Number.isSafeInteger(v.exitStatus) && v.exitStatus >= 0 && typeof v.stdout === 'string' && typeof v.stderr === 'string'; };
|
|
18
|
+
const admittedImages = new WeakMap();
|
|
19
|
+
/** Image/runtime condition and locations are instrument-admitted inputs, selected
|
|
20
|
+
* before requests. Condition includes trusted Node builtins/platform libraries;
|
|
21
|
+
* a binary's file digest alone does not seal every dynamic platform library. */
|
|
22
|
+
export function prepareNodeImage(input, inputImage) {
|
|
23
|
+
if (!inputImage?.commandPort)
|
|
24
|
+
refuse('missing admitted command port');
|
|
25
|
+
if (!inputImage || !isAbsolute(inputImage.cwd) || !inputImage.condition)
|
|
26
|
+
refuse('missing admitted Node image condition');
|
|
27
|
+
const host = { ...input, budget: { ...input.budget }, members: input.members.map(m => ({ ...m, bytes: new Uint8Array(m.bytes) })), loads: input.loads.map(e => ({ ...e, target: typeof e.target === 'string' ? e.target : { ...e.target } })), bindings: input.bindings.map(b => ({ ...b, places: [...b.places], args: [...b.args], inputs: [...b.inputs], environment: b.environment.map(p => [...p]), limits: { ...b.limits } })) };
|
|
28
|
+
// Validate all declarations and member bytes without any world proposal/launch.
|
|
29
|
+
planJobs(host, new TextEncoder().encode('[]'), digest);
|
|
30
|
+
const members = new Map(host.members.map(m => [m.coordinate, m])), locations = new Map();
|
|
31
|
+
let actualBytes = 0;
|
|
32
|
+
for (const member of host.members) {
|
|
33
|
+
actualBytes += member.bytes.length;
|
|
34
|
+
if (actualBytes > host.budget.bytes)
|
|
35
|
+
refuse('image byte resources');
|
|
36
|
+
}
|
|
37
|
+
const runtimeLocation = inputImage.locations.find(l => l.member === inputImage.runtime);
|
|
38
|
+
if (!runtimeLocation || realpathSync(runtimeLocation.path) !== realpathSync(process.execPath))
|
|
39
|
+
refuse('runtime differs from executing interpreter');
|
|
40
|
+
for (const location of inputImage.locations) {
|
|
41
|
+
if (locations.has(location.member) || !members.has(location.member) || !isAbsolute(location.path) || !['binary', 'module', 'data'].includes(location.kind))
|
|
42
|
+
refuse('unknown/duplicate image location ' + location.member);
|
|
43
|
+
if (fileDigest(location.path, host.budget.bytes) !== members.get(location.member).digest)
|
|
44
|
+
refuse('image bytes differ ' + location.member);
|
|
45
|
+
locations.set(location.member, { ...location });
|
|
46
|
+
}
|
|
47
|
+
if (locations.size !== members.size)
|
|
48
|
+
refuse('image omitted handed member');
|
|
49
|
+
const texts = new Map([...members].filter(([c]) => locations.get(c).kind === 'module' || c.endsWith('.json')).map(([c, m]) => { let source; try {
|
|
50
|
+
source = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(m.bytes);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return refuse('invalid text member ' + c);
|
|
54
|
+
} return [c, { source, digest: m.digest }]; }));
|
|
55
|
+
const snapshot = { identity: digest(new TextEncoder().encode(JSON.stringify([...members].map(([c, m]) => [c, m.digest]).sort()) + '\n')), entry: '', members: texts };
|
|
56
|
+
if (!inputImage.runtime || locations.get(inputImage.runtime)?.kind !== 'binary')
|
|
57
|
+
refuse('selected Node runtime absent');
|
|
58
|
+
if (inputImage.preload !== undefined && locations.get(inputImage.preload)?.kind !== 'module')
|
|
59
|
+
refuse('selected Node preloader absent');
|
|
60
|
+
if (!Array.isArray(inputImage.nativePreloadMembers) || (inputImage.preload !== undefined && !inputImage.nativePreloadMembers.includes(inputImage.preload)) || new Set(inputImage.nativePreloadMembers).size !== inputImage.nativePreloadMembers.length || inputImage.nativePreloadMembers.some(c => !members.has(c)))
|
|
61
|
+
refuse('native preloader members not handed');
|
|
62
|
+
const nativePreloadMembers = [...inputImage.nativePreloadMembers];
|
|
63
|
+
const proofs = new Map();
|
|
64
|
+
for (const binding of host.bindings) {
|
|
65
|
+
if (locations.get(binding.program)?.kind !== 'module' || binding.interpreter !== inputImage.runtime)
|
|
66
|
+
refuse('unsupported raw binary profile ' + binding.id);
|
|
67
|
+
if (binding.kind === 'process' && binding.entry !== undefined)
|
|
68
|
+
refuse('process entry unsupported ' + binding.id);
|
|
69
|
+
const roots = [binding.program, ...(binding.entry ? [binding.entry] : [])].filter(c => locations.get(c).kind === 'module');
|
|
70
|
+
if (!roots.length && locations.get(binding.program).kind !== 'binary')
|
|
71
|
+
refuse('unsupported program member ' + binding.program);
|
|
72
|
+
if (binding.interpreter && locations.get(binding.interpreter).kind !== 'binary')
|
|
73
|
+
refuse('interpreter is not a selected binary');
|
|
74
|
+
if (binding.kind === 'worker' && (!binding.entry || locations.get(binding.entry).kind !== 'module'))
|
|
75
|
+
refuse('worker entry is not a selected module');
|
|
76
|
+
const reached = roots.flatMap(entry => moduleClosure({ ...snapshot, entry }, () => undefined));
|
|
77
|
+
const declaredTargets = new Set([inputImage.preload, binding.program, binding.interpreter, binding.entry, ...binding.inputs, ...host.loads.map(e => typeof e.target === 'string' ? e.target : '')].filter(Boolean));
|
|
78
|
+
for (const reading of reached)
|
|
79
|
+
if (!declaredTargets.has(reading.coordinate))
|
|
80
|
+
refuse('literal module dependency not declared ' + reading.coordinate);
|
|
81
|
+
proofs.set(binding.id, JSON.stringify(reached));
|
|
82
|
+
}
|
|
83
|
+
host.policy = digest(new TextEncoder().encode(JSON.stringify([host.policy, 'captured-command/1', inputImage.condition, inputImage.runtime, inputImage.preload, nativePreloadMembers.map(c => [c, members.get(c).digest]), snapshot.identity, inputImage.commandPort ?? null, [...proofs]]) + '\n'));
|
|
84
|
+
const handle = Object.freeze({ identity: digest(new TextEncoder().encode(JSON.stringify([host.place, host.policy, snapshot.identity]))) });
|
|
85
|
+
admittedImages.set(handle, { handoff: host, image: { cwd: inputImage.cwd, condition: inputImage.condition, runtime: inputImage.runtime, preload: inputImage.preload, nativePreloadMembers, ...(inputImage.hostLoader ? { hostLoader: inputImage.hostLoader } : {}), ...(inputImage.commandPort ? { commandPort: { ...inputImage.commandPort, closure: [...inputImage.commandPort.closure], environment: [...inputImage.commandPort.environment] } } : {}), locations: [...locations.values()] }, locations, proofs, snapshot });
|
|
86
|
+
return handle;
|
|
87
|
+
}
|
|
88
|
+
/** Exact expectations from the same opaque host image used by execution. */
|
|
89
|
+
export function plannedJobs(handle, request) { const ready = admittedImages.get(handle); if (!ready)
|
|
90
|
+
return refuse('unknown prepared host image'); return planJobs(ready.handoff, request, digest).map(({ id, identity }) => ({ id, identity })); }
|
|
91
|
+
function parentExecution(ready, job) { const port = ready.image.commandPort; if (!port)
|
|
92
|
+
return refuse('producer requires declared parent port'); return { identity: job.identity, port: port.coordinate, binding: job.id, implementation: job.program[1], instrument: ready.handoff.instrument, policy: ready.handoff.policy, authority: ready.handoff.authoritySnapshot, inputs: job.edges.map(e => [e.target, e.digest]).filter((p, i, all) => all.findIndex(q => q[0] === p[0]) === i), environment: job.environment, argv: invocation(ready, job).args, cwd: ready.image.cwd, runtime: ready.locations.get(ready.image.runtime).path, timeoutMs: Math.min(port.timeoutMs, job.limits.timeMs), responseBytes: Math.min(port.responseBytes, job.limits.responseBytes) }; }
|
|
93
|
+
export function plannedParentExecutions(handle, request) { const ready = admittedImages.get(handle); if (!ready)
|
|
94
|
+
return refuse('unknown prepared host image'); return planJobs(ready.handoff, request, digest).map(job => parentExecution(ready, job)); }
|
|
95
|
+
const invocation = (ready, job) => {
|
|
96
|
+
const program = ready.locations.get(job.program[0]), interpreter = job.interpreter && ready.locations.get(job.interpreter[0]);
|
|
97
|
+
if (!interpreter)
|
|
98
|
+
return { command: program.path, args: [...job.args], cwd: ready.image.cwd };
|
|
99
|
+
const host = ready.image.hostLoader;
|
|
100
|
+
if (!host)
|
|
101
|
+
return refuse('fixed loader envelope absent');
|
|
102
|
+
const url = new URL(host.preload);
|
|
103
|
+
url.searchParams.set('bytes', String(ready.handoff.budget.bytes));
|
|
104
|
+
const envelope = Buffer.from(JSON.stringify({ format: 'native-image-envelope/1', entries: host.entries, declaration: host.declaration, execution: { runtime: ready.locations.get(ready.image.runtime).path, cwd: ready.image.cwd, mounts: ready.image.locations.map(location => ({ coordinate: location.member, path: location.path, kind: location.kind })) } }));
|
|
105
|
+
return { command: interpreter.path, args: ['--import', url.href, ...(ready.image.preload ? ['--import', ready.locations.get(ready.image.preload).path] : []), program.path, ...(job.kind === 'worker' ? [ready.locations.get(job.entry[0]).path] : []), ...job.args], cwd: ready.image.cwd, hostEnvelope: envelope, envelopeBytes: ready.handoff.budget.bytes };
|
|
106
|
+
};
|
|
107
|
+
/** No partial member escapes a failed batch. Uses the existing bounded async
|
|
108
|
+
* child-group executor; worker launcher is itself an exact selected module. */
|
|
109
|
+
export async function executeJobs(handle, request, options) {
|
|
110
|
+
const ready = admittedImages.get(handle);
|
|
111
|
+
if (!ready)
|
|
112
|
+
return refuse('unknown prepared host image');
|
|
113
|
+
const { phase, signal, trace } = options;
|
|
114
|
+
if (phase !== 'act')
|
|
115
|
+
refuse('invocation forbidden during ' + phase);
|
|
116
|
+
if (signal?.aborted)
|
|
117
|
+
refuse('interrupted before batch');
|
|
118
|
+
const jobs = planJobs(ready.handoff, request, digest);
|
|
119
|
+
// Complete batch and all locations are verified before invoking any program.
|
|
120
|
+
for (const member of ready.handoff.members) {
|
|
121
|
+
const loc = ready.locations.get(member.coordinate);
|
|
122
|
+
if (fileDigest(loc.path, ready.handoff.budget.bytes) !== member.digest)
|
|
123
|
+
refuse('image bytes differ ' + member.coordinate);
|
|
124
|
+
}
|
|
125
|
+
if (!jobs.length)
|
|
126
|
+
return [];
|
|
127
|
+
const slots = Math.min(...jobs.map(j => j.limits.slots)), controller = new AbortController(), abort = controller.abort.bind(controller, undefined);
|
|
128
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
129
|
+
if (signal?.aborted)
|
|
130
|
+
abort();
|
|
131
|
+
const results = new Array(jobs.length);
|
|
132
|
+
let next = 0;
|
|
133
|
+
let primary;
|
|
134
|
+
const work = async () => {
|
|
135
|
+
while (!controller.signal.aborted) {
|
|
136
|
+
const index = next++;
|
|
137
|
+
if (index >= jobs.length)
|
|
138
|
+
return;
|
|
139
|
+
const job = jobs[index], fixed = invocation(ready, job), implementation = job.program[1], port = { ...ready.image.commandPort, timeoutMs: Math.min(ready.image.commandPort.timeoutMs, job.limits.timeMs), responseBytes: Math.min(ready.image.commandPort.responseBytes, job.limits.responseBytes) };
|
|
140
|
+
const proofFor = () => { for (const edge of job.edges) {
|
|
141
|
+
const loc = ready.locations.get(edge.target);
|
|
142
|
+
if (!loc || fileDigest(loc.path, ready.handoff.budget.bytes) !== edge.digest)
|
|
143
|
+
return 'unmeasured';
|
|
144
|
+
} const roots = [job.program[0], ...(job.entry ? [job.entry[0]] : [])].filter(c => ready.locations.get(c).kind === 'module'); const actual = JSON.stringify(roots.flatMap(entry => moduleClosure({ ...ready.snapshot, entry }, () => undefined))); return actual === ready.proofs.get(job.id) ? 'verified' : 'unmeasured'; };
|
|
145
|
+
trace?.({ id: job.id, phase: 'start' });
|
|
146
|
+
let custody;
|
|
147
|
+
try {
|
|
148
|
+
if (options.custody)
|
|
149
|
+
custody = await options.custody.open(parentExecution(ready, job));
|
|
150
|
+
if (!custody)
|
|
151
|
+
return refuse('native descendant custody missing ' + job.id);
|
|
152
|
+
const envelope = JSON.parse(Buffer.from(fixed.hostEnvelope).toString('utf8'));
|
|
153
|
+
envelope.descendant = custody.descendant;
|
|
154
|
+
if (!envelope.descendant)
|
|
155
|
+
return refuse('fixed descendant envelope absent ' + job.id);
|
|
156
|
+
const guarded = { ...fixed, hostEnvelope: Buffer.from(JSON.stringify(envelope)) };
|
|
157
|
+
const result = await executeLot({ port, lot: { identity: job.identity, requests: [new TextDecoder().decode(job.payload)] }, phase, signal: controller.signal, environment: Object.fromEntries(job.environment), host: { admitsDeclaration: p => JSON.stringify(p) === JSON.stringify(port), covers: c => c === port.coordinate && ready.handoff.bindings.find(b => b.id === job.id).places.includes(ready.handoff.place), verifies: d => ready.handoff.members.some(m => m.digest === d && fileDigest(ready.locations.get(m.coordinate).path, ready.handoff.budget.bytes) === d), resolve: () => guarded, loaderClosure: (_p, actual) => JSON.stringify(actual) === JSON.stringify(guarded) ? proofFor() : 'unmeasured' }, accepts: (response, index) => index === 0 && capturedCommand(response) });
|
|
158
|
+
custody?.assertClosed();
|
|
159
|
+
results[index] = { id: job.id, identity: job.identity, responses: result.members };
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (primary === undefined)
|
|
163
|
+
primary = error;
|
|
164
|
+
controller.abort();
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
await custody?.close();
|
|
169
|
+
trace?.({ id: job.id, phase: 'end' });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
try {
|
|
174
|
+
const settled = await Promise.allSettled(Array.from({ length: Math.min(slots, jobs.length) }, work));
|
|
175
|
+
if (primary !== undefined)
|
|
176
|
+
throw primary;
|
|
177
|
+
const failed = settled.find(x => x.status === 'rejected');
|
|
178
|
+
if (failed?.status === 'rejected')
|
|
179
|
+
throw failed.reason;
|
|
180
|
+
if (controller.signal.aborted || results.some(x => x === undefined) || results.filter(Boolean).length !== jobs.length)
|
|
181
|
+
refuse('interrupted/incomplete batch');
|
|
182
|
+
return results;
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
signal?.removeEventListener('abort', abort);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Parse the actual Node launch without rewriting argv or evaluating text.
|
|
2
|
+
* Selection/authority and generated-member sealing belong to the Root host.
|
|
3
|
+
* This is a deliberately finite native profile, not a JS sandbox. */
|
|
4
|
+
export type NodeLaunch = {
|
|
5
|
+
kind: 'eval';
|
|
6
|
+
mode: 'commonjs' | 'module';
|
|
7
|
+
text: string;
|
|
8
|
+
args: readonly string[];
|
|
9
|
+
imports: readonly string[];
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'file';
|
|
12
|
+
entry: string;
|
|
13
|
+
args: readonly string[];
|
|
14
|
+
imports: readonly string[];
|
|
15
|
+
test: boolean;
|
|
16
|
+
};
|
|
17
|
+
export declare function nodeLaunch(args: readonly string[]): NodeLaunch;
|
|
18
|
+
import type { NativeProgramProjection } from './native-program.ts';
|
|
19
|
+
export interface SelectedInterpreter {
|
|
20
|
+
readonly path: string;
|
|
21
|
+
readonly digest: string;
|
|
22
|
+
}
|
|
23
|
+
export interface NativeProgramLaunch {
|
|
24
|
+
readonly interpreter: string;
|
|
25
|
+
readonly interpreterDigest: string;
|
|
26
|
+
readonly entry: string;
|
|
27
|
+
readonly args: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
/** Only an exact, unique declared alias selects a native program. No PATH,
|
|
30
|
+
* filename heuristics, option rewriting or consumer-specific names occur here. */
|
|
31
|
+
export declare function projectNativeProgramLaunch(programs: readonly NativeProgramProjection[], program: string, args: readonly string[], runtime: SelectedInterpreter): NativeProgramLaunch;
|
|
32
|
+
export interface NativeProgramParent {
|
|
33
|
+
readonly parent: string;
|
|
34
|
+
readonly root: string;
|
|
35
|
+
readonly members: readonly {
|
|
36
|
+
readonly path: string;
|
|
37
|
+
readonly digest: string;
|
|
38
|
+
}[];
|
|
39
|
+
}
|
|
40
|
+
/** Select a package-resolution context only for the exact reached parent member.
|
|
41
|
+
* Root must validate the supplied parent digest against actual bytes first.
|
|
42
|
+
* Node still performs actual resolution; Root checks the returned target digest
|
|
43
|
+
* before handing it back. Merely sharing a path prefix grants no membership. */
|
|
44
|
+
export declare function selectNativeProgramParent(programs: readonly NativeProgramProjection[], parent: {
|
|
45
|
+
readonly path: string;
|
|
46
|
+
readonly digest: string;
|
|
47
|
+
}): NativeProgramParent;
|
|
48
|
+
/** The fixed host receives this record only from the actual Root custodian's
|
|
49
|
+
* retained before-fork evidence. This projection grants no new executable. */
|
|
50
|
+
export interface NativeEvalActual {
|
|
51
|
+
readonly argv: readonly string[];
|
|
52
|
+
readonly rootPrefix: readonly string[];
|
|
53
|
+
readonly cwd: string;
|
|
54
|
+
readonly environment: readonly (readonly [string, string])[];
|
|
55
|
+
readonly runtime: string;
|
|
56
|
+
}
|
|
57
|
+
export interface NativeEvalParent {
|
|
58
|
+
readonly url: string;
|
|
59
|
+
readonly context: string;
|
|
60
|
+
readonly digest: string;
|
|
61
|
+
readonly mode: 'module' | 'commonjs';
|
|
62
|
+
}
|
|
63
|
+
import type { DerivedExecutable } from './generated.ts';
|
|
64
|
+
export declare function retainedNativeEvalMatches(record: DerivedExecutable, actual: NativeEvalActual): boolean;
|
|
65
|
+
/** Exact Node CLI eval origin, not a file-pattern exception. A different URL
|
|
66
|
+
* continues through ordinary physical-parent verification and refuses if absent.
|
|
67
|
+
* Node's module and CommonJS CLI forms have distinct observed synthetic names. */
|
|
68
|
+
export declare function nativeEvalParent(record: DerivedExecutable, actual: NativeEvalActual): NativeEvalParent;
|
|
69
|
+
/** Non-authoritative discovery from Node's actual runtime options only. A false
|
|
70
|
+
* result confers no permission: physical-parent/load checks remain unchanged.
|
|
71
|
+
* Before-fork argv validation and exact retained-record matching are separate. */
|
|
72
|
+
export declare function actualNodeEvalOptions(execArgv: readonly string[]): boolean;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
const refuse = (why) => { throw Error('REFUSE·native descendant ' + why); };
|
|
2
|
+
export function nodeLaunch(args) {
|
|
3
|
+
if (!Array.isArray(args) || args.some(s => typeof s !== 'string' || s.includes('\0')))
|
|
4
|
+
refuse('argv invalid');
|
|
5
|
+
let mode = 'commonjs', evalText, entry, test = false;
|
|
6
|
+
const imports = [];
|
|
7
|
+
for (let i = 0; i < args.length; i++) {
|
|
8
|
+
const arg = args[i];
|
|
9
|
+
if (arg === '--') {
|
|
10
|
+
entry = args[++i];
|
|
11
|
+
break;
|
|
12
|
+
}
|
|
13
|
+
if (arg === '--input-type=module') {
|
|
14
|
+
mode = 'module';
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (arg === '--input-type=commonjs') {
|
|
18
|
+
mode = 'commonjs';
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (arg === '--input-type') {
|
|
22
|
+
const value = args[++i];
|
|
23
|
+
if (value !== 'module' && value !== 'commonjs')
|
|
24
|
+
refuse('input type unsupported');
|
|
25
|
+
mode = value;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (arg === '-e' || arg === '--eval') {
|
|
29
|
+
if (evalText !== undefined)
|
|
30
|
+
refuse('eval ambiguous');
|
|
31
|
+
evalText = args[++i];
|
|
32
|
+
if (evalText === undefined)
|
|
33
|
+
refuse('eval absent');
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (arg.startsWith('--eval=')) {
|
|
37
|
+
if (evalText !== undefined)
|
|
38
|
+
refuse('eval ambiguous');
|
|
39
|
+
evalText = arg.slice(7);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (arg === '--import' || arg === '--require' || arg === '-r') {
|
|
43
|
+
const value = args[++i];
|
|
44
|
+
if (value === undefined)
|
|
45
|
+
refuse('preload absent');
|
|
46
|
+
imports.push(value);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (arg.startsWith('--import=') || arg.startsWith('--require=')) {
|
|
50
|
+
imports.push(arg.slice(arg.indexOf('=') + 1));
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (arg === '--test') {
|
|
54
|
+
test = true;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (arg.startsWith('--test-') || arg === '--experimental-strip-types' || arg === '--experimental-transform-types')
|
|
58
|
+
continue;
|
|
59
|
+
if (arg.startsWith('-'))
|
|
60
|
+
refuse('option unsupported ' + arg);
|
|
61
|
+
if (evalText !== undefined)
|
|
62
|
+
break;
|
|
63
|
+
entry = arg;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
if (evalText !== undefined) {
|
|
67
|
+
if (entry !== undefined)
|
|
68
|
+
refuse('eval plus executable file');
|
|
69
|
+
return { kind: 'eval', mode, text: evalText, args: [...args], imports };
|
|
70
|
+
}
|
|
71
|
+
if (!entry)
|
|
72
|
+
refuse(test ? 'implicit test discovery unsupported' : 'executable target absent');
|
|
73
|
+
return { kind: 'file', entry, args: [...args], imports, test };
|
|
74
|
+
}
|
|
75
|
+
/** Pure host glue. Inputs are projections obtained from verified opaque native
|
|
76
|
+
* program handles under Root's guard, never accepted from a world proposal.
|
|
77
|
+
* Selection names preserve argv; they confer no independent authority. */
|
|
78
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
79
|
+
import { isSha256 } from "../hash.js";
|
|
80
|
+
const programFail = (why) => { throw Error('REFUSE·native launch ' + why); };
|
|
81
|
+
const digest = isSha256;
|
|
82
|
+
const exactPath = (value) => typeof value === 'string' && isAbsolute(value) && resolve(value) === value;
|
|
83
|
+
function graph(program) {
|
|
84
|
+
if (!program || typeof program.alias !== 'string' || !program.alias.length || !exactPath(program.interpreter) || !digest(program.interpreterDigest) || !exactPath(program.entry) || !exactPath(program.root) || !Array.isArray(program.members))
|
|
85
|
+
programFail('verified program projection absent');
|
|
86
|
+
const paths = new Set();
|
|
87
|
+
for (const member of program.members) {
|
|
88
|
+
if (!member || !exactPath(member.path) || !digest(member.digest) || paths.has(member.path))
|
|
89
|
+
programFail('program graph member ambiguous');
|
|
90
|
+
paths.add(member.path);
|
|
91
|
+
}
|
|
92
|
+
if (!paths.has(program.entry))
|
|
93
|
+
programFail('program entry not in selected graph');
|
|
94
|
+
}
|
|
95
|
+
/** Only an exact, unique declared alias selects a native program. No PATH,
|
|
96
|
+
* filename heuristics, option rewriting or consumer-specific names occur here. */
|
|
97
|
+
export function projectNativeProgramLaunch(programs, program, args, runtime) {
|
|
98
|
+
if (!Array.isArray(programs) || typeof program !== 'string' || !program.length || !Array.isArray(args) || args.some(arg => typeof arg !== 'string' || arg.includes('\0')) || !runtime || !exactPath(runtime.path) || !digest(runtime.digest))
|
|
99
|
+
programFail('invocation context absent');
|
|
100
|
+
const matches = programs.filter(p => p?.alias === program);
|
|
101
|
+
if (matches.length !== 1)
|
|
102
|
+
programFail(matches.length ? 'program alias ambiguous ' + program : 'program alias undeclared ' + program);
|
|
103
|
+
const selected = matches[0];
|
|
104
|
+
graph(selected);
|
|
105
|
+
if (selected.interpreter !== runtime.path || selected.interpreterDigest !== runtime.digest)
|
|
106
|
+
programFail('selected interpreter differs');
|
|
107
|
+
return Object.freeze({ interpreter: selected.interpreter, interpreterDigest: selected.interpreterDigest, entry: selected.entry, args: Object.freeze([...args]) });
|
|
108
|
+
}
|
|
109
|
+
/** Select a package-resolution context only for the exact reached parent member.
|
|
110
|
+
* Root must validate the supplied parent digest against actual bytes first.
|
|
111
|
+
* Node still performs actual resolution; Root checks the returned target digest
|
|
112
|
+
* before handing it back. Merely sharing a path prefix grants no membership. */
|
|
113
|
+
export function selectNativeProgramParent(programs, parent) {
|
|
114
|
+
if (!Array.isArray(programs) || !parent || !exactPath(parent.path) || !digest(parent.digest))
|
|
115
|
+
programFail('parent content context absent');
|
|
116
|
+
const matches = programs.filter(p => Array.isArray(p?.members) && p.members.some((m) => m?.path === parent.path && m.digest === parent.digest));
|
|
117
|
+
if (matches.length !== 1)
|
|
118
|
+
programFail(matches.length ? 'parent graph ambiguous' : 'parent not in selected graph');
|
|
119
|
+
const selected = matches[0];
|
|
120
|
+
graph(selected);
|
|
121
|
+
return Object.freeze({ parent: parent.path, root: selected.root, members: Object.freeze(selected.members.map((member) => Object.freeze({ ...member }))) });
|
|
122
|
+
}
|
|
123
|
+
import { createHash } from 'node:crypto';
|
|
124
|
+
import { pathToFileURL } from 'node:url';
|
|
125
|
+
import { canonicalJson } from "./reading.js";
|
|
126
|
+
export function retainedNativeEvalMatches(record, actual) {
|
|
127
|
+
if (!record || record.kind !== 'derived-executable/1' || record.target?.kind !== 'eval' || !actual || !Array.isArray(actual.argv) || !Array.isArray(actual.rootPrefix) || !exactPath(actual.cwd) || !exactPath(actual.runtime) || !Array.isArray(actual.environment))
|
|
128
|
+
return false;
|
|
129
|
+
if (actual.rootPrefix.some((arg, i) => actual.argv[i] !== arg) || canonicalJson(actual.argv) !== canonicalJson(record.target.argv) || actual.cwd !== record.target.cwd || actual.cwd !== record.invocation.cwd || actual.runtime !== record.invocation.runtime || canonicalJson(actual.environment) !== canonicalJson(record.invocation.environment) || canonicalJson(actual.argv.slice(actual.rootPrefix.length)) !== canonicalJson(record.invocation.argv))
|
|
130
|
+
return false;
|
|
131
|
+
let launch;
|
|
132
|
+
try {
|
|
133
|
+
launch = nodeLaunch(actual.argv);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
if (launch.kind !== 'eval' || launch.text !== record.target.text || launch.mode !== record.mode)
|
|
139
|
+
return false;
|
|
140
|
+
const bytes = Buffer.from(launch.text, 'utf8');
|
|
141
|
+
return bytes.toString('utf8') === launch.text && bytes.length === record.byteLength && 'sha256:' + createHash('sha256').update(bytes).digest('hex') === record.digest;
|
|
142
|
+
}
|
|
143
|
+
/** Exact Node CLI eval origin, not a file-pattern exception. A different URL
|
|
144
|
+
* continues through ordinary physical-parent verification and refuses if absent.
|
|
145
|
+
* Node's module and CommonJS CLI forms have distinct observed synthetic names. */
|
|
146
|
+
export function nativeEvalParent(record, actual) {
|
|
147
|
+
if (!retainedNativeEvalMatches(record, actual))
|
|
148
|
+
programFail('actual eval differs from Root-retained context');
|
|
149
|
+
return Object.freeze({ url: record.mode === 'module' ? pathToFileURL(actual.cwd + '/').href + '[eval1]' : pathToFileURL(resolve(actual.cwd, '[eval]')).href, context: record.identity, digest: record.digest, mode: record.mode });
|
|
150
|
+
}
|
|
151
|
+
/** Non-authoritative discovery from Node's actual runtime options only. A false
|
|
152
|
+
* result confers no permission: physical-parent/load checks remain unchanged.
|
|
153
|
+
* Before-fork argv validation and exact retained-record matching are separate. */
|
|
154
|
+
export function actualNodeEvalOptions(execArgv) { try {
|
|
155
|
+
return nodeLaunch(execArgv).kind === 'eval';
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return false;
|
|
159
|
+
} }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Fixed instrument preload. FD3 is an instrument-created execution envelope,
|
|
2
|
+
* never world stdin or workerData. It introduces no key or command grant.
|
|
3
|
+
*/
|
|
4
|
+
import { readSync, closeSync } from 'node:fs';
|
|
5
|
+
import { realpathSync } from 'node:fs';
|
|
6
|
+
import { installDescendantBoundary } from "./descendants.js";
|
|
7
|
+
import { installBoundary, installNativeResolutionReuse } from "./loader.js";
|
|
8
|
+
const fail = (why) => { throw Error('REFUSE·native preload ' + why); };
|
|
9
|
+
const limit = Number(new URL(import.meta.url).searchParams.get('bytes'));
|
|
10
|
+
if (!Number.isSafeInteger(limit) || limit < 1)
|
|
11
|
+
fail('missing admitted envelope resources');
|
|
12
|
+
const header = Buffer.alloc(8);
|
|
13
|
+
let read = 0;
|
|
14
|
+
while (read < header.length) {
|
|
15
|
+
const n = readSync(3, header, read, header.length - read, null);
|
|
16
|
+
if (!n)
|
|
17
|
+
fail('truncated host frame');
|
|
18
|
+
read += n;
|
|
19
|
+
}
|
|
20
|
+
const size = header.readBigUInt64BE();
|
|
21
|
+
if (size > BigInt(limit))
|
|
22
|
+
fail('host envelope bytes exceeded');
|
|
23
|
+
const raw = Buffer.alloc(Number(size));
|
|
24
|
+
read = 0;
|
|
25
|
+
while (read < raw.length) {
|
|
26
|
+
const n = readSync(3, raw, read, raw.length - read, null);
|
|
27
|
+
if (!n)
|
|
28
|
+
fail('truncated host envelope');
|
|
29
|
+
read += n;
|
|
30
|
+
}
|
|
31
|
+
if (readSync(3, Buffer.alloc(1), 0, 1, null) !== 0)
|
|
32
|
+
fail('host envelope has trailing bytes');
|
|
33
|
+
closeSync(3);
|
|
34
|
+
const frame = JSON.parse(raw.toString('utf8'));
|
|
35
|
+
if (frame.format !== 'native-image-envelope/1' || !Array.isArray(frame.entries) || !frame.entries.includes(realpathSync(process.argv[1])))
|
|
36
|
+
fail('entry outside host image');
|
|
37
|
+
if (frame.descendant)
|
|
38
|
+
installDescendantBoundary(frame.descendant);
|
|
39
|
+
installNativeResolutionReuse(frame.declaration);
|
|
40
|
+
if (frame.execution) {
|
|
41
|
+
const facade = Object.freeze({ ...frame.execution, preload: frame.descendant?.custodian?.preload, mounts: Object.freeze(frame.execution.mounts.map(mount => Object.freeze({ ...mount }))) });
|
|
42
|
+
Object.defineProperty(globalThis, Symbol.for('bound.native.execution@1'), { value: facade, writable: false, configurable: false });
|
|
43
|
+
}
|
|
44
|
+
const boundary = installBoundary({ ...frame.declaration, entry: realpathSync(process.argv[1]) });
|
|
45
|
+
process.once('beforeExit', () => boundary.assertClosed());
|