@pulse-compute/wasm-guest-link 0.0.0 → 1.0.0-beta.1
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 +93 -2
- package/package.json +29 -7
- package/src/constants.js +159 -0
- package/src/contracts.js +1143 -0
- package/src/errors.js +76 -0
- package/src/files.js +149 -0
- package/src/index.js +33 -0
- package/src/inspect.js +510 -0
- package/src/materialize.js +424 -0
- package/src/memory-owner.js +29 -0
- package/src/pipeline.js +729 -0
- package/src/stage.js +578 -0
- package/src/toolchain.js +125 -0
package/src/stage.js
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
|
|
8
|
+
function loadFinalWasmPolicy() {
|
|
9
|
+
try {
|
|
10
|
+
return require('@pulse-compute/wasm-contracts/provider/final-wasm-policy');
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) {
|
|
13
|
+
return require('../../contracts/src/provider/final-wasm-policy.js');
|
|
14
|
+
}
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function loadPackageContract() {
|
|
20
|
+
try {
|
|
21
|
+
return require('@pulse-compute/wasm-contracts/package/package-contract');
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) {
|
|
24
|
+
return require('../../contracts/src/package/package-contract.js');
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
defineFinalWasmPolicy,
|
|
32
|
+
projectFinalWasmPolicy
|
|
33
|
+
} = loadFinalWasmPolicy();
|
|
34
|
+
const {
|
|
35
|
+
GUEST_UNIT_CONTRIBUTION_FIELDS,
|
|
36
|
+
normalizeCanonicalGuestUnitContribution
|
|
37
|
+
} = loadPackageContract();
|
|
38
|
+
const guestLink = require('./index.js');
|
|
39
|
+
const { fail, wrap } = require('./errors.js');
|
|
40
|
+
|
|
41
|
+
const GUEST_LINK_STAGE_INVOCATION_VERSION = 'pulse.guest-link-stage-invocation.v1';
|
|
42
|
+
const GUEST_LINK_STAGE_RESULT_VERSION = 'pulse.guest-link-stage-result.v1';
|
|
43
|
+
const GUEST_LINK_STAGE_INVOCATION_FIELDS = Object.freeze([
|
|
44
|
+
'version',
|
|
45
|
+
'primaryWasm',
|
|
46
|
+
'guestUnits',
|
|
47
|
+
'projectRoot',
|
|
48
|
+
'profile',
|
|
49
|
+
'finalWasmPolicy',
|
|
50
|
+
'synchronizedPackages',
|
|
51
|
+
'optimizationPosture'
|
|
52
|
+
]);
|
|
53
|
+
const GUEST_LINK_STAGE_RESULT_FIELDS = Object.freeze([
|
|
54
|
+
'version',
|
|
55
|
+
'wasm',
|
|
56
|
+
'wat',
|
|
57
|
+
'guestUnits',
|
|
58
|
+
'plan',
|
|
59
|
+
'report',
|
|
60
|
+
'audit',
|
|
61
|
+
'materialization',
|
|
62
|
+
'finalArtifact',
|
|
63
|
+
'providerPackaging',
|
|
64
|
+
'fallback'
|
|
65
|
+
]);
|
|
66
|
+
const FINAL_WASM_POLICY_FIELDS = Object.freeze([
|
|
67
|
+
'version',
|
|
68
|
+
'descriptorOwner',
|
|
69
|
+
'toolchainVersion',
|
|
70
|
+
'descriptorIdentity',
|
|
71
|
+
'permittedImports',
|
|
72
|
+
'requiredExports',
|
|
73
|
+
'featureBaseline',
|
|
74
|
+
'allowedFeatures',
|
|
75
|
+
'memory',
|
|
76
|
+
'start',
|
|
77
|
+
'descriptorSha256'
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
function sha256(value) {
|
|
81
|
+
return crypto.createHash('sha256').update(value).digest('hex');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function stable(value) {
|
|
85
|
+
if (Array.isArray(value)) return value.map(stable);
|
|
86
|
+
if (!value || typeof value !== 'object') return value;
|
|
87
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function plainObject(value) {
|
|
91
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
92
|
+
const prototype = Object.getPrototypeOf(value);
|
|
93
|
+
return prototype === Object.prototype || prototype === null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function exactKeys(value, fields, label, code = guestLink.diagnosticCodes.invalid) {
|
|
97
|
+
if (!plainObject(value)) fail(code, `${label} must be an object.`);
|
|
98
|
+
const unknown = Object.keys(value).filter((key) => !fields.includes(key)).sort();
|
|
99
|
+
const missing = fields.filter((key) => !Object.hasOwn(value, key));
|
|
100
|
+
if (unknown.length > 0 || missing.length > 0) {
|
|
101
|
+
fail(code, `${label} does not match its exact versioned field set.`, { unknown, missing });
|
|
102
|
+
}
|
|
103
|
+
for (const key of Object.keys(value)) {
|
|
104
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
105
|
+
if (!descriptor || !descriptor.enumerable || !Object.hasOwn(descriptor, 'value')) {
|
|
106
|
+
fail(code, `${label}.${key} must be an enumerable data property.`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function string(value, label) {
|
|
112
|
+
if (typeof value !== 'string' || !value.trim() || value !== value.trim()) {
|
|
113
|
+
fail(guestLink.diagnosticCodes.invalid, `${label} must be a normalized non-empty string.`);
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function exactSemver(value, label) {
|
|
119
|
+
const normalized = string(value, label);
|
|
120
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(normalized)) {
|
|
121
|
+
fail(guestLink.diagnosticCodes.ownerMismatch, `${label} must be an exact semantic version.`);
|
|
122
|
+
}
|
|
123
|
+
return normalized;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeContribution(value) {
|
|
127
|
+
try {
|
|
128
|
+
return normalizeCanonicalGuestUnitContribution(value);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
wrap(error, guestLink.diagnosticCodes.invalid, 'Guest-link requires one exact canonical guest-unit contribution.');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function normalizeFinalWasmPolicy(value) {
|
|
135
|
+
exactKeys(value, FINAL_WASM_POLICY_FIELDS, 'Guest-link final-Wasm policy', 'PULSE_GUEST_TARGET_POLICY_REQUIRED');
|
|
136
|
+
let normalized;
|
|
137
|
+
try {
|
|
138
|
+
normalized = defineFinalWasmPolicy({
|
|
139
|
+
version: value.version,
|
|
140
|
+
descriptorOwner: value.descriptorOwner,
|
|
141
|
+
toolchainVersion: value.toolchainVersion,
|
|
142
|
+
descriptorIdentity: value.descriptorIdentity,
|
|
143
|
+
permittedImports: value.permittedImports,
|
|
144
|
+
requiredExports: value.requiredExports
|
|
145
|
+
});
|
|
146
|
+
} catch (error) {
|
|
147
|
+
wrap(error, 'PULSE_GUEST_TARGET_POLICY_REQUIRED', 'Guest-link requires a valid selected-target final-Wasm policy.');
|
|
148
|
+
}
|
|
149
|
+
if (JSON.stringify(stable(value)) !== JSON.stringify(stable(normalized))) {
|
|
150
|
+
fail('PULSE_GUEST_TARGET_POLICY_REQUIRED', 'Guest-link final-Wasm policy does not match its normalized descriptor identity.');
|
|
151
|
+
}
|
|
152
|
+
return normalized;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeSynchronizedPackages(values) {
|
|
156
|
+
if (!Array.isArray(values)) {
|
|
157
|
+
fail(guestLink.diagnosticCodes.ownerMismatch, 'Guest-link requires an exact synchronized package catalog.');
|
|
158
|
+
}
|
|
159
|
+
const names = new Set();
|
|
160
|
+
return Object.freeze(values.map((entry, index) => {
|
|
161
|
+
exactKeys(entry, ['name', 'version'], `Guest-link synchronizedPackages[${index}]`, guestLink.diagnosticCodes.ownerMismatch);
|
|
162
|
+
const name = string(entry.name, `Guest-link synchronizedPackages[${index}].name`);
|
|
163
|
+
if (names.has(name)) {
|
|
164
|
+
fail(guestLink.diagnosticCodes.ownerMismatch, `Guest-link synchronized package ${name} is duplicated.`);
|
|
165
|
+
}
|
|
166
|
+
names.add(name);
|
|
167
|
+
return Object.freeze({
|
|
168
|
+
name,
|
|
169
|
+
version: exactSemver(entry.version, `Guest-link synchronizedPackages[${index}].version`)
|
|
170
|
+
});
|
|
171
|
+
}).sort((left, right) => left.name.localeCompare(right.name)));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function normalizeStageSelection(value, index) {
|
|
175
|
+
exactKeys(value, ['contribution', 'packageRoot'], `Guest-link guestUnits[${index}]`);
|
|
176
|
+
return Object.freeze({
|
|
177
|
+
contribution: normalizeContribution(value.contribution),
|
|
178
|
+
packageRoot: string(value.packageRoot, `Guest-link guestUnits[${index}].packageRoot`)
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function normalizeGuestLinkStageInvocation(input) {
|
|
183
|
+
exactKeys(input, GUEST_LINK_STAGE_INVOCATION_FIELDS, 'Guest-link stage invocation');
|
|
184
|
+
if (input.version !== GUEST_LINK_STAGE_INVOCATION_VERSION) {
|
|
185
|
+
fail(
|
|
186
|
+
guestLink.diagnosticCodes.invalid,
|
|
187
|
+
`Guest-link stage invocation must use ${GUEST_LINK_STAGE_INVOCATION_VERSION}.`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
if (!Array.isArray(input.guestUnits)) {
|
|
191
|
+
fail(guestLink.diagnosticCodes.invalid, 'Guest-link stage invocation.guestUnits must be an array.');
|
|
192
|
+
}
|
|
193
|
+
if (input.guestUnits.length !== 1) {
|
|
194
|
+
fail(
|
|
195
|
+
'PULSE_GUEST_UNIT_CARDINALITY_UNSUPPORTED',
|
|
196
|
+
'The initial guest-unit composition contract accepts exactly one selected unit.',
|
|
197
|
+
{
|
|
198
|
+
selected: input.guestUnits.map((entry) => entry && entry.contribution && entry.contribution.id).filter(Boolean).sort(),
|
|
199
|
+
automaticFallback: false
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (!Buffer.isBuffer(input.primaryWasm)) {
|
|
204
|
+
fail(guestLink.diagnosticCodes.invalid, 'Guest-link stage invocation.primaryWasm must be exact primary Wasm bytes.');
|
|
205
|
+
}
|
|
206
|
+
const optimizationPosture = string(
|
|
207
|
+
input.optimizationPosture,
|
|
208
|
+
'Guest-link stage invocation.optimizationPosture'
|
|
209
|
+
);
|
|
210
|
+
if (!['native-default', 'native-size'].includes(optimizationPosture)) {
|
|
211
|
+
fail(
|
|
212
|
+
guestLink.diagnosticCodes.optimizationFailed,
|
|
213
|
+
`Unsupported guest-link optimization posture ${optimizationPosture}.`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return Object.freeze({
|
|
217
|
+
version: GUEST_LINK_STAGE_INVOCATION_VERSION,
|
|
218
|
+
primaryWasm: Buffer.from(input.primaryWasm),
|
|
219
|
+
guestUnits: Object.freeze(input.guestUnits.map(normalizeStageSelection)),
|
|
220
|
+
projectRoot: string(input.projectRoot, 'Guest-link stage invocation.projectRoot'),
|
|
221
|
+
profile: string(input.profile, 'Guest-link stage invocation.profile'),
|
|
222
|
+
finalWasmPolicy: normalizeFinalWasmPolicy(input.finalWasmPolicy),
|
|
223
|
+
synchronizedPackages: normalizeSynchronizedPackages(input.synchronizedPackages),
|
|
224
|
+
optimizationPosture
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function inside(root, candidate) {
|
|
229
|
+
const relative = path.relative(root, candidate);
|
|
230
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function readSelectedManifest(selection) {
|
|
234
|
+
const contribution = selection.contribution;
|
|
235
|
+
let packageRoot;
|
|
236
|
+
try {
|
|
237
|
+
packageRoot = fs.realpathSync(selection.packageRoot);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
wrap(error, guestLink.diagnosticCodes.invalid, 'Guest-unit package root is unavailable.');
|
|
240
|
+
}
|
|
241
|
+
const relativeManifest = contribution.manifest.replace(/^\.\//, '');
|
|
242
|
+
const manifestFile = path.resolve(packageRoot, relativeManifest);
|
|
243
|
+
if (!inside(packageRoot, manifestFile) || !fs.existsSync(manifestFile)) {
|
|
244
|
+
fail(
|
|
245
|
+
guestLink.diagnosticCodes.invalid,
|
|
246
|
+
`Guest-unit manifest ${contribution.manifest} is not contained by ${contribution.owner}.`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
const realManifest = fs.realpathSync(manifestFile);
|
|
250
|
+
if (!inside(packageRoot, realManifest) || !fs.statSync(realManifest).isFile()) {
|
|
251
|
+
fail(
|
|
252
|
+
guestLink.diagnosticCodes.invalid,
|
|
253
|
+
`Guest-unit manifest ${contribution.manifest} is not a contained regular file.`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const bytes = fs.readFileSync(realManifest);
|
|
257
|
+
let manifest;
|
|
258
|
+
try {
|
|
259
|
+
manifest = guestLink.normalizeGuestUnitManifest(JSON.parse(bytes.toString('utf8')));
|
|
260
|
+
} catch (error) {
|
|
261
|
+
wrap(error, guestLink.diagnosticCodes.invalid, `Guest-unit manifest ${contribution.manifest} is invalid.`);
|
|
262
|
+
}
|
|
263
|
+
if (
|
|
264
|
+
manifest.id !== contribution.id
|
|
265
|
+
|| manifest.owner !== contribution.owner
|
|
266
|
+
|| manifest.packageVersion !== contribution.packageVersion
|
|
267
|
+
|| manifest.origin !== contribution.origin
|
|
268
|
+
) {
|
|
269
|
+
fail(
|
|
270
|
+
guestLink.diagnosticCodes.ownerMismatch,
|
|
271
|
+
`Guest-unit contribution ${contribution.id} does not match its package-owned manifest.`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
return Object.freeze({
|
|
275
|
+
packageRoot,
|
|
276
|
+
manifestFile: relativeManifest,
|
|
277
|
+
manifest,
|
|
278
|
+
manifestSha256: sha256(bytes)
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function hostImports(primaryBytes, guestModule) {
|
|
283
|
+
let module;
|
|
284
|
+
try {
|
|
285
|
+
module = new WebAssembly.Module(primaryBytes);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
wrap(error, guestLink.diagnosticCodes.invalid, 'Guest-link primary input is not valid core Wasm.');
|
|
288
|
+
}
|
|
289
|
+
return WebAssembly.Module.imports(module)
|
|
290
|
+
.filter((entry) => entry.module !== guestModule)
|
|
291
|
+
.filter((entry) => !(entry.module === 'env' && entry.name === 'memory' && entry.kind === 'memory'))
|
|
292
|
+
.map((entry) => Object.freeze({ module: entry.module, name: entry.name, kind: entry.kind }));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function optimizationForPosture(posture) {
|
|
296
|
+
return posture === 'native-size'
|
|
297
|
+
? Object.freeze({ mode: 'experimental-native-size', posture: 'native-size' })
|
|
298
|
+
: Object.freeze({ mode: 'default', posture: 'native-default' });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function makeGuestUnitPlan(selection, resolved, facts, primaryBytes) {
|
|
302
|
+
let targetPolicy;
|
|
303
|
+
try {
|
|
304
|
+
targetPolicy = projectFinalWasmPolicy(
|
|
305
|
+
facts.finalWasmPolicy,
|
|
306
|
+
hostImports(primaryBytes, resolved.manifest.module)
|
|
307
|
+
);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
wrap(
|
|
310
|
+
error,
|
|
311
|
+
'PULSE_GUEST_TARGET_POLICY_REQUIRED',
|
|
312
|
+
'Guest-link primary imports exceed the selected target final-Wasm policy.'
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
const manifest = resolved.manifest;
|
|
316
|
+
const isV2 = manifest.version === guestLink.versions.guestUnitV2;
|
|
317
|
+
const optimization = optimizationForPosture(facts.optimizationPosture);
|
|
318
|
+
const input = {
|
|
319
|
+
version: isV2 ? guestLink.versions.guestUnitPlanV2 : guestLink.versions.guestUnitPlan,
|
|
320
|
+
profile: facts.profile,
|
|
321
|
+
target: targetPolicy.descriptorIdentity,
|
|
322
|
+
unit: isV2 ? {
|
|
323
|
+
manifestVersion: manifest.version,
|
|
324
|
+
id: manifest.id,
|
|
325
|
+
module: manifest.module,
|
|
326
|
+
owner: manifest.owner,
|
|
327
|
+
packageVersion: manifest.packageVersion,
|
|
328
|
+
abi: manifest.abi,
|
|
329
|
+
origin: manifest.origin,
|
|
330
|
+
manifestSha256: resolved.manifestSha256,
|
|
331
|
+
artifact: {
|
|
332
|
+
bytes: manifest.artifact.bytes,
|
|
333
|
+
sha256: manifest.artifact.sha256
|
|
334
|
+
},
|
|
335
|
+
source: {
|
|
336
|
+
included: true,
|
|
337
|
+
treeSha256: manifest.source.treeSha256
|
|
338
|
+
},
|
|
339
|
+
memoryIdentity: manifest.memory.identity,
|
|
340
|
+
frame: {
|
|
341
|
+
identity: guestLink.es256FrameV2.identity,
|
|
342
|
+
capacityBytes: guestLink.es256FrameV2.capacityBytes,
|
|
343
|
+
alignmentBytes: guestLink.es256FrameV2.alignmentBytes,
|
|
344
|
+
pointerWidthBits: guestLink.es256FrameV2.pointerWidthBits
|
|
345
|
+
},
|
|
346
|
+
toolchain: {
|
|
347
|
+
binaryenVersion: manifest.toolchain.versions.binaryen,
|
|
348
|
+
wasmOptSha256: manifest.provenance.binaryenWasmOptSha256
|
|
349
|
+
}
|
|
350
|
+
} : {
|
|
351
|
+
id: manifest.id,
|
|
352
|
+
module: manifest.module,
|
|
353
|
+
owner: manifest.owner,
|
|
354
|
+
packageVersion: manifest.packageVersion,
|
|
355
|
+
abi: manifest.abi,
|
|
356
|
+
origin: manifest.origin,
|
|
357
|
+
manifestSha256: resolved.manifestSha256,
|
|
358
|
+
artifactSha256: manifest.artifact.sha256,
|
|
359
|
+
source: {
|
|
360
|
+
included: Boolean(manifest.source),
|
|
361
|
+
treeSha256: manifest.source ? manifest.source.treeSha256 : null
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
targetPolicy,
|
|
365
|
+
materialization: {
|
|
366
|
+
workspace: '.pulse/guests',
|
|
367
|
+
directory: `.pulse/guests/${manifest.id}/${manifest.artifact.sha256}`,
|
|
368
|
+
artifact: 'unit.wasm',
|
|
369
|
+
manifest: 'unit.json',
|
|
370
|
+
contentAddress: 'artifact-sha256',
|
|
371
|
+
generated: true,
|
|
372
|
+
authoritative: false
|
|
373
|
+
},
|
|
374
|
+
trust: isV2 ? {
|
|
375
|
+
lowerer: 'trusted-first-party',
|
|
376
|
+
catalog: 'pulse.jwt-crypto-working-candidate.v1',
|
|
377
|
+
catalogStatus: 'unpublished-synchronized',
|
|
378
|
+
ownerMatchesContributionPackage: true,
|
|
379
|
+
installedVersionMatches: true,
|
|
380
|
+
status: 'passed'
|
|
381
|
+
} : {
|
|
382
|
+
lowerer: 'trusted-first-party',
|
|
383
|
+
releaseManifest: 'synchronized',
|
|
384
|
+
ownerMatchesContributionPackage: true,
|
|
385
|
+
installedVersionMatches: true,
|
|
386
|
+
status: 'passed'
|
|
387
|
+
},
|
|
388
|
+
composition: {
|
|
389
|
+
kind: 'core-wasm-static-link',
|
|
390
|
+
semanticContract: 'connect-validated-core-units-into-one-final-audited-native-artifact',
|
|
391
|
+
primaryModule: 'pulse_primary',
|
|
392
|
+
memoryOwnerModule: 'env'
|
|
393
|
+
},
|
|
394
|
+
fallback: { enabled: false, onFailure: 'stop-before-provider-packaging' }
|
|
395
|
+
};
|
|
396
|
+
if (isV2) {
|
|
397
|
+
input.optimization = {
|
|
398
|
+
mode: optimization.mode,
|
|
399
|
+
postLink: true,
|
|
400
|
+
posture: optimization.posture
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
return guestLink.normalizeGuestUnitPlan(input);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function normalizeGuestLinkStageResult(input) {
|
|
407
|
+
exactKeys(input, GUEST_LINK_STAGE_RESULT_FIELDS, 'Guest-link stage result');
|
|
408
|
+
if (input.version !== GUEST_LINK_STAGE_RESULT_VERSION) {
|
|
409
|
+
fail(
|
|
410
|
+
guestLink.diagnosticCodes.finalAuditFailed,
|
|
411
|
+
`Guest-link stage result must use ${GUEST_LINK_STAGE_RESULT_VERSION}.`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
if (!Buffer.isBuffer(input.wasm) || typeof input.wat !== 'string') {
|
|
415
|
+
fail(guestLink.diagnosticCodes.finalAuditFailed, 'Guest-link stage result must contain exact final Wasm and text bytes.');
|
|
416
|
+
}
|
|
417
|
+
if (!Array.isArray(input.guestUnits) || input.guestUnits.length !== 1) {
|
|
418
|
+
fail(guestLink.diagnosticCodes.finalAuditFailed, 'Guest-link stage result must identify exactly one normalized contribution.');
|
|
419
|
+
}
|
|
420
|
+
const guestUnits = Object.freeze(input.guestUnits.map(normalizeContribution));
|
|
421
|
+
const plan = guestLink.normalizeGuestUnitPlan(input.plan);
|
|
422
|
+
const expectedReportVersion = plan.version === guestLink.versions.guestUnitPlanV2
|
|
423
|
+
? guestLink.versions.guestLinkReportV2
|
|
424
|
+
: guestLink.versions.guestLinkReport;
|
|
425
|
+
const expectedAuditVersion = plan.version === guestLink.versions.guestUnitPlanV2
|
|
426
|
+
? guestLink.versions.finalWasmAuditV2
|
|
427
|
+
: guestLink.versions.finalWasmAudit;
|
|
428
|
+
if (!plainObject(input.report) || input.report.version !== expectedReportVersion || input.report.status !== 'passed') {
|
|
429
|
+
fail(guestLink.diagnosticCodes.finalAuditFailed, 'Guest-link stage result report identity is invalid.');
|
|
430
|
+
}
|
|
431
|
+
if (!plainObject(input.audit) || input.audit.version !== expectedAuditVersion || input.audit.status !== 'passed') {
|
|
432
|
+
fail(guestLink.diagnosticCodes.finalAuditFailed, 'Guest-link stage result audit identity is invalid.');
|
|
433
|
+
}
|
|
434
|
+
exactKeys(input.finalArtifact, ['bytes', 'sha256'], 'Guest-link stage result.finalArtifact', guestLink.diagnosticCodes.finalAuditFailed);
|
|
435
|
+
const observedArtifact = Object.freeze({ bytes: input.wasm.length, sha256: sha256(input.wasm) });
|
|
436
|
+
if (
|
|
437
|
+
input.finalArtifact.bytes !== observedArtifact.bytes
|
|
438
|
+
|| input.finalArtifact.sha256 !== observedArtifact.sha256
|
|
439
|
+
|| input.report.finalArtifact.bytes !== observedArtifact.bytes
|
|
440
|
+
|| input.report.finalArtifact.sha256 !== observedArtifact.sha256
|
|
441
|
+
|| input.audit.artifact.bytes !== observedArtifact.bytes
|
|
442
|
+
|| input.audit.artifact.sha256 !== observedArtifact.sha256
|
|
443
|
+
) {
|
|
444
|
+
fail(guestLink.diagnosticCodes.finalAuditFailed, 'Guest-link stage result bytes do not match their report and audit identity.');
|
|
445
|
+
}
|
|
446
|
+
exactKeys(input.materialization, ['directory', 'reused', 'contentAddressed'], 'Guest-link stage result.materialization', guestLink.diagnosticCodes.finalAuditFailed);
|
|
447
|
+
if (input.materialization.contentAddressed !== true || typeof input.materialization.reused !== 'boolean') {
|
|
448
|
+
fail(guestLink.diagnosticCodes.finalAuditFailed, 'Guest-link stage materialization disposition is invalid.');
|
|
449
|
+
}
|
|
450
|
+
exactKeys(input.providerPackaging, [
|
|
451
|
+
'authorized',
|
|
452
|
+
'input',
|
|
453
|
+
'artifactSha256',
|
|
454
|
+
'automaticFallback'
|
|
455
|
+
], 'Guest-link stage result.providerPackaging', guestLink.diagnosticCodes.finalAuditFailed);
|
|
456
|
+
if (
|
|
457
|
+
input.providerPackaging.authorized !== true
|
|
458
|
+
|| input.providerPackaging.input !== 'exact-audited-final-bytes'
|
|
459
|
+
|| input.providerPackaging.artifactSha256 !== observedArtifact.sha256
|
|
460
|
+
|| input.providerPackaging.automaticFallback !== false
|
|
461
|
+
|| input.audit.providerPackaging.authorized !== true
|
|
462
|
+
) {
|
|
463
|
+
fail(guestLink.diagnosticCodes.finalAuditFailed, 'Guest-link stage did not authorize only exact audited final bytes.');
|
|
464
|
+
}
|
|
465
|
+
exactKeys(input.fallback, [
|
|
466
|
+
'enabled',
|
|
467
|
+
'used',
|
|
468
|
+
'onFailure',
|
|
469
|
+
'alternateOutputAuthorized'
|
|
470
|
+
], 'Guest-link stage result.fallback', guestLink.diagnosticCodes.finalAuditFailed);
|
|
471
|
+
if (
|
|
472
|
+
input.fallback.enabled !== false
|
|
473
|
+
|| input.fallback.used !== false
|
|
474
|
+
|| input.fallback.onFailure !== 'stop-before-provider-packaging'
|
|
475
|
+
|| input.fallback.alternateOutputAuthorized !== false
|
|
476
|
+
|| input.report.fallback.enabled !== false
|
|
477
|
+
|| input.report.fallback.used !== false
|
|
478
|
+
) {
|
|
479
|
+
fail(guestLink.diagnosticCodes.finalAuditFailed, 'Guest-link stage fallback disposition is invalid.');
|
|
480
|
+
}
|
|
481
|
+
return Object.freeze({
|
|
482
|
+
version: GUEST_LINK_STAGE_RESULT_VERSION,
|
|
483
|
+
wasm: Buffer.from(input.wasm),
|
|
484
|
+
wat: input.wat,
|
|
485
|
+
guestUnits,
|
|
486
|
+
plan,
|
|
487
|
+
report: input.report,
|
|
488
|
+
audit: input.audit,
|
|
489
|
+
materialization: Object.freeze({
|
|
490
|
+
directory: input.materialization.directory,
|
|
491
|
+
reused: input.materialization.reused,
|
|
492
|
+
contentAddressed: true
|
|
493
|
+
}),
|
|
494
|
+
finalArtifact: observedArtifact,
|
|
495
|
+
providerPackaging: Object.freeze({
|
|
496
|
+
authorized: true,
|
|
497
|
+
input: 'exact-audited-final-bytes',
|
|
498
|
+
artifactSha256: observedArtifact.sha256,
|
|
499
|
+
automaticFallback: false
|
|
500
|
+
}),
|
|
501
|
+
fallback: Object.freeze({
|
|
502
|
+
enabled: false,
|
|
503
|
+
used: false,
|
|
504
|
+
onFailure: 'stop-before-provider-packaging',
|
|
505
|
+
alternateOutputAuthorized: false
|
|
506
|
+
})
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function realizeGuestLinkStage(input) {
|
|
511
|
+
const invocation = normalizeGuestLinkStageInvocation(input);
|
|
512
|
+
const selection = invocation.guestUnits[0];
|
|
513
|
+
const resolved = readSelectedManifest(selection);
|
|
514
|
+
const plan = makeGuestUnitPlan(selection, resolved, invocation, invocation.primaryWasm);
|
|
515
|
+
const stageDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'pulse-guest-link-stage-'));
|
|
516
|
+
try {
|
|
517
|
+
const primaryFile = path.join(stageDirectory, 'primary.wasm');
|
|
518
|
+
fs.writeFileSync(primaryFile, invocation.primaryWasm);
|
|
519
|
+
const owner = guestLink.writeMemoryOwnerModule(stageDirectory);
|
|
520
|
+
const linked = guestLink.realizeGuestLinkPlan({
|
|
521
|
+
plan,
|
|
522
|
+
packageRoot: resolved.packageRoot,
|
|
523
|
+
manifestFile: resolved.manifestFile,
|
|
524
|
+
projectRoot: invocation.projectRoot,
|
|
525
|
+
synchronizedPackages: invocation.synchronizedPackages,
|
|
526
|
+
primaryFile,
|
|
527
|
+
memoryOwnerFile: owner.file,
|
|
528
|
+
outputDirectory: path.join(stageDirectory, 'output'),
|
|
529
|
+
optimizationPosture: plan.version === guestLink.versions.guestUnitPlanV2
|
|
530
|
+
? plan.optimization.posture
|
|
531
|
+
: 'native-size'
|
|
532
|
+
});
|
|
533
|
+
const verifiedArtifact = guestLink.assertFinalArtifactIdentity(linked.files.final, linked.audit);
|
|
534
|
+
const finalBytes = fs.readFileSync(linked.files.final);
|
|
535
|
+
const finalWat = fs.readFileSync(linked.files.finalWat, 'utf8');
|
|
536
|
+
return normalizeGuestLinkStageResult({
|
|
537
|
+
version: GUEST_LINK_STAGE_RESULT_VERSION,
|
|
538
|
+
wasm: finalBytes,
|
|
539
|
+
wat: finalWat,
|
|
540
|
+
guestUnits: [selection.contribution],
|
|
541
|
+
plan,
|
|
542
|
+
report: linked.report,
|
|
543
|
+
audit: linked.audit,
|
|
544
|
+
materialization: {
|
|
545
|
+
directory: linked.materialization.directory,
|
|
546
|
+
reused: linked.materialization.reused,
|
|
547
|
+
contentAddressed: true
|
|
548
|
+
},
|
|
549
|
+
finalArtifact: verifiedArtifact,
|
|
550
|
+
providerPackaging: {
|
|
551
|
+
authorized: true,
|
|
552
|
+
input: 'exact-audited-final-bytes',
|
|
553
|
+
artifactSha256: verifiedArtifact.sha256,
|
|
554
|
+
automaticFallback: false
|
|
555
|
+
},
|
|
556
|
+
fallback: {
|
|
557
|
+
enabled: false,
|
|
558
|
+
used: false,
|
|
559
|
+
onFailure: 'stop-before-provider-packaging',
|
|
560
|
+
alternateOutputAuthorized: false
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
} finally {
|
|
564
|
+
fs.rmSync(stageDirectory, { recursive: true, force: true });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
module.exports = Object.freeze({
|
|
569
|
+
GUEST_LINK_STAGE_INVOCATION_VERSION,
|
|
570
|
+
GUEST_LINK_STAGE_RESULT_VERSION,
|
|
571
|
+
GUEST_LINK_STAGE_INVOCATION_FIELDS,
|
|
572
|
+
GUEST_LINK_STAGE_RESULT_FIELDS,
|
|
573
|
+
normalizeGuestLinkStageInvocation,
|
|
574
|
+
normalizeGuestLinkStageResult,
|
|
575
|
+
makeGuestUnitPlan,
|
|
576
|
+
realizeGuestLinkStage,
|
|
577
|
+
GUEST_UNIT_CONTRIBUTION_FIELDS
|
|
578
|
+
});
|
package/src/toolchain.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { spawnSync } = require('node:child_process');
|
|
6
|
+
const { binaryenVersion, diagnosticCodes } = require('./constants.js');
|
|
7
|
+
const { fail } = require('./errors.js');
|
|
8
|
+
const { sha256 } = require('./files.js');
|
|
9
|
+
|
|
10
|
+
const allowedTools = new Set(['wasm-as', 'wasm-dis', 'wasm-merge', 'wasm-opt']);
|
|
11
|
+
|
|
12
|
+
function binaryenPackage() {
|
|
13
|
+
let manifestFile;
|
|
14
|
+
let manifest;
|
|
15
|
+
try {
|
|
16
|
+
manifestFile = require.resolve('binaryen/package.json');
|
|
17
|
+
manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
18
|
+
} catch {
|
|
19
|
+
fail(diagnosticCodes.invalid, 'The direct pinned Binaryen dependency is not installed or has invalid metadata.');
|
|
20
|
+
}
|
|
21
|
+
if (manifest.name !== 'binaryen' || manifest.version !== binaryenVersion) {
|
|
22
|
+
fail(diagnosticCodes.invalid, `Guest linking requires direct Binaryen ${binaryenVersion}.`, {
|
|
23
|
+
observedName: manifest.name,
|
|
24
|
+
observedVersion: manifest.version
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return Object.freeze({
|
|
28
|
+
root: fs.realpathSync(path.dirname(manifestFile)),
|
|
29
|
+
manifestFile: fs.realpathSync(manifestFile),
|
|
30
|
+
version: manifest.version
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveTool(name) {
|
|
35
|
+
if (!allowedTools.has(name)) fail(diagnosticCodes.invalid, `Unknown Binaryen tool ${name}.`);
|
|
36
|
+
const packageInfo = binaryenPackage();
|
|
37
|
+
const candidate = path.join(packageInfo.root, 'bin', name);
|
|
38
|
+
if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) {
|
|
39
|
+
fail(diagnosticCodes.invalid, `Pinned Binaryen does not contain ${name}.`);
|
|
40
|
+
}
|
|
41
|
+
const real = fs.realpathSync(candidate);
|
|
42
|
+
if (!real.startsWith(`${packageInfo.root}${path.sep}`)) {
|
|
43
|
+
fail(diagnosticCodes.invalid, `Pinned Binaryen tool ${name} escapes its package root.`);
|
|
44
|
+
}
|
|
45
|
+
return Object.freeze({
|
|
46
|
+
name,
|
|
47
|
+
file: real,
|
|
48
|
+
version: packageInfo.version,
|
|
49
|
+
sha256: sha256(fs.readFileSync(real))
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function restrictedEnvironment() {
|
|
54
|
+
const result = {};
|
|
55
|
+
for (const name of ['PATH', 'TMPDIR', 'TMP', 'TEMP', 'SystemRoot', 'WINDIR']) {
|
|
56
|
+
if (process.env[name]) result[name] = process.env[name];
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function sanitizedOutput(value, replacements) {
|
|
62
|
+
let output = String(value || '');
|
|
63
|
+
for (const [from, to] of replacements) {
|
|
64
|
+
if (from) output = output.split(from).join(to);
|
|
65
|
+
}
|
|
66
|
+
return output.trim().slice(0, 4096);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function runTool(name, args, options = {}) {
|
|
70
|
+
const tool = resolveTool(name);
|
|
71
|
+
if (!Array.isArray(args) || args.some((entry) => typeof entry !== 'string')) {
|
|
72
|
+
fail(diagnosticCodes.invalid, `Arguments for ${name} must be a trusted normalized string array.`);
|
|
73
|
+
}
|
|
74
|
+
const cwd = options.cwd ? fs.realpathSync(options.cwd) : process.cwd();
|
|
75
|
+
const result = spawnSync(tool.file, args, {
|
|
76
|
+
cwd,
|
|
77
|
+
encoding: 'utf8',
|
|
78
|
+
env: restrictedEnvironment(),
|
|
79
|
+
timeout: options.timeoutMs || 30000,
|
|
80
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
81
|
+
shell: false
|
|
82
|
+
});
|
|
83
|
+
if (result.error || result.status !== 0) {
|
|
84
|
+
const replacements = [
|
|
85
|
+
[cwd, '<work>'],
|
|
86
|
+
[tool.file, `<binaryen>/${name}`],
|
|
87
|
+
[path.dirname(tool.file), '<binaryen>'],
|
|
88
|
+
...args
|
|
89
|
+
.filter((entry) => path.isAbsolute(entry))
|
|
90
|
+
.map((entry) => [entry, `<input>/${path.basename(entry)}`])
|
|
91
|
+
];
|
|
92
|
+
fail(options.failureCode || diagnosticCodes.invalid, options.failureMessage || `${name} failed.`, {
|
|
93
|
+
tool: name,
|
|
94
|
+
version: tool.version,
|
|
95
|
+
status: result.status,
|
|
96
|
+
stdout: sanitizedOutput(result.stdout, replacements),
|
|
97
|
+
stderr: sanitizedOutput(result.stderr, replacements),
|
|
98
|
+
cause: result.error && sanitizedOutput(result.error.message, replacements)
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return Object.freeze({
|
|
102
|
+
tool: Object.freeze({ name, version: tool.version, sha256: tool.sha256 }),
|
|
103
|
+
stdout: String(result.stdout || '').trim(),
|
|
104
|
+
stderr: String(result.stderr || '').trim()
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function binaryenIdentity() {
|
|
109
|
+
const packageInfo = binaryenPackage();
|
|
110
|
+
return Object.freeze({
|
|
111
|
+
name: 'binaryen',
|
|
112
|
+
version: packageInfo.version,
|
|
113
|
+
tools: Object.freeze(['wasm-as', 'wasm-dis', 'wasm-merge', 'wasm-opt'].map((name) => {
|
|
114
|
+
const tool = resolveTool(name);
|
|
115
|
+
return Object.freeze({ name, sha256: tool.sha256 });
|
|
116
|
+
}))
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = Object.freeze({
|
|
121
|
+
binaryenPackage,
|
|
122
|
+
resolveTool,
|
|
123
|
+
runTool,
|
|
124
|
+
binaryenIdentity
|
|
125
|
+
});
|