@lapxo/bound 0.1.8 → 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 +25 -25
- 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/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/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 +296 -256
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { actCosts } from "./act-cost.js";
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
2
3
|
import { boundedProcess } from "./process-lifetime.js";
|
|
3
4
|
import { spawnSync } from 'node:child_process';
|
|
4
5
|
import { PROTOCOL } from '@lapxo/topos/wire';
|
|
@@ -35,7 +36,9 @@ export function validResponse(value, request) {
|
|
|
35
36
|
/** Last-line JSON is the wire framing. No partial batch, failed exit or synthetic missing response is accepted. */
|
|
36
37
|
export function executeCapsule(command, args, requests, limits = {}, lifetime) {
|
|
37
38
|
return actCosts.measure(() => {
|
|
38
|
-
const
|
|
39
|
+
const cwd = limits.cwd;
|
|
40
|
+
const directory = cwd === undefined ? undefined : typeof cwd === 'string' ? cwd : fileURLToPath(cwd);
|
|
41
|
+
const run = lifetime === undefined ? spawnSync(command, args, { input: JSON.stringify(requests), encoding: 'utf8', maxBuffer: 1 << 26, ...limits }) : boundedProcess(command, args, JSON.stringify(requests), lifetime, directory, limits.env);
|
|
39
42
|
if ('error' in run && run.error)
|
|
40
43
|
throw new CapsuleProcessError(`failed: ${run.error.message}`, run);
|
|
41
44
|
if (run.signal !== null || run.status !== 0)
|
package/dist/host/capsule.d.ts
CHANGED
|
@@ -2,6 +2,10 @@ import type { ReaderReference } from '@lapxo/topos/wire';
|
|
|
2
2
|
import type { Declaration } from '@lapxo/topos/capsule';
|
|
3
3
|
import type { Request, Response } from '@lapxo/topos/contract';
|
|
4
4
|
export interface Capsule {
|
|
5
|
+
readonly selectedWorld?: {
|
|
6
|
+
readonly standing: import("@lapxo/topos/standing").StandingTopos;
|
|
7
|
+
readonly offer: string;
|
|
8
|
+
};
|
|
5
9
|
readonly selection?: string;
|
|
6
10
|
readonly digest: string;
|
|
7
11
|
readonly declaration: Declaration;
|
package/dist/host/capsule.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { readerLifetime, parse } from '@lapxo/topos/wire';
|
|
2
2
|
import { contentStore, verifiedContent, contentPolicy } from "./content-store.js";
|
|
3
3
|
import { readOnly } from "./read-only.js";
|
|
4
|
+
import { readingBoundary } from "./reading-context.js";
|
|
5
|
+
import { nativeReaderRoute } from "./native-reader-context.js";
|
|
6
|
+
import { assertNativeRouteCurrent, invokeNativeRoute, materializeNativeRoute } from "./native/route.js";
|
|
4
7
|
import { sha } from "./hash.js";
|
|
5
8
|
import { wireLinesOf as linesOf } from "../fold/claims.js";
|
|
6
9
|
import { createHash } from 'node:crypto';
|
|
@@ -87,6 +90,13 @@ const spawnOf = (at, names) => {
|
|
|
87
90
|
};
|
|
88
91
|
export const childFlags = (argv = process.execArgv) => argv.filter((flag, i) => !/^(-e|-p|--eval|--print)$/.test(argv[i - 1] ?? '') && !/^(-e|-p|--eval|--print|--input-type)(=|$)/.test(flag));
|
|
89
92
|
function answered(digest, bytes, requests, names, tree, cacheStore = ownStore(), entry, reader) {
|
|
93
|
+
const boundary = readingBoundary();
|
|
94
|
+
const native = nativeReaderRoute();
|
|
95
|
+
if (native) {
|
|
96
|
+
if (!boundary?.execution || digest !== native.profile.selection.archive)
|
|
97
|
+
throw Error('REFUSE·native reader lacks its complete implementation identity');
|
|
98
|
+
assertNativeRouteCurrent(native);
|
|
99
|
+
}
|
|
90
100
|
const child = entry === undefined ? spawnOf(tree ?? unpacked(digest, bytes), names) : [entry];
|
|
91
101
|
if (!child.length) {
|
|
92
102
|
process.stderr.write(`LOAD regions · none of ${names.length} under the directories release/layout names\n`);
|
|
@@ -95,7 +105,7 @@ function answered(digest, bytes, requests, names, tree, cacheStore = ownStore(),
|
|
|
95
105
|
const loader = entry !== undefined ? `${createHash('sha256').update(`${relative(tree, entry)}\n`).digest('hex')}\n` : child.length > 1 ? `${createHash('sha256').update(observeFile(child[0]) ?? new Uint8Array()).digest('hex')}\n` : '';
|
|
96
106
|
const lifetime = readerLifetime(contentPolicy().flatMap(line => { const p = parse(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader ?? digest);
|
|
97
107
|
const policy = lifetime === undefined ? '' : JSON.stringify(lifetime) + '\n';
|
|
98
|
-
const hitOf = (request) => sha(`${digest}\n${loader}${policy}${JSON.stringify(request)}`);
|
|
108
|
+
const hitOf = (request) => sha(`${digest}\n${loader}${policy}${boundary === undefined ? '' : boundary.identity + '\n'}${JSON.stringify(request)}`);
|
|
99
109
|
const hits = requests.map(hitOf);
|
|
100
110
|
const kept = hits.map(hit => storeAt(cacheStore, 'cas', 'answers', hit));
|
|
101
111
|
const held = kept.map((at, i) => {
|
|
@@ -116,7 +126,11 @@ function answered(digest, bytes, requests, names, tree, cacheStore = ownStore(),
|
|
|
116
126
|
if (missing.length)
|
|
117
127
|
process.stderr.write(`CAPSULE ${digest.slice(7, 19)} · ${requests.length} asked · ${missing.length} run · ${missing.map((one) => `${one.rootScope}${one.region}`).join(' ')}\n`);
|
|
118
128
|
// Execute and validate the entire batch before committing any answer to the cache.
|
|
119
|
-
|
|
129
|
+
if (native && !lifetime)
|
|
130
|
+
throw Error('REFUSE·native reader missing declared lifetime');
|
|
131
|
+
const ran = !missing.length ? [] : native
|
|
132
|
+
? invokeNativeRoute(native, { ...materializeNativeRoute(native), requests: missing, execution: boundary.execution, limits: lifetime })
|
|
133
|
+
: executeCapsule(process.execPath, [...childFlags(boundary?.execution?.flags), hostOf(), ...child], missing, boundary?.execution ? { env: boundary.execution.env, cwd: boundary.execution.cwd } : {}, lifetime);
|
|
120
134
|
return requests.map((request, i) => {
|
|
121
135
|
if (held[i] !== undefined)
|
|
122
136
|
return held[i];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type PlaceReceipt } from '@lapxo/topos/core';
|
|
2
|
+
export declare function hasContextualPlace(root: string): boolean;
|
|
3
|
+
/** Exact selected SDK frame plus complete pre-signed receiver statements. No
|
|
4
|
+
* executable/verifier/epoch selection can be supplied by this envelope. */
|
|
5
|
+
export declare function landContextualPlace(root: string, file: string): Promise<import("./ports/place-act.ts").PlaceActBundle & {
|
|
6
|
+
readonly kind: "landed" | "once";
|
|
7
|
+
readonly identity: string;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function contextualAct(root: string, identity: string): {
|
|
10
|
+
profile: string;
|
|
11
|
+
identity: string;
|
|
12
|
+
context: string;
|
|
13
|
+
receipts: readonly PlaceReceipt[];
|
|
14
|
+
contexts: {
|
|
15
|
+
identity: string;
|
|
16
|
+
context: string;
|
|
17
|
+
receipts: readonly PlaceReceipt[];
|
|
18
|
+
authority: ReturnType<(encoded: string) => {
|
|
19
|
+
anchor: import("@lapxo/topos/wire").Signer;
|
|
20
|
+
history: readonly string[];
|
|
21
|
+
checkpoint: string;
|
|
22
|
+
}>;
|
|
23
|
+
}[];
|
|
24
|
+
} | undefined;
|
|
25
|
+
/** Carriers retain exact originals and authenticated context/receipts. Each
|
|
26
|
+
* namespace folds independently; labels assert no statistical independence. */
|
|
27
|
+
export declare function contextualStanding(root: string): {
|
|
28
|
+
profile: string;
|
|
29
|
+
root: readonly string[];
|
|
30
|
+
legacyPlaces: {
|
|
31
|
+
namespace: string;
|
|
32
|
+
place: string;
|
|
33
|
+
standing: string[];
|
|
34
|
+
history: readonly string[];
|
|
35
|
+
}[];
|
|
36
|
+
contexts: {
|
|
37
|
+
identity: string;
|
|
38
|
+
context: string;
|
|
39
|
+
receipts: readonly PlaceReceipt[];
|
|
40
|
+
authority: {
|
|
41
|
+
anchor: import("@lapxo/topos/wire").Signer;
|
|
42
|
+
history: readonly string[];
|
|
43
|
+
checkpoint: string;
|
|
44
|
+
};
|
|
45
|
+
}[];
|
|
46
|
+
places: {
|
|
47
|
+
namespace: string;
|
|
48
|
+
place: string;
|
|
49
|
+
standing: string[];
|
|
50
|
+
retracted: readonly string[];
|
|
51
|
+
superseded: readonly string[];
|
|
52
|
+
forks: readonly import("../fold/claims.ts").Fork[];
|
|
53
|
+
contexts: {
|
|
54
|
+
identity: string;
|
|
55
|
+
context: string;
|
|
56
|
+
receipts: readonly PlaceReceipt[];
|
|
57
|
+
}[];
|
|
58
|
+
}[];
|
|
59
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { openSync, fstatSync, readSync, closeSync } from 'node:fs';
|
|
2
|
+
import { readPlaceActContext, PLACE_ACT_CONTEXT } from '@lapxo/topos/core';
|
|
3
|
+
import { declaredRootClockSource, hasRootClockSource } from "./root-clock-source.js";
|
|
4
|
+
import { commitPlaceAct, placeActResult, committedPlaceActResults } from "./ports/place-act.js";
|
|
5
|
+
import { placesIn } from "../fold/places.js";
|
|
6
|
+
import { appliedHistoryIn } from "../fold/signed.js";
|
|
7
|
+
import { storeOf } from "../land/ledger.js";
|
|
8
|
+
import { authenticatedRootStanding } from "./admitted-place.js";
|
|
9
|
+
import { foldClaims, fieldOf } from "../fold/claims.js";
|
|
10
|
+
const fail = (s) => { throw Error('REFUSE·contextual place ' + s); };
|
|
11
|
+
function read(path, limit) {
|
|
12
|
+
const fd = openSync(path, 'r');
|
|
13
|
+
try {
|
|
14
|
+
const stat = fstatSync(fd);
|
|
15
|
+
if (!stat.isFile() || stat.size > limit)
|
|
16
|
+
fail('batch byte budget');
|
|
17
|
+
const data = Buffer.alloc(stat.size);
|
|
18
|
+
let n = 0;
|
|
19
|
+
while (n < data.length) {
|
|
20
|
+
const got = readSync(fd, data, n, data.length - n, null);
|
|
21
|
+
if (!got)
|
|
22
|
+
fail('batch changed during read');
|
|
23
|
+
n += got;
|
|
24
|
+
}
|
|
25
|
+
if (readSync(fd, Buffer.alloc(1), 0, 1, null))
|
|
26
|
+
fail('batch changed during read');
|
|
27
|
+
return new TextDecoder('utf8', { fatal: true }).decode(data);
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
closeSync(fd);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function hasContextualPlace(root) { return hasRootClockSource(root); }
|
|
34
|
+
/** Exact selected SDK frame plus complete pre-signed receiver statements. No
|
|
35
|
+
* executable/verifier/epoch selection can be supplied by this envelope. */
|
|
36
|
+
export async function landContextualPlace(root, file) {
|
|
37
|
+
const selected = declaredRootClockSource(root), raw = read(file, selected.selection.budget.bytes);
|
|
38
|
+
let packet;
|
|
39
|
+
try {
|
|
40
|
+
packet = JSON.parse(raw);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return fail('invalid batch JSON');
|
|
44
|
+
}
|
|
45
|
+
if (!packet || Object.keys(packet).sort().join(',') !== 'context,profile,receipts' || packet.profile !== PLACE_ACT_CONTEXT || typeof packet.context !== 'string' || !Array.isArray(packet.receipts) || JSON.stringify(packet) + '\n' !== raw)
|
|
46
|
+
fail('unsupported/noncanonical batch envelope');
|
|
47
|
+
return commitPlaceAct(storeOf(root), packet.context, packet.receipts, selected.trust, selected.selection.storageBudget);
|
|
48
|
+
}
|
|
49
|
+
export function contextualAct(root, identity) {
|
|
50
|
+
const selected = declaredRootClockSource(root), bundle = placeActResult(storeOf(root), identity, selected.trust, selected.selection.storageBudget);
|
|
51
|
+
if (!bundle)
|
|
52
|
+
return undefined;
|
|
53
|
+
const all = new Map(committedPlaceActResults(storeOf(root), selected.trust, selected.selection.storageBudget).map(a => [a.identity, a])), kept = new Set(), contexts = [];
|
|
54
|
+
const visit = (id) => { if (kept.has(id))
|
|
55
|
+
return; const a = all.get(id); if (!a)
|
|
56
|
+
return; kept.add(id); const decoded = readPlaceActContext(a.bundle.context, selected.selection.budget); for (const p of decoded.places)
|
|
57
|
+
visit(p.previous); contexts.push({ identity: a.identity, context: a.bundle.context, receipts: a.bundle.receipts, authority: selected.authority(a.bundle.context) }); };
|
|
58
|
+
visit(identity);
|
|
59
|
+
return { profile: PLACE_ACT_CONTEXT, identity, context: bundle.context, receipts: bundle.receipts, contexts };
|
|
60
|
+
}
|
|
61
|
+
/** Carriers retain exact originals and authenticated context/receipts. Each
|
|
62
|
+
* namespace folds independently; labels assert no statistical independence. */
|
|
63
|
+
export function contextualStanding(root) {
|
|
64
|
+
const selected = declaredRootClockSource(root), acts = committedPlaceActResults(storeOf(root), selected.trust, selected.selection.storageBudget);
|
|
65
|
+
const groups = new Map();
|
|
66
|
+
for (const act of acts) {
|
|
67
|
+
const decoded = readPlaceActContext(act.bundle.context, selected.selection.budget);
|
|
68
|
+
for (const p of decoded.places) {
|
|
69
|
+
const key = JSON.stringify([decoded.root.namespace, p.place]), group = groups.get(key) ?? { namespace: decoded.root.namespace, place: p.place, lines: [], contexts: [] };
|
|
70
|
+
group.lines.push(...p.records);
|
|
71
|
+
group.contexts.push({ identity: act.identity, context: act.bundle.context, receipts: act.bundle.receipts });
|
|
72
|
+
groups.set(key, group);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const legacyPlaces = placesIn(root).map(place => { const history = appliedHistoryIn(root, place), folded = foldClaims(history); return { namespace: 'legacy-global', place: [place, 'TARGET.bound'].join('/'), standing: folded.standing.filter(line => fieldOf(line, 'value') !== 'withdraw'), history }; });
|
|
76
|
+
return { profile: PLACE_ACT_CONTEXT, root: authenticatedRootStanding(root), legacyPlaces, contexts: acts.map(a => ({ identity: a.identity, context: a.bundle.context, receipts: a.bundle.receipts, authority: selected.authority(a.bundle.context) })), places: [...groups.values()].map(group => { const folded = foldClaims(group.lines); return { namespace: group.namespace, place: group.place, standing: folded.standing.filter(line => fieldOf(line, 'value') !== 'withdraw'), retracted: folded.retracted, superseded: folded.superseded, forks: folded.forks, contexts: group.contexts }; }) };
|
|
77
|
+
}
|
package/dist/host/hash.d.ts
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
|
+
import { type Hash } from 'node:crypto';
|
|
1
2
|
/** SHA-256 byte hashing for host formats that explicitly name this algorithm. */
|
|
2
3
|
export declare const sha: (bytes: string | Uint8Array) => string;
|
|
4
|
+
/** Typed digest spelling shared by fixed host boundaries; hashes identical bytes. */
|
|
5
|
+
export declare const sha256: (bytes: string | Uint8Array) => string;
|
|
6
|
+
/** Length-delimited UTF-8 fields for existing host history identities. */
|
|
7
|
+
export declare function hashText(hash: Hash, value: string): void;
|
|
8
|
+
/** Exact typed SHA-256 spelling used by fixed host declarations. */
|
|
9
|
+
export declare const isSha256: (value: unknown) => value is string;
|
package/dist/host/hash.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
/** SHA-256 byte hashing for host formats that explicitly name this algorithm. */
|
|
3
3
|
export const sha = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
4
|
+
/** Typed digest spelling shared by fixed host boundaries; hashes identical bytes. */
|
|
5
|
+
export const sha256 = (bytes) => 'sha256:' + sha(bytes);
|
|
6
|
+
/** Length-delimited UTF-8 fields for existing host history identities. */
|
|
7
|
+
export function hashText(hash, value) { hash.update(String(Buffer.byteLength(value, 'utf8')) + ':'); hash.update(value, 'utf8'); }
|
|
8
|
+
/** Exact typed SHA-256 spelling used by fixed host declarations. */
|
|
9
|
+
export const isSha256 = (value) => typeof value === 'string' && /^sha256:[0-9a-f]{64}$/.test(value);
|
package/dist/host/io.d.ts
CHANGED
|
@@ -7,3 +7,5 @@ export { basename, dirname, join, relative, resolve } from 'node:path';
|
|
|
7
7
|
export { fileURLToPath } from 'node:url';
|
|
8
8
|
export { parentPort, workerData } from 'node:worker_threads';
|
|
9
9
|
export { gzipSync } from 'node:zlib';
|
|
10
|
+
/** UTF-8 encoding for host byte formats. */
|
|
11
|
+
export declare const encodeUTF8: (text: string) => Uint8Array;
|
package/dist/host/io.js
CHANGED
|
@@ -7,3 +7,5 @@ export { basename, dirname, join, relative, resolve } from 'node:path';
|
|
|
7
7
|
export { fileURLToPath } from 'node:url';
|
|
8
8
|
export { parentPort, workerData } from 'node:worker_threads';
|
|
9
9
|
export { gzipSync } from 'node:zlib';
|
|
10
|
+
/** UTF-8 encoding for host byte formats. */
|
|
11
|
+
export const encodeUTF8 = (text) => new TextEncoder().encode(text);
|
package/dist/host/lifetime.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const request = JSON.parse(readFileSync(0, 'utf8'));
|
|
5
|
-
if (!Number.isSafeInteger(request.limits.timeoutMs) || request.limits.timeoutMs <= 0 || !Number.isSafeInteger(request.limits.responseBytes) || request.limits.responseBytes <= 0)
|
|
2
|
+
const deadline = BigInt(process.argv[2] ?? '0'), responseBytes = Number(process.argv[3]);
|
|
3
|
+
if (deadline <= 0n || !Number.isSafeInteger(responseBytes) || responseBytes <= 0)
|
|
6
4
|
throw Error('REFUSE·reader invalid process limits');
|
|
7
|
-
|
|
8
|
-
const child = spawn(request.command, [...request.args], { cwd: request.cwd, env: request.env, detached: grouped, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
5
|
+
let child, finished = false, failure, size = 0;
|
|
9
6
|
const stdout = [], stderr = [];
|
|
10
|
-
|
|
11
|
-
const cancel = () => { if (child
|
|
7
|
+
const grouped = process.platform !== 'win32';
|
|
8
|
+
const cancel = () => { if (child?.pid === undefined)
|
|
12
9
|
return; try {
|
|
13
10
|
if (grouped)
|
|
14
11
|
process.kill(-child.pid, 'SIGKILL');
|
|
@@ -16,25 +13,67 @@ const cancel = () => { if (child.pid === undefined)
|
|
|
16
13
|
child.kill('SIGKILL');
|
|
17
14
|
}
|
|
18
15
|
catch { } };
|
|
19
|
-
const refuse = (why) => { failure ?? (failure = why); cancel(); };
|
|
20
|
-
const receive = (chunks, bytes) => { size += bytes.length; if (size > request.limits.responseBytes) {
|
|
21
|
-
refuse('response exceeds declared byte bound');
|
|
22
|
-
return;
|
|
23
|
-
} chunks.push(bytes); };
|
|
24
|
-
child.stdout.on('data', (bytes) => receive(stdout, bytes));
|
|
25
|
-
child.stderr.on('data', (bytes) => receive(stderr, bytes));
|
|
26
|
-
child.stdin.on('error', error => { if (!finished)
|
|
27
|
-
refuse(`input failed: ${error.message}`); });
|
|
28
|
-
const timer = setTimeout(() => refuse('declared timeout exhausted'), request.limits.timeoutMs);
|
|
29
16
|
const finish = (status, signal) => {
|
|
30
17
|
if (finished)
|
|
31
18
|
return;
|
|
32
19
|
finished = true;
|
|
33
20
|
clearTimeout(timer);
|
|
34
21
|
cancel();
|
|
35
|
-
|
|
22
|
+
child?.stdin?.destroy();
|
|
23
|
+
child?.stdout?.destroy();
|
|
24
|
+
child?.stderr?.destroy();
|
|
25
|
+
process.stdin.destroy();
|
|
26
|
+
const bytes = failure ? Buffer.alloc(0) : Buffer.concat(stdout);
|
|
27
|
+
const result = { stdout: bytes.toString('utf8'), stdoutBytes: bytes.toString('base64'), stderr: Buffer.concat(stderr).toString('utf8'), status, signal, ...(failure ? { failure } : {}) };
|
|
36
28
|
process.stdout.write(JSON.stringify(result));
|
|
37
29
|
};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
30
|
+
const refuse = (why) => { if (failure && why === 'response exceeds declared byte bound' && !failure.includes(why))
|
|
31
|
+
failure += ' · ' + why;
|
|
32
|
+
else
|
|
33
|
+
failure ?? (failure = why); finish(null, 'SIGKILL'); };
|
|
34
|
+
// A closed input pipe can precede the queued stderr/close events of an early
|
|
35
|
+
// rejecting child. Cancel immediately, but drain only until the SAME deadline.
|
|
36
|
+
const inputFailed = (why) => { if (finished)
|
|
37
|
+
return; failure ?? (failure = why); stdout.length = 0; cancel(); child?.stdin?.destroy(); process.stdin.destroy(); if (process.hrtime.bigint() >= deadline)
|
|
38
|
+
finish(null, 'SIGKILL'); };
|
|
39
|
+
const remaining = Number(deadline - process.hrtime.bigint()) / 1e6;
|
|
40
|
+
const timer = setTimeout(() => refuse('declared timeout exhausted'), Math.max(0, remaining));
|
|
41
|
+
process.once('SIGTERM', () => refuse('interrupted'));
|
|
42
|
+
try {
|
|
43
|
+
if (remaining <= 0)
|
|
44
|
+
refuse('declared timeout exhausted');
|
|
45
|
+
const chunks = [];
|
|
46
|
+
for await (const chunk of process.stdin) {
|
|
47
|
+
if (finished)
|
|
48
|
+
break;
|
|
49
|
+
chunks.push(Buffer.from(chunk));
|
|
50
|
+
}
|
|
51
|
+
if (!finished) {
|
|
52
|
+
const request = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
53
|
+
if (request.limits.responseBytes !== responseBytes || !Number.isSafeInteger(request.limits.timeoutMs) || request.limits.timeoutMs <= 0)
|
|
54
|
+
throw Error('invalid process limits');
|
|
55
|
+
// Parsing is synchronous; recheck before spawning even if the timer has not
|
|
56
|
+
// had a turn to run. An expired handoff must never start the command.
|
|
57
|
+
if (process.hrtime.bigint() >= deadline) {
|
|
58
|
+
refuse('declared timeout exhausted');
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
child = spawn(request.command, [...request.args], { cwd: request.cwd, env: request.env, detached: grouped, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
62
|
+
const receive = (items, bytes) => { if (finished)
|
|
63
|
+
return; size += bytes.length; if (size > responseBytes)
|
|
64
|
+
refuse('response exceeds declared byte bound');
|
|
65
|
+
else if (!failure || items === stderr)
|
|
66
|
+
items.push(bytes); };
|
|
67
|
+
child.stdout.on('data', (b) => receive(stdout, b));
|
|
68
|
+
child.stderr.on('data', (b) => receive(stderr, b));
|
|
69
|
+
child.stdin.on('error', error => inputFailed('input failed: ' + error.message));
|
|
70
|
+
child.once('error', error => refuse(error.message));
|
|
71
|
+
child.once('close', finish);
|
|
72
|
+
child.stdin.end(request.input);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
if (!finished)
|
|
78
|
+
refuse('input unavailable or malformed');
|
|
79
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The opened descriptor is bounded before allocation/read. Growth after fstat
|
|
2
|
+
* is detected with one sentinel byte; no oversized buffer reaches decode/hash.
|
|
3
|
+
*/
|
|
4
|
+
export declare function readBoundedFile(path: string, remaining: number, coordinate: string): Buffer;
|
|
5
|
+
export declare function readBoundedBatch(paths: readonly {
|
|
6
|
+
path: string;
|
|
7
|
+
coordinate: string;
|
|
8
|
+
}[], budget: number): readonly Buffer[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
/** The opened descriptor is bounded before allocation/read. Growth after fstat
|
|
3
|
+
* is detected with one sentinel byte; no oversized buffer reaches decode/hash.
|
|
4
|
+
*/
|
|
5
|
+
export function readBoundedFile(path, remaining, coordinate) {
|
|
6
|
+
if (!Number.isSafeInteger(remaining) || remaining < 0)
|
|
7
|
+
throw Error('REFUSE·native file missing byte budget ' + coordinate);
|
|
8
|
+
const fd = fs.openSync(path, 'r');
|
|
9
|
+
try {
|
|
10
|
+
const stat = fs.fstatSync(fd);
|
|
11
|
+
if (!stat.isFile() || !Number.isSafeInteger(stat.size) || stat.size > remaining)
|
|
12
|
+
throw Error('REFUSE·native file byte budget before read ' + coordinate);
|
|
13
|
+
const bytes = Buffer.alloc(stat.size);
|
|
14
|
+
let offset = 0;
|
|
15
|
+
while (offset < bytes.length) {
|
|
16
|
+
const count = fs.readSync(fd, bytes, offset, bytes.length - offset, null);
|
|
17
|
+
if (!count)
|
|
18
|
+
throw Error('REFUSE·native file changed during read ' + coordinate);
|
|
19
|
+
offset += count;
|
|
20
|
+
}
|
|
21
|
+
if (fs.readSync(fd, Buffer.alloc(1), 0, 1, null) !== 0)
|
|
22
|
+
throw Error('REFUSE·native file grew during read ' + coordinate);
|
|
23
|
+
return bytes;
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
fs.closeSync(fd);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function readBoundedBatch(paths, budget) {
|
|
30
|
+
let bytes = 0;
|
|
31
|
+
return paths.map(file => { const data = readBoundedFile(file.path, budget - bytes, file.coordinate); bytes += data.length; return data; });
|
|
32
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { CommandPort, CommandLot } from '@lapxo/topos/contract';
|
|
2
|
+
type Phase = 'selection' | 'help' | 'check' | 'preview' | 'sign' | 'act';
|
|
3
|
+
export interface Invocation {
|
|
4
|
+
readonly command: string;
|
|
5
|
+
readonly args: readonly string[];
|
|
6
|
+
readonly cwd: string;
|
|
7
|
+
readonly hostEnvelope?: Uint8Array;
|
|
8
|
+
readonly envelopeBytes?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface HostBoundary {
|
|
11
|
+
readonly admitsDeclaration: (port: CommandPort) => boolean;
|
|
12
|
+
readonly covers: (coordinate: string) => boolean;
|
|
13
|
+
readonly verifies: (digest: string) => boolean;
|
|
14
|
+
/** Host chooses a verified invocation. A capsule cannot supply a program. */
|
|
15
|
+
readonly resolve: (port: CommandPort) => Invocation;
|
|
16
|
+
/** Independent loader-closure proof, not a digest-list membership test. */
|
|
17
|
+
readonly loaderClosure: (port: CommandPort, invocation: Invocation) => 'verified' | 'unmeasured';
|
|
18
|
+
}
|
|
19
|
+
/** macOS prototype: bounded async invocation with POSIX child-group cancellation.
|
|
20
|
+
* No acquisition or persistence on prospective paths; no partial success frames.
|
|
21
|
+
* Host callbacks are trusted admission/verification operations, never signatures
|
|
22
|
+
* or authority assertions supplied by the command's own output. */
|
|
23
|
+
export declare function executeLot(options: {
|
|
24
|
+
port: CommandPort;
|
|
25
|
+
lot: CommandLot;
|
|
26
|
+
host: HostBoundary;
|
|
27
|
+
phase: Phase;
|
|
28
|
+
environment: Readonly<Record<string, string>>;
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
accepts: (member: unknown, index: number) => boolean;
|
|
31
|
+
}): Promise<{
|
|
32
|
+
readonly members: readonly unknown[];
|
|
33
|
+
readonly stderr: string;
|
|
34
|
+
readonly invocations: 1;
|
|
35
|
+
}>;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { channel } from 'node:diagnostics_channel';
|
|
3
|
+
import { isAbsolute } from 'node:path';
|
|
4
|
+
import { checkCommandPort, commandRequest, commandResponse } from '@lapxo/topos/contract';
|
|
5
|
+
import { requireEffects } from "../read-only.js";
|
|
6
|
+
import { assertNativeRuntime } from "./runtime.js";
|
|
7
|
+
import { scheduleDeadline } from "./deadline.js";
|
|
8
|
+
const rejectedCommand = channel('bound.command.refusal');
|
|
9
|
+
/** macOS prototype: bounded async invocation with POSIX child-group cancellation.
|
|
10
|
+
* No acquisition or persistence on prospective paths; no partial success frames.
|
|
11
|
+
* Host callbacks are trusted admission/verification operations, never signatures
|
|
12
|
+
* or authority assertions supplied by the command's own output. */
|
|
13
|
+
export async function executeLot(options) {
|
|
14
|
+
const start = process.hrtime.bigint();
|
|
15
|
+
const { host, signal, phase, accepts } = options;
|
|
16
|
+
// Admitted semantic configuration is snapshotted for the whole invocation;
|
|
17
|
+
// mutations of a caller's delivery object cannot alter checks mid-flight.
|
|
18
|
+
const port = Object.freeze({ ...options.port, closure: Object.freeze([...options.port.closure]), environment: Object.freeze([...options.port.environment]) });
|
|
19
|
+
const lot = Object.freeze({ ...options.lot, requests: Object.freeze([...options.lot.requests]) });
|
|
20
|
+
const environment = Object.freeze({ ...options.environment });
|
|
21
|
+
assertNativeRuntime(environment, []);
|
|
22
|
+
const refuse = (why) => { throw Error(`REFUSE·port ${port.coordinate} ${why}`); };
|
|
23
|
+
if (phase !== 'act')
|
|
24
|
+
refuse('invocation forbidden during ' + phase);
|
|
25
|
+
requireEffects('command ' + port.coordinate);
|
|
26
|
+
if (signal?.aborted)
|
|
27
|
+
refuse('interrupted before invocation');
|
|
28
|
+
checkCommandPort(port, host);
|
|
29
|
+
const deadline = start + BigInt(port.timeoutMs) * 1000000n;
|
|
30
|
+
const resolved = host.resolve(port), invocation = Object.freeze({ ...resolved, args: Object.freeze([...resolved.args]) });
|
|
31
|
+
if (!isAbsolute(invocation.command) || !isAbsolute(invocation.cwd))
|
|
32
|
+
refuse('host invocation needs absolute coordinates');
|
|
33
|
+
if (host.loaderClosure(port, invocation) !== 'verified')
|
|
34
|
+
refuse('runtime-loader closure unmeasured');
|
|
35
|
+
const names = Object.keys(environment).sort(), declared = [...new Set(port.environment)].sort();
|
|
36
|
+
if (names.length !== declared.length || names.some((name, index) => name !== declared[index]) || names.some(name => typeof environment[name] !== 'string' || environment[name].includes('\0')))
|
|
37
|
+
refuse('environment differs from declared handoff');
|
|
38
|
+
const frame = commandRequest(port, lot);
|
|
39
|
+
const envelope = invocation.hostEnvelope;
|
|
40
|
+
if (envelope && (!Number.isSafeInteger(invocation.envelopeBytes) || invocation.envelopeBytes < 1 || envelope.byteLength > invocation.envelopeBytes))
|
|
41
|
+
refuse('host envelope byte resources');
|
|
42
|
+
if (process.hrtime.bigint() >= deadline)
|
|
43
|
+
refuse('timeout before invocation');
|
|
44
|
+
if (signal?.aborted)
|
|
45
|
+
refuse('interrupted before invocation');
|
|
46
|
+
const result = await new Promise((resolve, reject) => {
|
|
47
|
+
const child = spawn(invocation.command, [...invocation.args], { cwd: invocation.cwd, env: { ...environment }, shell: false, detached: false, stdio: envelope ? ['pipe', 'pipe', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe'] });
|
|
48
|
+
const stdout = [], stderr = [];
|
|
49
|
+
let bytes = 0, why, done = false;
|
|
50
|
+
const stop = (cause) => { if (why)
|
|
51
|
+
return; why = cause; try {
|
|
52
|
+
child.kill('SIGKILL');
|
|
53
|
+
}
|
|
54
|
+
catch { } child.stdin.destroy(); if (envelope)
|
|
55
|
+
child.stdio[3]?.destroy(); };
|
|
56
|
+
const cancelTimer = scheduleDeadline(deadline, () => stop('timeout'));
|
|
57
|
+
const abort = () => stop('interrupted');
|
|
58
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
59
|
+
if (signal?.aborted)
|
|
60
|
+
abort();
|
|
61
|
+
const collect = (target, chunk, channel) => { if (why)
|
|
62
|
+
return; bytes += chunk.length; if (bytes > port.responseBytes) {
|
|
63
|
+
stop('response-bytes exceeded');
|
|
64
|
+
return;
|
|
65
|
+
} if (channel === 'stderr' && port.stderr === 'reject') {
|
|
66
|
+
rejectedCommand.publish({ coordinate: port.coordinate, stderr: chunk.subarray(0, Math.min(port.responseBytes, 1024)).toString('utf8') });
|
|
67
|
+
stop('stderr forbidden');
|
|
68
|
+
return;
|
|
69
|
+
} target.push(chunk); };
|
|
70
|
+
child.stdout.on('data', (chunk) => collect(stdout, chunk, 'stdout'));
|
|
71
|
+
child.stderr.on('data', (chunk) => collect(stderr, chunk, 'stderr'));
|
|
72
|
+
child.stdin.on('error', () => stop('request handoff failed'));
|
|
73
|
+
const finish = (status, error) => { if (done)
|
|
74
|
+
return; done = true; cancelTimer(); signal?.removeEventListener('abort', abort); if (error || why) {
|
|
75
|
+
reject(Error(`REFUSE·port ${port.coordinate} ${why ?? error.message}`));
|
|
76
|
+
return;
|
|
77
|
+
} resolve({ status, interrupted: false, stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8') }); };
|
|
78
|
+
child.once('error', error => finish(null, error));
|
|
79
|
+
child.once('close', status => finish(status));
|
|
80
|
+
if (envelope) {
|
|
81
|
+
const pipe = child.stdio[3];
|
|
82
|
+
const header = Buffer.alloc(8);
|
|
83
|
+
header.writeBigUInt64BE(BigInt(envelope.byteLength));
|
|
84
|
+
pipe.on('error', () => stop('host envelope handoff failed'));
|
|
85
|
+
pipe.end(Buffer.concat([header, Buffer.from(envelope)]));
|
|
86
|
+
}
|
|
87
|
+
child.stdin.end(frame);
|
|
88
|
+
});
|
|
89
|
+
const members = commandResponse(port, lot, result, accepts);
|
|
90
|
+
return { members, stderr: result.stderr, invocations: 1 };
|
|
91
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** Sealed Root control code: one fresh custodian connection per request. */
|
|
2
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
3
|
+
import { createConnection } from 'node:net';
|
|
4
|
+
const { endpoint, deadlineMs, shared, inputBytes, outputBytes } = workerData;
|
|
5
|
+
const port = parentPort;
|
|
6
|
+
if (!port)
|
|
7
|
+
throw Error('REFUSE·Root control transport parent absent');
|
|
8
|
+
const header = new Int32Array(shared, 0, 8), input = new Uint8Array(shared, 32, inputBytes), output = new Uint8Array(shared, 32 + inputBytes, outputBytes);
|
|
9
|
+
const initial = Math.min(endpoint.timeoutMs, deadlineMs - Date.now());
|
|
10
|
+
const deadline = process.hrtime.bigint() + BigInt(Math.max(0, initial)) * 1000000n;
|
|
11
|
+
const remaining = () => Math.max(0, Number((deadline - process.hrtime.bigint() + 999999n) / 1000000n));
|
|
12
|
+
let busy = false, red, active;
|
|
13
|
+
function finish(state, bytes) {
|
|
14
|
+
if (bytes.byteLength > outputBytes) {
|
|
15
|
+
state = 3;
|
|
16
|
+
bytes = Buffer.from('REFUSE·reply bytes').subarray(0, outputBytes);
|
|
17
|
+
}
|
|
18
|
+
output.set(bytes);
|
|
19
|
+
Atomics.store(header, 3, bytes.byteLength);
|
|
20
|
+
Atomics.store(header, 0, state);
|
|
21
|
+
Atomics.notify(header, 0);
|
|
22
|
+
}
|
|
23
|
+
// Timer slices never widen the original monotonic deadline.
|
|
24
|
+
let timer;
|
|
25
|
+
function arm() {
|
|
26
|
+
const left = remaining();
|
|
27
|
+
if (left > 0) {
|
|
28
|
+
timer = setTimeout(arm, Math.min(left, 2147483647));
|
|
29
|
+
timer.unref();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
red ?? (red = 'REFUSE·parent deadline');
|
|
33
|
+
active?.destroy(Error(red));
|
|
34
|
+
if (Atomics.load(header, 0) === 1)
|
|
35
|
+
finish(3, Buffer.from(red).subarray(0, outputBytes));
|
|
36
|
+
port.close();
|
|
37
|
+
}
|
|
38
|
+
arm();
|
|
39
|
+
port.on('close', () => { clearTimeout(timer); active?.destroy(); });
|
|
40
|
+
port.on('message', async (message) => {
|
|
41
|
+
if (busy || red || message !== Atomics.load(header, 1) || Atomics.load(header, 0) !== 1) {
|
|
42
|
+
red ?? (red = 'REFUSE·sequence/state');
|
|
43
|
+
finish(3, Buffer.from(red).subarray(0, outputBytes));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
busy = true;
|
|
47
|
+
try {
|
|
48
|
+
const length = Atomics.load(header, 2);
|
|
49
|
+
if (length < 1 || length > inputBytes)
|
|
50
|
+
throw Error('REFUSE·request bytes');
|
|
51
|
+
if (remaining() <= 0)
|
|
52
|
+
throw Error('REFUSE·parent deadline');
|
|
53
|
+
const frame = Buffer.from(input.slice(0, length));
|
|
54
|
+
const bytes = await new Promise((resolve, reject) => {
|
|
55
|
+
const socket = createConnection(endpoint.socket);
|
|
56
|
+
active = socket;
|
|
57
|
+
const chunks = [];
|
|
58
|
+
let size = 0, done = false;
|
|
59
|
+
const end = (error) => { if (done)
|
|
60
|
+
return; done = true; if (error)
|
|
61
|
+
reject(error);
|
|
62
|
+
else
|
|
63
|
+
resolve(Buffer.concat(chunks)); };
|
|
64
|
+
socket.once('connect', () => socket.write(Buffer.concat([frame, Buffer.from('\n')])));
|
|
65
|
+
socket.on('data', chunk => { size += chunk.length; if (size > outputBytes)
|
|
66
|
+
socket.destroy(Error('REFUSE·reply bytes'));
|
|
67
|
+
else
|
|
68
|
+
chunks.push(chunk); });
|
|
69
|
+
socket.once('error', end);
|
|
70
|
+
socket.once('close', () => end());
|
|
71
|
+
});
|
|
72
|
+
active = undefined;
|
|
73
|
+
if (!bytes.length)
|
|
74
|
+
throw Error('REFUSE·interrupted before reply');
|
|
75
|
+
if (remaining() <= 0)
|
|
76
|
+
throw Error('REFUSE·parent deadline');
|
|
77
|
+
busy = false;
|
|
78
|
+
finish(2, bytes);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
active = undefined;
|
|
82
|
+
red ?? (red = error instanceof Error ? error.message : String(error));
|
|
83
|
+
busy = false;
|
|
84
|
+
finish(3, Buffer.from(red).subarray(0, outputBytes));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
Atomics.store(header, 5, 1);
|
|
88
|
+
Atomics.notify(header, 5);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface ControlEndpoint {
|
|
2
|
+
readonly socket: string;
|
|
3
|
+
readonly timeoutMs: number;
|
|
4
|
+
readonly deadlineMs: number;
|
|
5
|
+
readonly responseBytes: number;
|
|
6
|
+
}
|
|
7
|
+
export interface FixedControlBinding {
|
|
8
|
+
readonly endpoint: ControlEndpoint;
|
|
9
|
+
readonly workerBytes: Uint8Array;
|
|
10
|
+
readonly worker: {
|
|
11
|
+
readonly digest: string;
|
|
12
|
+
readonly coordinate: string;
|
|
13
|
+
readonly bytes: number;
|
|
14
|
+
};
|
|
15
|
+
readonly runtime: {
|
|
16
|
+
readonly path: string;
|
|
17
|
+
readonly digest: string;
|
|
18
|
+
readonly bytes: number;
|
|
19
|
+
};
|
|
20
|
+
readonly resources: {
|
|
21
|
+
readonly inputBytes: number;
|
|
22
|
+
readonly outputBytes: number;
|
|
23
|
+
readonly sharedBytes: number;
|
|
24
|
+
};
|
|
25
|
+
readonly deadlineMs: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function createFixedControlBridge(inputBinding: FixedControlBinding): {
|
|
28
|
+
exchange(frame: string): string;
|
|
29
|
+
close(): Promise<void>;
|
|
30
|
+
};
|