@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,267 @@
|
|
|
1
|
+
import { sha256 as sha } from "../hash.js";
|
|
2
|
+
/** One asynchronous Root custodian owns aggregate counters for one actual job.
|
|
3
|
+
* Endpoint is injected by instrument code, never selected by a world payload. */
|
|
4
|
+
import { createServer, createConnection } from 'node:net';
|
|
5
|
+
import { chmodSync, rmSync, realpathSync, writeFileSync, renameSync, existsSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { randomBytes } from 'node:crypto';
|
|
8
|
+
import { canonicalJson } from "./reading.js";
|
|
9
|
+
import { verifyNativeBinary, nativeBinaryProjection } from "./native-binary.js";
|
|
10
|
+
import { commandRequest, commandResponse } from '@lapxo/topos/contract';
|
|
11
|
+
import { assertProducerInterpretation, chargeProducerRequest, createGeneratedProducer, readBounded, validateDescendantInvocation, sealGeneratedFile, sealNativeEval, sealInterpretation, recordProducerEmission, recordProducerEmissions, registerProducerCandidates, gateDerived, producerTransport, producerUsage } from "./generated.js";
|
|
12
|
+
import { retainedNativeEvalMatches } from "./node-launch.js";
|
|
13
|
+
import { verifyNativeProgram, nativeProgramProjection } from "./native-program.js";
|
|
14
|
+
/** Refusal transport is bounded too; an undersized channel closes without a
|
|
15
|
+
* frame while the parent retains the named refusal. No truncation is success. */
|
|
16
|
+
export function boundedRefusalFrame(endpoint, why) { if (why.length > endpoint.responseBytes || Buffer.byteLength(why) > endpoint.responseBytes)
|
|
17
|
+
return; const frame = JSON.stringify({ protocol: 'command-lot/1', coordinate: endpoint.coordinate, implementation: endpoint.implementation, identity: endpoint.identity, responses: [{ kind: 'refuse', why }] }) + '\n'; return Buffer.byteLength(frame) <= endpoint.responseBytes ? frame : undefined; }
|
|
18
|
+
export async function startProducerCustodian(handle, snapshot, nativeImplementations, directory, inputNativePrograms = []) {
|
|
19
|
+
const nativePrograms = inputNativePrograms.map(grant => ({ identity: grant.identity, program: { ...grant.program } }));
|
|
20
|
+
if (nativePrograms.some(g => !/^sha256:[0-9a-f]{64}$/.test(g.identity) || g.program.kind !== 'native-binary' || typeof g.program.alias !== 'string' || !g.program.alias.length || !g.program.entry.startsWith('/') || !/^sha256:[0-9a-f]{64}$/.test(g.program.entryDigest) || !Number.isSafeInteger(g.program.entryBytes) || g.program.entryBytes < 1 || g.program.entryBytes > snapshot.budget.bytes || !g.program.condition) || new Set(nativePrograms.map(g => g.program.alias)).size !== nativePrograms.length)
|
|
21
|
+
throw Error('REFUSE·producer native program selection ambiguous/over budget');
|
|
22
|
+
const selected = producerTransport(handle), endpoint = { ...selected, deadlineMs: Date.now() + selected.timeoutMs, socket: join(directory, 'p-' + randomBytes(6).toString('hex')), token: randomBytes(32).toString('hex') }, connections = new Set(), unfinishedRequests = new Set(), refusals = [], knownDigests = new Set(snapshot.members.map(m => m.digest));
|
|
23
|
+
const retainedContexts = new Map(), retainedValues = new Map();
|
|
24
|
+
let contextBytes = 0;
|
|
25
|
+
const reconstruct = (compact) => { const full = { ...compact }; for (const key of ['invocation', 'edges'])
|
|
26
|
+
if (full[key]?.retainedValue) {
|
|
27
|
+
const value = retainedValues.get(full[key].retainedValue);
|
|
28
|
+
if (value === undefined)
|
|
29
|
+
throw Error('REFUSE·producer retained value absent');
|
|
30
|
+
full[key] = value;
|
|
31
|
+
} return full; };
|
|
32
|
+
const retainContext = (context) => { const held = retainedContexts.get(context.identity); if (held) {
|
|
33
|
+
if (canonicalJson(reconstruct(held)) !== canonicalJson(context))
|
|
34
|
+
throw Error('REFUSE·producer context identity collision');
|
|
35
|
+
return;
|
|
36
|
+
} const compact = { ...context }, fresh = new Map(); for (const key of ['invocation', 'edges']) {
|
|
37
|
+
const value = context[key];
|
|
38
|
+
if (value && typeof value === 'object') {
|
|
39
|
+
const encoded = canonicalJson(value), id = sha(encoded), prior = retainedValues.get(id);
|
|
40
|
+
if (prior !== undefined && canonicalJson(prior) !== encoded)
|
|
41
|
+
throw Error('REFUSE·producer retained value collision');
|
|
42
|
+
if (prior === undefined && !fresh.has(id))
|
|
43
|
+
fresh.set(id, JSON.parse(encoded));
|
|
44
|
+
compact[key] = { retainedValue: id };
|
|
45
|
+
}
|
|
46
|
+
} let bytes = Buffer.byteLength(canonicalJson(compact)); for (const [id, value] of fresh)
|
|
47
|
+
bytes += Buffer.byteLength(canonicalJson([id, value])); if (bytes > selected.responseBytes - contextBytes)
|
|
48
|
+
throw Error('REFUSE·producer retained contexts bytes'); for (const [id, value] of fresh)
|
|
49
|
+
retainedValues.set(id, value); retainedContexts.set(context.identity, compact); contextBytes += bytes; };
|
|
50
|
+
const server = createServer(socket => {
|
|
51
|
+
try {
|
|
52
|
+
chargeProducerRequest(handle);
|
|
53
|
+
if (Date.now() >= endpoint.deadlineMs)
|
|
54
|
+
throw Error('REFUSE·producer parent deadline');
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
refusals.push(error instanceof Error ? error.message : String(error));
|
|
58
|
+
socket.destroy();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
connections.add(socket);
|
|
62
|
+
unfinishedRequests.add(socket);
|
|
63
|
+
const finishReply = (reply) => { const done = () => unfinishedRequests.delete(socket); if (reply === undefined)
|
|
64
|
+
socket.end(done);
|
|
65
|
+
else
|
|
66
|
+
socket.end(reply, done); };
|
|
67
|
+
socket.once('error', () => { if (!refusals.length)
|
|
68
|
+
refusals.push('REFUSE·producer IPC response interrupted'); });
|
|
69
|
+
socket.setTimeout(Math.max(1, endpoint.deadlineMs - Date.now()), () => { refusals.push('REFUSE·producer IPC timeout'); socket.destroy(); });
|
|
70
|
+
const chunks = [];
|
|
71
|
+
let bytes = 0, handled = false;
|
|
72
|
+
socket.on('data', chunk => {
|
|
73
|
+
bytes += chunk.length;
|
|
74
|
+
if (bytes > endpoint.responseBytes) {
|
|
75
|
+
refusals.push('REFUSE·producer IPC request bytes');
|
|
76
|
+
socket.destroy();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
chunks.push(chunk);
|
|
80
|
+
if (!chunk.includes(10))
|
|
81
|
+
return;
|
|
82
|
+
if (handled)
|
|
83
|
+
return;
|
|
84
|
+
handled = true;
|
|
85
|
+
socket.pause();
|
|
86
|
+
try {
|
|
87
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
88
|
+
if (refusals.length)
|
|
89
|
+
throw Error(refusals[0]);
|
|
90
|
+
const frame = JSON.parse(raw.trim());
|
|
91
|
+
if (frame.protocol !== 'command-lot/1' || frame.coordinate !== endpoint.coordinate || frame.implementation !== endpoint.implementation || frame.identity !== endpoint.identity || !Array.isArray(frame.requests) || frame.requests.length !== 1)
|
|
92
|
+
throw Error('REFUSE·producer IPC frame');
|
|
93
|
+
const body = JSON.parse(frame.requests[0]);
|
|
94
|
+
if (body.token !== endpoint.token || !['entry', 'emission', 'emission-batch', 'native-program', 'interpretation', 'eval-context', 'violation'].includes(body.operation))
|
|
95
|
+
throw Error('REFUSE·producer IPC capability');
|
|
96
|
+
if (body.operation === 'violation') {
|
|
97
|
+
if (typeof body.why !== 'string' || body.why.length > endpoint.responseBytes || Buffer.byteLength(body.why) > endpoint.responseBytes)
|
|
98
|
+
throw Error('REFUSE·producer violation bytes');
|
|
99
|
+
throw Error('REFUSE·producer pre-entry ' + body.why);
|
|
100
|
+
}
|
|
101
|
+
let context;
|
|
102
|
+
if (body.operation === 'eval-context') {
|
|
103
|
+
const actual = body.request;
|
|
104
|
+
const matches = [...retainedContexts.values()].map(reconstruct).filter(record => retainedNativeEvalMatches(record, actual));
|
|
105
|
+
if (matches.length !== 1)
|
|
106
|
+
throw Error('REFUSE·producer retained eval context ' + (matches.length ? 'ambiguous' : 'absent'));
|
|
107
|
+
context = matches[0];
|
|
108
|
+
}
|
|
109
|
+
else if (body.operation === 'emission-batch') {
|
|
110
|
+
const request = body.request;
|
|
111
|
+
if (!request || !Array.isArray(request.outputs) || request.outputs.length > selected.limits.work)
|
|
112
|
+
throw Error('REFUSE·producer emission batch context/count absent');
|
|
113
|
+
context = recordProducerEmissions(handle, request.outputs, request.invocation);
|
|
114
|
+
}
|
|
115
|
+
else if (body.operation === 'emission') {
|
|
116
|
+
const request = body.request;
|
|
117
|
+
if (!request || typeof request.path !== 'string')
|
|
118
|
+
throw Error('REFUSE·producer emission context absent');
|
|
119
|
+
context = recordProducerEmission(handle, request.path, request.invocation);
|
|
120
|
+
}
|
|
121
|
+
else if (body.operation === 'native-program') {
|
|
122
|
+
const request = body.request;
|
|
123
|
+
if (!request || typeof request.alias !== 'string' || typeof request.path !== 'string')
|
|
124
|
+
throw Error('REFUSE·producer native invocation absent');
|
|
125
|
+
const grant = nativePrograms.find(g => g.program.alias === request.alias);
|
|
126
|
+
if (!grant)
|
|
127
|
+
throw Error('REFUSE·producer native program alias undeclared ' + request.alias);
|
|
128
|
+
const invocation = validateDescendantInvocation(handle, request.invocation);
|
|
129
|
+
let actual;
|
|
130
|
+
try {
|
|
131
|
+
actual = realpathSync(request.path);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
throw Error('REFUSE·producer native program target absent');
|
|
135
|
+
}
|
|
136
|
+
if (actual !== grant.program.entry)
|
|
137
|
+
throw Error('REFUSE·producer native program route differs');
|
|
138
|
+
const bytes = readBounded(actual, grant.program.entryBytes), digest = sha(bytes);
|
|
139
|
+
if (bytes.length !== grant.program.entryBytes || digest !== grant.program.entryDigest)
|
|
140
|
+
throw Error('REFUSE·producer native program bytes differ');
|
|
141
|
+
const transaction = { kind: 'native-program-transaction/1', parent: selected.parent, producer: selected.identity, program: grant.identity, alias: grant.program.alias, target: actual, digest, byteLength: bytes.length, invocation, condition: grant.program.condition };
|
|
142
|
+
context = { ...transaction, identity: sha(JSON.stringify(transaction)) };
|
|
143
|
+
}
|
|
144
|
+
else if (body.operation === 'interpretation') {
|
|
145
|
+
const request = body.request;
|
|
146
|
+
if (!request || typeof request.path !== 'string' || typeof request.interpretation !== 'string')
|
|
147
|
+
throw Error('REFUSE·producer interpretation context absent');
|
|
148
|
+
context = sealInterpretation(handle, request.path, request.interpretation, request.invocation);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const request = body.request;
|
|
152
|
+
if (!request || !['module', 'commonjs'].includes(request.mode))
|
|
153
|
+
throw Error('REFUSE·producer unknown entry mode');
|
|
154
|
+
if (request.candidates !== undefined)
|
|
155
|
+
registerProducerCandidates(handle, request.candidates);
|
|
156
|
+
if (request.kind === 'file') {
|
|
157
|
+
if (request.interpretation !== undefined) {
|
|
158
|
+
assertProducerInterpretation(handle, request.interpretation);
|
|
159
|
+
if (request.interpretation !== 'node-ambiguous-javascript@1')
|
|
160
|
+
throw Error('REFUSE·producer entry interpretation unsupported');
|
|
161
|
+
}
|
|
162
|
+
if (!request.path)
|
|
163
|
+
throw Error('REFUSE·producer missing path');
|
|
164
|
+
const path = realpathSync(request.path), actual = readBounded(path, snapshot.budget.bytes);
|
|
165
|
+
if (actual.length > snapshot.budget.bytes)
|
|
166
|
+
throw Error('REFUSE·producer static input bytes');
|
|
167
|
+
const digest = sha(actual);
|
|
168
|
+
if (knownDigests.has(digest)) {
|
|
169
|
+
context = { kind: 'mounted-executable/1', parent: selected.parent, producer: selected.identity, path, digest, mode: request.mode, ...(request.interpretation === undefined ? {} : { interpretation: request.interpretation }), invocation: validateDescendantInvocation(handle, request.invocation), identity: sha(JSON.stringify([selected.parent, selected.identity, path, digest, request.mode, request.invocation, ...(request.interpretation === undefined ? [] : [request.interpretation])])), qualification: nativeImplementations.has(digest) ? 'separately selected privileged native implementation' : 'same sealed content mounted at actual path' };
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const record = sealGeneratedFile(handle, path, request.mode, request.invocation, request.interpretation), edges = gateDerived(handle, record, { path }, snapshot, nativeImplementations);
|
|
173
|
+
context = { ...record, edges };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else if (request.kind === 'eval') {
|
|
177
|
+
if (!request.argv || !request.rootPrefix)
|
|
178
|
+
throw Error('REFUSE·producer missing eval binding');
|
|
179
|
+
const record = sealNativeEval(handle, request.argv, request.rootPrefix, request.invocation);
|
|
180
|
+
if (record.mode !== request.mode)
|
|
181
|
+
throw Error('REFUSE·producer eval mode differs from argv');
|
|
182
|
+
const edges = gateDerived(handle, record, { argv: request.argv }, snapshot, nativeImplementations);
|
|
183
|
+
context = { ...record, edges };
|
|
184
|
+
}
|
|
185
|
+
else
|
|
186
|
+
throw Error('REFUSE·producer unknown entry kind');
|
|
187
|
+
}
|
|
188
|
+
retainContext(context);
|
|
189
|
+
const reply = JSON.stringify({ protocol: 'command-lot/1', coordinate: endpoint.coordinate, implementation: endpoint.implementation, identity: endpoint.identity, responses: [{ kind: 'entry-context', context, usage: producerUsage(handle) }] }) + '\n';
|
|
190
|
+
if (Buffer.byteLength(reply) > endpoint.responseBytes)
|
|
191
|
+
throw Error('REFUSE·producer IPC response bytes');
|
|
192
|
+
finishReply(reply);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
196
|
+
if (!refusals.length)
|
|
197
|
+
refusals.push(why);
|
|
198
|
+
const refusal = boundedRefusalFrame(endpoint, why);
|
|
199
|
+
if (refusal === undefined) {
|
|
200
|
+
finishReply();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
finishReply(refusal);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
socket.once('close', () => { if (unfinishedRequests.delete(socket) && !refusals.length)
|
|
207
|
+
refusals.push('REFUSE·producer IPC interrupted before bounded reply'); connections.delete(socket); });
|
|
208
|
+
});
|
|
209
|
+
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(endpoint.socket, () => { chmodSync(endpoint.socket, 0o600); resolve(); }); });
|
|
210
|
+
const deadline = setTimeout(() => { refusals.push('REFUSE·producer parent deadline'); for (const socket of connections)
|
|
211
|
+
socket.destroy(); }, endpoint.timeoutMs);
|
|
212
|
+
return { endpoint, get contexts() { return [...retainedContexts.values()].map(reconstruct); }, retention: () => ({ bytes: contextBytes, contexts: [...retainedContexts.values()].map(value => JSON.parse(canonicalJson(value))), values: [...retainedValues].map(([id, value]) => [id, JSON.parse(canonicalJson(value))]) }), assertClosed: () => { if (Date.now() >= endpoint.deadlineMs)
|
|
213
|
+
throw Error('REFUSE·producer parent deadline'); if (refusals.length)
|
|
214
|
+
throw Error(refusals[0]); if (unfinishedRequests.size)
|
|
215
|
+
throw Error('REFUSE·producer IPC unfinished'); }, refusals, usage: () => producerUsage(handle), close: async () => { clearTimeout(deadline); for (const socket of connections)
|
|
216
|
+
socket.destroy(); await new Promise(resolve => server.close(() => resolve())); rmSync(endpoint.socket, { force: true }); } };
|
|
217
|
+
}
|
|
218
|
+
/** Asynchronous client for original async native APIs. Original sync APIs use
|
|
219
|
+
* the fixed client executable; both address this SAME custodian/counters. */
|
|
220
|
+
export async function requestRetainedEvalContext(endpoint, actual) { return requestProducerOperation(endpoint, 'eval-context', actual); }
|
|
221
|
+
export async function requestProducerEntry(endpoint, request) { return requestProducerOperation(endpoint, 'entry', request); }
|
|
222
|
+
export async function requestProducerInterpretation(endpoint, request) { return requestProducerOperation(endpoint, 'interpretation', request); }
|
|
223
|
+
export async function requestNativeProgram(endpoint, request) { return requestProducerOperation(endpoint, 'native-program', request); }
|
|
224
|
+
export async function requestProducerEmission(endpoint, request) { return requestProducerOperation(endpoint, 'emission', request); }
|
|
225
|
+
export async function requestProducerEmissionBatch(endpoint, request) { return requestProducerOperation(endpoint, 'emission-batch', request); }
|
|
226
|
+
async function requestProducerOperation(endpoint, operation, request) { const port = { coordinate: endpoint.coordinate, implementation: endpoint.implementation, closure: [endpoint.implementation], timeoutMs: endpoint.timeoutMs, responseBytes: endpoint.responseBytes, environment: [], stderr: 'reject' }, lot = { identity: endpoint.identity, requests: [JSON.stringify({ operation, token: endpoint.token, request })] }, frame = commandRequest(port, lot); if (Buffer.byteLength(frame) > endpoint.responseBytes)
|
|
227
|
+
throw Error('REFUSE·producer IPC request bytes'); const raw = await new Promise((resolve, reject) => { const socket = createConnection(endpoint.socket), chunks = [], timer = setTimeout(() => socket.destroy(Error('REFUSE·producer IPC timeout')), Math.max(1, Math.min(endpoint.timeoutMs, endpoint.deadlineMs - Date.now()))); let bytes = 0; socket.on('connect', () => socket.write(frame + '\n')); socket.on('data', c => { bytes += c.length; if (bytes > endpoint.responseBytes)
|
|
228
|
+
socket.destroy(Error('REFUSE·producer IPC response bytes'));
|
|
229
|
+
else
|
|
230
|
+
chunks.push(c); }); socket.once('error', reject); socket.once('close', () => { clearTimeout(timer); resolve(Buffer.concat(chunks).toString('utf8')); }); }); const values = commandResponse(port, lot, { status: 0, interrupted: false, stdout: raw, stderr: '' }, value => !!value && typeof value === 'object' && ['entry-context', 'refuse'].includes(value.kind)); const one = values[0]; if (one.kind === 'refuse')
|
|
231
|
+
throw Error(one.why); return one.context; }
|
|
232
|
+
/** Bound's guarded host constructs this factory, not a world. The transport path
|
|
233
|
+
* is selected by the sealed native preload configuration, not a grant in data.
|
|
234
|
+
* A shared context file cannot be multiplexed across simultaneous bindings. */
|
|
235
|
+
export function createProducerJobCustody(input) { let active = false; return { async open(parent) { if (active || existsSync(input.contextFile))
|
|
236
|
+
throw Error('REFUSE·producer execution context occupied'); active = true; let service; const temporary = input.contextFile + '.' + randomBytes(6).toString('hex'); try {
|
|
237
|
+
const programs = (input.programSelections ?? []).map(selected => nativeProgramProjection(verifyNativeProgram(input.root, selected.coordinate, parent, selected.runtimeBinding)));
|
|
238
|
+
const handle = createGeneratedProducer(input.root, input.coordinate, parent, input.implementationBytes);
|
|
239
|
+
const nativePrograms = (input.nativeBinaries ?? []).map(binding => { const verified = verifyNativeBinary(input.root, binding.coordinate, parent, binding.privateBinding); return { identity: verified.identity, program: nativeBinaryProjection(verified) }; });
|
|
240
|
+
service = await startProducerCustodian(handle, input.snapshot, input.nativeImplementations, input.directory, nativePrograms);
|
|
241
|
+
const declaredPrograms = [...programs, ...nativePrograms.map(g => g.program)];
|
|
242
|
+
if (new Set(declaredPrograms.map(p => p.alias)).size !== declaredPrograms.length)
|
|
243
|
+
throw Error('REFUSE·producer native alias ambiguous');
|
|
244
|
+
const text = JSON.stringify({ endpoint: service.endpoint, parentExecution: parent, programs: declaredPrograms });
|
|
245
|
+
if (Buffer.byteLength(text) > parent.responseBytes)
|
|
246
|
+
throw Error('REFUSE·producer execution context bytes');
|
|
247
|
+
writeFileSync(temporary, text, { flag: 'wx', mode: 0o600 });
|
|
248
|
+
renameSync(temporary, input.contextFile);
|
|
249
|
+
const held = service;
|
|
250
|
+
return { assertClosed: () => held.assertClosed(), async close() { try {
|
|
251
|
+
await held.close();
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
rmSync(input.contextFile, { force: true });
|
|
255
|
+
active = false;
|
|
256
|
+
} } };
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
rmSync(temporary, { force: true });
|
|
260
|
+
await service?.close();
|
|
261
|
+
active = false;
|
|
262
|
+
throw error;
|
|
263
|
+
} } }; }
|
|
264
|
+
/** Fixed Root preload reports a local before-entry refusal to the SAME parent
|
|
265
|
+
* custodian. A caught child error therefore cannot become a success receipt. */
|
|
266
|
+
export async function reportProducerViolation(endpoint, why) { if (typeof why !== 'string' || why.length > endpoint.responseBytes || Buffer.byteLength(why) > endpoint.responseBytes)
|
|
267
|
+
throw Error('REFUSE·producer violation bytes'); const frame = JSON.stringify({ protocol: 'command-lot/1', coordinate: endpoint.coordinate, implementation: endpoint.implementation, identity: endpoint.identity, requests: [JSON.stringify({ operation: 'violation', token: endpoint.token, why })] }); await new Promise((resolve, reject) => { const socket = createConnection(endpoint.socket), timer = setTimeout(() => socket.destroy(Error('REFUSE·producer IPC timeout')), Math.max(1, endpoint.deadlineMs - Date.now())); socket.once('connect', () => socket.write(frame + '\n')); socket.on('data', () => { }); socket.once('error', reject); socket.once('close', () => { clearTimeout(timer); resolve(); }); }); throw Error('REFUSE·producer pre-entry ' + why); }
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { type RootStanding } from './reading.ts';
|
|
2
|
+
export interface ProducerLimits {
|
|
3
|
+
members: number;
|
|
4
|
+
bytes: number;
|
|
5
|
+
work: number;
|
|
6
|
+
argvBytes: number;
|
|
7
|
+
argvItems: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ProducerProfile {
|
|
10
|
+
profile: 'host-generated-executable@3';
|
|
11
|
+
coordinate: string;
|
|
12
|
+
implementation: string;
|
|
13
|
+
interpretations: readonly string[];
|
|
14
|
+
environmentNames: readonly string[];
|
|
15
|
+
selector: {
|
|
16
|
+
port: string;
|
|
17
|
+
binding: string;
|
|
18
|
+
implementation: string;
|
|
19
|
+
};
|
|
20
|
+
limits: ProducerLimits;
|
|
21
|
+
}
|
|
22
|
+
export interface ParentExecution {
|
|
23
|
+
identity: string;
|
|
24
|
+
port: string;
|
|
25
|
+
binding: string;
|
|
26
|
+
implementation: string;
|
|
27
|
+
instrument: string;
|
|
28
|
+
policy: string;
|
|
29
|
+
authority: string;
|
|
30
|
+
inputs: readonly (readonly [string, string])[];
|
|
31
|
+
environment: readonly (readonly [string, string])[];
|
|
32
|
+
argv: readonly string[];
|
|
33
|
+
cwd: string;
|
|
34
|
+
runtime: string;
|
|
35
|
+
timeoutMs: number;
|
|
36
|
+
responseBytes: number;
|
|
37
|
+
}
|
|
38
|
+
export interface DescendantInvocation {
|
|
39
|
+
cwd: string;
|
|
40
|
+
argv: readonly string[];
|
|
41
|
+
environment: readonly (readonly [string, string])[];
|
|
42
|
+
runtime: string;
|
|
43
|
+
timeoutMs: number;
|
|
44
|
+
responseBytes: number;
|
|
45
|
+
}
|
|
46
|
+
export interface GeneratedProducer {
|
|
47
|
+
readonly identity: string;
|
|
48
|
+
}
|
|
49
|
+
export interface DerivedExecutable {
|
|
50
|
+
kind: 'derived-executable/1';
|
|
51
|
+
parent: string;
|
|
52
|
+
producer: string;
|
|
53
|
+
identity: string;
|
|
54
|
+
digest: string;
|
|
55
|
+
mode: 'module' | 'commonjs';
|
|
56
|
+
interpretation?: string;
|
|
57
|
+
invocation: DescendantInvocation;
|
|
58
|
+
target: {
|
|
59
|
+
kind: 'file';
|
|
60
|
+
path: string;
|
|
61
|
+
} | {
|
|
62
|
+
kind: 'eval';
|
|
63
|
+
cwd: string;
|
|
64
|
+
argv: readonly string[];
|
|
65
|
+
text: string;
|
|
66
|
+
};
|
|
67
|
+
byteLength: number;
|
|
68
|
+
}
|
|
69
|
+
export interface DerivedInterpretation {
|
|
70
|
+
kind: 'derived-interpretation/1';
|
|
71
|
+
parent: string;
|
|
72
|
+
producer: string;
|
|
73
|
+
identity: string;
|
|
74
|
+
path: string;
|
|
75
|
+
digest: string;
|
|
76
|
+
interpretation: string;
|
|
77
|
+
invocation: DescendantInvocation;
|
|
78
|
+
byteLength: number;
|
|
79
|
+
}
|
|
80
|
+
export interface ProducerEmission {
|
|
81
|
+
kind: 'producer-emission/1';
|
|
82
|
+
parent: string;
|
|
83
|
+
producer: string;
|
|
84
|
+
identity: string;
|
|
85
|
+
path: string;
|
|
86
|
+
digest: string;
|
|
87
|
+
invocation: DescendantInvocation;
|
|
88
|
+
invocationIdentity: string;
|
|
89
|
+
byteLength: number;
|
|
90
|
+
}
|
|
91
|
+
export interface RootMember {
|
|
92
|
+
path: string;
|
|
93
|
+
digest: string;
|
|
94
|
+
}
|
|
95
|
+
export interface LiteralSnapshot {
|
|
96
|
+
budget: {
|
|
97
|
+
members: number;
|
|
98
|
+
bytes: number;
|
|
99
|
+
};
|
|
100
|
+
members: readonly RootMember[];
|
|
101
|
+
libraries: readonly {
|
|
102
|
+
name: string;
|
|
103
|
+
manifest: string;
|
|
104
|
+
}[];
|
|
105
|
+
builtins: readonly string[];
|
|
106
|
+
platform: string;
|
|
107
|
+
}
|
|
108
|
+
export interface ProducerCandidates {
|
|
109
|
+
readonly invocation: DescendantInvocation;
|
|
110
|
+
readonly outputs: readonly {
|
|
111
|
+
path: string;
|
|
112
|
+
digest?: string;
|
|
113
|
+
reason?: string;
|
|
114
|
+
}[];
|
|
115
|
+
}
|
|
116
|
+
/** Call inside the parent's real authority guard BEFORE emission/work. Signature
|
|
117
|
+
* and current eligibility are shared with the Root port constructor, unchanged. */
|
|
118
|
+
export declare function createGeneratedProducer(root: RootStanding, coordinate: string, parent: ParentExecution, implementationBytes: Uint8Array): GeneratedProducer;
|
|
119
|
+
export declare function validateDescendantInvocation(handle: GeneratedProducer, input: DescendantInvocation): DescendantInvocation;
|
|
120
|
+
export declare function producerTransport(handle: GeneratedProducer): Readonly<{
|
|
121
|
+
coordinate: string;
|
|
122
|
+
implementation: string;
|
|
123
|
+
identity: string;
|
|
124
|
+
parent: string;
|
|
125
|
+
timeoutMs: number;
|
|
126
|
+
responseBytes: number;
|
|
127
|
+
limits: {
|
|
128
|
+
members: number;
|
|
129
|
+
bytes: number;
|
|
130
|
+
work: number;
|
|
131
|
+
argvBytes: number;
|
|
132
|
+
argvItems: number;
|
|
133
|
+
};
|
|
134
|
+
interpretations: string[];
|
|
135
|
+
}>;
|
|
136
|
+
export declare function readBounded(path: string, limit: number): Uint8Array;
|
|
137
|
+
export declare function assertProducerInterpretation(handle: GeneratedProducer, name: string): void;
|
|
138
|
+
export declare function sealGeneratedFile(handle: GeneratedProducer, path: string, mode: 'module' | 'commonjs', invocation: DescendantInvocation, interpretation?: string): DerivedExecutable;
|
|
139
|
+
/** Preserve the four consumed native eval forms exactly; no rewriting/default
|
|
140
|
+
* flags/option blacklist. Prefix is the exact Root-selected inherited preload. */
|
|
141
|
+
export declare function sealNativeEval(handle: GeneratedProducer, actualArgv: readonly string[], rootPrefix: readonly string[], invocation: DescendantInvocation): DerivedExecutable;
|
|
142
|
+
/** Interpretive data is provenance, never an executable or an authority grant.
|
|
143
|
+
* The finite interpretation is Root-signed before bounded IO/JSON work. */
|
|
144
|
+
export declare function sealInterpretation(handle: GeneratedProducer, path: string, interpretation: string, invocation: DescendantInvocation): DerivedInterpretation;
|
|
145
|
+
/** Metadata from the fixed observer is only a candidate. No file is opened,
|
|
146
|
+
* no emission accepted, and no execution permission follows from registration. */
|
|
147
|
+
export declare function registerProducerCandidates(handle: GeneratedProducer, groups: readonly ProducerCandidates[]): void;
|
|
148
|
+
export declare function recordProducerEmission(handle: GeneratedProducer, path: string, invocation: DescendantInvocation): ProducerEmission;
|
|
149
|
+
/** All observed outputs must match before any provenance is accepted. One
|
|
150
|
+
* invocation context is committed once, with no code grant or quota widening. */
|
|
151
|
+
export declare function recordProducerEmissions(handle: GeneratedProducer, outputs: readonly {
|
|
152
|
+
path: string;
|
|
153
|
+
digest: string;
|
|
154
|
+
}[], invocation: DescendantInvocation): {
|
|
155
|
+
identity: string;
|
|
156
|
+
kind: string;
|
|
157
|
+
parent: string;
|
|
158
|
+
producer: string;
|
|
159
|
+
invocation: DescendantInvocation;
|
|
160
|
+
invocationIdentity: string;
|
|
161
|
+
outputs: {
|
|
162
|
+
path: string;
|
|
163
|
+
digest: string;
|
|
164
|
+
identity: string;
|
|
165
|
+
byteLength: number;
|
|
166
|
+
}[];
|
|
167
|
+
};
|
|
168
|
+
/** Verify the ACTUAL body/argv again before execution. A record's existence
|
|
169
|
+
* alone authorizes nothing and cannot be moved to another parent context. */
|
|
170
|
+
export declare function verifyDerived(handle: GeneratedProducer, record: DerivedExecutable, actual: {
|
|
171
|
+
path?: string;
|
|
172
|
+
argv?: readonly string[];
|
|
173
|
+
}): Uint8Array;
|
|
174
|
+
export declare function gateDerived(handle: GeneratedProducer, record: DerivedExecutable, actual: {
|
|
175
|
+
path?: string;
|
|
176
|
+
argv?: readonly string[];
|
|
177
|
+
}, snapshot: LiteralSnapshot, nativeImplementations?: ReadonlySet<string>): readonly {
|
|
178
|
+
target: string;
|
|
179
|
+
digest?: string;
|
|
180
|
+
platform?: string;
|
|
181
|
+
}[];
|
|
182
|
+
export declare function chargeProducerRequest(handle: GeneratedProducer): void;
|
|
183
|
+
export declare function producerUsage(handle: GeneratedProducer): Readonly<{
|
|
184
|
+
members: number;
|
|
185
|
+
bytes: number;
|
|
186
|
+
work: number;
|
|
187
|
+
retainedContents: number;
|
|
188
|
+
codeBytes: number;
|
|
189
|
+
limits: {
|
|
190
|
+
members: number;
|
|
191
|
+
bytes: number;
|
|
192
|
+
work: number;
|
|
193
|
+
argvBytes: number;
|
|
194
|
+
argvItems: number;
|
|
195
|
+
};
|
|
196
|
+
}>;
|
|
197
|
+
/** Shared host traversal. The reader callback supplies verification; traversal
|
|
198
|
+
* itself introduces no provenance or execution authority. */
|
|
199
|
+
export declare function traverseGeneratedClosure(input: {
|
|
200
|
+
source: Uint8Array;
|
|
201
|
+
path: string;
|
|
202
|
+
mode: 'module' | 'commonjs';
|
|
203
|
+
libs: ReadonlyMap<string, string>;
|
|
204
|
+
native: ReadonlySet<string>;
|
|
205
|
+
platform: string;
|
|
206
|
+
nativeImplementations: ReadonlySet<string>;
|
|
207
|
+
verify: (path: string) => {
|
|
208
|
+
real: string;
|
|
209
|
+
bytes: Uint8Array;
|
|
210
|
+
digest: string;
|
|
211
|
+
};
|
|
212
|
+
step: () => void;
|
|
213
|
+
}): readonly {
|
|
214
|
+
target: string;
|
|
215
|
+
digest?: string;
|
|
216
|
+
platform?: string;
|
|
217
|
+
}[];
|