@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.
@@ -0,0 +1,729 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const {
6
+ versions,
7
+ diagnosticCodes,
8
+ memoryAbi,
9
+ memoryAbiV2
10
+ } = require('./constants.js');
11
+ const { fail, wrap } = require('./errors.js');
12
+ const { normalizeGuestUnitPlan, optimizationArguments } = require('./contracts.js');
13
+ const { materializePackagePrebuilt } = require('./materialize.js');
14
+ const { inspectWasmFile, reportInspection, surfaceImport, surfaceExport } = require('./inspect.js');
15
+ const { runTool, binaryenIdentity } = require('./toolchain.js');
16
+ const { fileRecord, stableJson, writeAtomic } = require('./files.js');
17
+
18
+ function sortedSurface(values) {
19
+ return values.slice().sort((first, second) => (
20
+ compareText(String(first.module || ''), String(second.module || ''))
21
+ || compareText(first.name, second.name)
22
+ || compareText(first.kind, second.kind)
23
+ ));
24
+ }
25
+
26
+ function compareText(first, second) {
27
+ return first < second ? -1 : first > second ? 1 : 0;
28
+ }
29
+
30
+ function targetImport(entry) {
31
+ return Object.freeze({ module: entry.module, name: entry.name, kind: entry.kind });
32
+ }
33
+
34
+ function equal(value, expected) {
35
+ return JSON.stringify(value) === JSON.stringify(expected);
36
+ }
37
+
38
+ function segmentFacts(values) {
39
+ return values
40
+ .map((entry) => ({
41
+ memoryIndex: entry.memoryIndex,
42
+ offset: entry.offset,
43
+ endExclusive: entry.endExclusive,
44
+ bytes: entry.bytes,
45
+ sha256: entry.sha256
46
+ }))
47
+ .sort((first, second) => (
48
+ first.memoryIndex - second.memoryIndex
49
+ || first.offset - second.offset
50
+ || compareText(first.sha256, second.sha256)
51
+ ));
52
+ }
53
+
54
+ function rangeContains(range, segment) {
55
+ return segment.offset >= range.start && segment.endExclusive <= range.endExclusive;
56
+ }
57
+
58
+ function memoryContractForPlan(plan) {
59
+ return plan.version === versions.guestUnitPlanV2 ? memoryAbiV2 : memoryAbi;
60
+ }
61
+
62
+ function memoryContractForManifest(manifest) {
63
+ return manifest.version === versions.guestUnitV2 ? memoryAbiV2 : memoryAbi;
64
+ }
65
+
66
+ function pairwiseDisjoint(ranges) {
67
+ const sorted = ranges.slice().sort((left, right) => left.start - right.start);
68
+ return sorted.every((range, index) => (
69
+ range.start >= 0
70
+ && range.endExclusive > range.start
71
+ && (index === 0 || sorted[index - 1].endExclusive <= range.start)
72
+ ));
73
+ }
74
+
75
+ function memoryLayoutProof(plan, before, inspection, stage) {
76
+ const contract = memoryContractForPlan(plan);
77
+ const expectedSegments = segmentFacts([
78
+ ...before.owner.dataSegments,
79
+ ...before.primary.dataSegments,
80
+ ...before.guest.dataSegments
81
+ ]);
82
+ const observedSegments = segmentFacts(inspection.dataSegments);
83
+ const ranges = [
84
+ Object.freeze({
85
+ id: 'rust-stack',
86
+ ...contract.layout.rustStack,
87
+ disposition: plan.version === versions.guestUnitPlanV2
88
+ ? 'stack-and-transient-fixed-scratch'
89
+ : 'reserved-stack'
90
+ }),
91
+ Object.freeze({
92
+ id: 'rust-static-data',
93
+ ...contract.layout.rustStatic,
94
+ disposition: 'reserved-static-region'
95
+ }),
96
+ Object.freeze({
97
+ id: 'primary-static-data',
98
+ ...contract.layout.primaryStatic,
99
+ disposition: 'reserved-static-region'
100
+ })
101
+ ];
102
+ if (plan.version === versions.guestUnitPlanV2) {
103
+ ranges.push(Object.freeze({
104
+ id: 'private-invocation-frame-v2',
105
+ ...contract.layout.invocationFrame,
106
+ disposition: 'caller-owned-cleared-after-scalar-capture'
107
+ }));
108
+ } else {
109
+ ranges.push(Object.freeze({
110
+ id: 'borrowed-input',
111
+ ...contract.layout.borrowedInput,
112
+ disposition: 'synchronous-read-only'
113
+ }));
114
+ }
115
+ if (!pairwiseDisjoint(ranges)) {
116
+ fail(diagnosticCodes.finalAuditFailed, `${stage} memory reservations overlap.`);
117
+ }
118
+ for (const segment of observedSegments) {
119
+ if (
120
+ !rangeContains(contract.layout.rustStatic, segment)
121
+ && !rangeContains(contract.layout.primaryStatic, segment)
122
+ ) {
123
+ fail(diagnosticCodes.finalAuditFailed, `${stage} contains static data outside its reserved regions.`);
124
+ }
125
+ }
126
+ return Object.freeze({
127
+ stage,
128
+ memoryIdentity: contract.identity,
129
+ fixedMemory: Object.freeze({
130
+ pages: contract.minimumPages,
131
+ bytes: contract.bytes,
132
+ growable: false
133
+ }),
134
+ ranges: Object.freeze(ranges.map((range) => Object.freeze({
135
+ ...range,
136
+ bytes: range.endExclusive - range.start,
137
+ observedDataSegments: Object.freeze(observedSegments.filter((segment) => rangeContains(range, segment)))
138
+ }))),
139
+ fixedScratch: Object.freeze({
140
+ separateRange: false,
141
+ location: plan.version === versions.guestUnitPlanV2
142
+ ? 'rust-stack'
143
+ : 'none-observed'
144
+ }),
145
+ outputStatus: Object.freeze({
146
+ representation: 'scalar-i32-function-result',
147
+ linearMemoryRange: null
148
+ }),
149
+ expectedStaticSegments: Object.freeze(expectedSegments),
150
+ observedStaticSegments: Object.freeze(observedSegments),
151
+ staticSegmentsMatchValidatedInputs: equal(expectedSegments, observedSegments),
152
+ occupiedAndReservedRangesPairwiseDisjoint: true
153
+ });
154
+ }
155
+
156
+ function assertMemoryType(inspection, ownership, label, contract) {
157
+ const expectedType = [{
158
+ minimumPages: contract.minimumPages,
159
+ maximumPages: contract.maximumPages,
160
+ shared: false
161
+ }];
162
+ if (
163
+ inspection.memories !== 1
164
+ || inspection.importedMemories !== ownership.imported
165
+ || inspection.definedMemories !== ownership.defined
166
+ || !equal(inspection.memoryTypes, expectedType)
167
+ ) {
168
+ fail(diagnosticCodes.memoryMismatch, `${label} does not use the accepted fixed 32-page memory.`);
169
+ }
170
+ if (inspection.hasStart) fail(diagnosticCodes.startMismatch, `${label} has a forbidden start section.`);
171
+ if (inspection.features.length) fail(diagnosticCodes.featureMismatch, `${label} uses a feature outside the MVP baseline.`);
172
+ if (
173
+ inspection.instructions.memoryGrow
174
+ || inspection.instructions.memoryCopy
175
+ || inspection.instructions.memoryFill
176
+ || inspection.instructions.memoryInit
177
+ || inspection.instructions.callIndirect
178
+ ) {
179
+ fail(diagnosticCodes.memoryMismatch, `${label} violates the accepted fixed-memory instruction policy.`);
180
+ }
181
+ }
182
+
183
+ function assertPrimaryAndOwner(plan, manifest, primary, owner) {
184
+ const contract = memoryContractForManifest(manifest);
185
+ assertMemoryType(primary, { imported: 1, defined: 0 }, 'Primary module', contract);
186
+ assertMemoryType(owner, { imported: 0, defined: 1 }, 'Memory-owner module', contract);
187
+ if (owner.imports.length !== 0 || owner.exports.length !== 1) {
188
+ fail(diagnosticCodes.memoryMismatch, 'Memory-owner module must contain only one exported memory.');
189
+ }
190
+ const ownerExport = owner.exports[0];
191
+ if (ownerExport.name !== contract.importName || ownerExport.kind !== 'memory') {
192
+ fail(diagnosticCodes.memoryMismatch, 'Memory-owner export must be env.memory.');
193
+ }
194
+ if (
195
+ owner.dataSegments.length
196
+ || owner.mutableGlobals.length
197
+ || Object.values(owner.instructions).some((count) => count !== 0)
198
+ || owner.tables !== 0
199
+ ) {
200
+ fail(diagnosticCodes.memoryMismatch, 'Memory-owner module must be inert.');
201
+ }
202
+
203
+ const abiExports = manifest.exports.filter((entry) => entry.role === 'abi');
204
+ const guestImports = primary.imports.filter((entry) => entry.module === manifest.module);
205
+ const expectedGuestImports = abiExports.map((entry) => ({
206
+ module: manifest.module,
207
+ name: entry.name,
208
+ kind: entry.kind,
209
+ type: { parameters: entry.parameters, results: entry.results }
210
+ }));
211
+ if (!equal(guestImports.map(surfaceImport), expectedGuestImports)) {
212
+ fail(diagnosticCodes.abiMismatch, 'Primary guest imports do not exactly match the selected guest ABI.');
213
+ }
214
+ const memoryImports = primary.imports.filter((entry) => entry.kind === 'memory');
215
+ if (
216
+ memoryImports.length !== 1
217
+ || memoryImports[0].module !== plan.composition.memoryOwnerModule
218
+ || memoryImports[0].name !== contract.importName
219
+ ) {
220
+ fail(diagnosticCodes.memoryMismatch, 'Primary memory import does not bind to the accepted memory owner.');
221
+ }
222
+ const unresolved = primary.imports.filter((entry) => (
223
+ entry.module !== manifest.module
224
+ && !(entry.kind === 'memory' && entry.module === plan.composition.memoryOwnerModule && entry.name === contract.importName)
225
+ )).map(targetImport);
226
+ if (!equal(sortedSurface(unresolved), sortedSurface(plan.targetPolicy.allowedImports))) {
227
+ fail(diagnosticCodes.importMismatch, 'Primary host imports do not match the selected target policy.');
228
+ }
229
+ for (const segment of primary.dataSegments) {
230
+ if (!rangeContains(contract.layout.primaryStatic, segment)) {
231
+ fail(diagnosticCodes.memoryMismatch, 'Primary static data escapes its reserved region.');
232
+ }
233
+ }
234
+ }
235
+
236
+ function expectedFinalExports(primary, guest, owner) {
237
+ const values = [
238
+ ...owner.exports.map(surfaceExport),
239
+ ...primary.exports.map(surfaceExport),
240
+ ...guest.exports.map(surfaceExport)
241
+ ];
242
+ const names = values.map((entry) => entry.name);
243
+ if (new Set(names).size !== names.length) {
244
+ fail(diagnosticCodes.exportMismatch, 'Guest composition inputs contain conflicting export names.');
245
+ }
246
+ return sortedSurface(values);
247
+ }
248
+
249
+ function assertFinal(plan, before, final) {
250
+ const contract = memoryContractForPlan(plan);
251
+ const allowedImports = sortedSurface(plan.targetPolicy.allowedImports);
252
+ const actualImports = sortedSurface(final.imports.map(targetImport));
253
+ if (!equal(actualImports, allowedImports)) {
254
+ fail(diagnosticCodes.finalAuditFailed, 'Final Wasm imports do not match the target policy.', {
255
+ expected: allowedImports,
256
+ actual: actualImports
257
+ });
258
+ }
259
+ const expectedExports = expectedFinalExports(before.primary, before.guest, before.owner);
260
+ const actualExports = sortedSurface(final.exports.map(surfaceExport));
261
+ if (!equal(actualExports, expectedExports)) {
262
+ fail(diagnosticCodes.finalAuditFailed, 'Final Wasm exports do not exactly preserve the validated input surface.');
263
+ }
264
+ for (const required of plan.targetPolicy.requiredExports) {
265
+ if (!actualExports.some((entry) => entry.name === required.name && entry.kind === required.kind)) {
266
+ fail(diagnosticCodes.finalAuditFailed, `Final Wasm is missing target-required export ${required.name}.`);
267
+ }
268
+ }
269
+ assertMemoryType(final, { imported: 0, defined: 1 }, 'Final Wasm', contract);
270
+ const inputMutableGlobals = before.primary.mutableGlobals.length
271
+ + before.guest.mutableGlobals.length
272
+ + before.owner.mutableGlobals.length;
273
+ const inputTables = before.primary.tables + before.guest.tables + before.owner.tables;
274
+ const inputGlobalSets = before.primary.instructions.globalSet
275
+ + before.guest.instructions.globalSet
276
+ + before.owner.instructions.globalSet;
277
+ const inputStores = before.primary.instructions.store
278
+ + before.guest.instructions.store
279
+ + before.owner.instructions.store;
280
+ if (
281
+ final.mutableGlobals.length > inputMutableGlobals
282
+ || final.tables > inputTables
283
+ || final.instructions.globalSet > inputGlobalSets
284
+ || (
285
+ plan.version === versions.guestUnitPlanV2
286
+ && final.instructions.store > inputStores
287
+ )
288
+ ) {
289
+ fail(
290
+ diagnosticCodes.finalAuditFailed,
291
+ plan.version === versions.guestUnitPlanV2
292
+ ? 'Final Wasm introduces mutable state or writes absent from the validated inputs.'
293
+ : 'Final Wasm introduces mutable global or indirect state absent from the validated inputs.'
294
+ );
295
+ }
296
+ const expectedSegments = segmentFacts([
297
+ ...before.owner.dataSegments,
298
+ ...before.primary.dataSegments,
299
+ ...before.guest.dataSegments
300
+ ]);
301
+ const actualSegments = segmentFacts(final.dataSegments);
302
+ if (!equal(actualSegments, expectedSegments)) {
303
+ fail(diagnosticCodes.finalAuditFailed, 'Final Wasm did not preserve validated static data segments exactly.');
304
+ }
305
+ for (const segment of final.dataSegments) {
306
+ if (
307
+ !rangeContains(contract.layout.rustStatic, segment)
308
+ && !rangeContains(contract.layout.primaryStatic, segment)
309
+ ) {
310
+ fail(diagnosticCodes.finalAuditFailed, 'Final Wasm static data escapes accepted reserved regions.');
311
+ }
312
+ }
313
+ }
314
+
315
+ function redactionRecord() {
316
+ return Object.freeze({
317
+ environmentSecrets: 'omitted',
318
+ credentials: 'omitted',
319
+ privateMaterial: 'omitted',
320
+ rawBuffers: 'omitted',
321
+ diagnosticStrategy: 'key-classification-pattern-and-environment-value-redaction'
322
+ });
323
+ }
324
+
325
+ function evidenceClassification() {
326
+ return Object.freeze({
327
+ observedFacts: Object.freeze([
328
+ 'packaged-hashes-verified',
329
+ 'input-bytes-independently-inspected',
330
+ 'validated-inputs-composed',
331
+ 'post-link-bytes-optimized',
332
+ 'final-bytes-independently-audited'
333
+ ]),
334
+ toolDocumentedBehavior: Object.freeze([]),
335
+ inferences: Object.freeze([]),
336
+ unresolvedAssumptions: Object.freeze([])
337
+ });
338
+ }
339
+
340
+ function finalAudit(plan, before, afterLink, final, artifactRecord) {
341
+ const contract = memoryContractForPlan(plan);
342
+ const beforeOptimization = memoryLayoutProof(plan, before, afterLink, 'before-optimization');
343
+ const afterOptimization = memoryLayoutProof(plan, before, final, 'after-optimization');
344
+ if (
345
+ !beforeOptimization.staticSegmentsMatchValidatedInputs
346
+ || !afterOptimization.staticSegmentsMatchValidatedInputs
347
+ ) {
348
+ fail(diagnosticCodes.finalAuditFailed, 'Static layout changed across guest composition or optimization.');
349
+ }
350
+ const validatedInputWrites = Object.freeze({
351
+ globalSet: before.primary.instructions.globalSet
352
+ + before.guest.instructions.globalSet
353
+ + before.owner.instructions.globalSet,
354
+ store: before.primary.instructions.store
355
+ + before.guest.instructions.store
356
+ + before.owner.instructions.store
357
+ });
358
+ return Object.freeze({
359
+ version: plan.version === versions.guestUnitPlanV2
360
+ ? versions.finalWasmAuditV2
361
+ : versions.finalWasmAudit,
362
+ status: 'passed',
363
+ artifact: Object.freeze({
364
+ file: 'canonical-native.wasm',
365
+ bytes: artifactRecord.bytes,
366
+ sha256: artifactRecord.sha256,
367
+ valid: final.valid
368
+ }),
369
+ targetPolicy: Object.freeze({
370
+ descriptorIdentity: plan.targetPolicy.descriptorIdentity,
371
+ descriptorSha256: plan.targetPolicy.descriptorSha256
372
+ }),
373
+ structure: Object.freeze({
374
+ imports: Object.freeze(final.imports.map(surfaceImport)),
375
+ exports: Object.freeze(final.exports.map(surfaceExport)),
376
+ definedMemories: final.definedMemories,
377
+ importedMemories: final.importedMemories,
378
+ memoryTypes: final.memoryTypes,
379
+ hasStart: final.hasStart,
380
+ features: final.features,
381
+ dataSegments: Object.freeze(segmentFacts(final.dataSegments)),
382
+ mutableGlobals: final.mutableGlobals,
383
+ tables: final.tables
384
+ }),
385
+ instructions: Object.freeze({
386
+ memoryGrow: final.instructions.memoryGrow,
387
+ memoryCopy: final.instructions.memoryCopy,
388
+ memoryFill: final.instructions.memoryFill,
389
+ memoryInit: final.instructions.memoryInit,
390
+ callIndirect: final.instructions.callIndirect,
391
+ ...(plan.version === versions.guestUnitPlanV2 ? {
392
+ globalSet: final.instructions.globalSet,
393
+ store: final.instructions.store
394
+ } : {})
395
+ }),
396
+ ...(plan.version === versions.guestUnitPlanV2 ? {
397
+ mutableState: Object.freeze({
398
+ globals: final.mutableGlobals,
399
+ authorization: 'reviewed-es256-stack-pointer-and-stack-transient-writes',
400
+ validatedInputWrites,
401
+ finalWrites: Object.freeze({
402
+ globalSet: final.instructions.globalSet,
403
+ store: final.instructions.store
404
+ }),
405
+ noAdditionalWritesIntroduced: true
406
+ })
407
+ } : {}),
408
+ layout: Object.freeze({
409
+ memoryContract: contract.identity,
410
+ occupiedAndReservedRangesPairwiseDisjoint: true,
411
+ staticSegmentsPreserved: true,
412
+ ...(plan.version === versions.guestUnitPlanV2 ? {
413
+ beforeOptimization,
414
+ afterOptimization
415
+ } : {})
416
+ }),
417
+ checks: Object.freeze([
418
+ Object.freeze({ id: 'artifact-hash', status: 'passed' }),
419
+ Object.freeze({ id: 'target-policy', status: 'passed' }),
420
+ Object.freeze({ id: 'guest-contract', status: 'passed' }),
421
+ Object.freeze({ id: 'fixed-memory', status: 'passed' }),
422
+ Object.freeze({ id: 'static-layout', status: 'passed' }),
423
+ Object.freeze({ id: 'forbidden-instructions', status: 'passed' })
424
+ ]),
425
+ providerPackaging: Object.freeze({
426
+ authorized: true,
427
+ auditArtifactSha256MatchesInput: true
428
+ }),
429
+ evidenceClassification: evidenceClassification(),
430
+ redaction: redactionRecord()
431
+ });
432
+ }
433
+
434
+ function guestLinkReport(plan, materialized, before, afterLink, final, records, toolchain, posture) {
435
+ const linker = toolchain.tools.find((entry) => entry.name === 'wasm-merge');
436
+ const optimizer = toolchain.tools.find((entry) => entry.name === 'wasm-opt');
437
+ return Object.freeze({
438
+ version: plan.version === versions.guestUnitPlanV2
439
+ ? versions.guestLinkReportV2
440
+ : versions.guestLinkReport,
441
+ status: 'passed',
442
+ profile: plan.profile,
443
+ target: plan.target,
444
+ units: Object.freeze([
445
+ Object.freeze({
446
+ id: materialized.manifest.id,
447
+ module: materialized.manifest.module,
448
+ owner: materialized.manifest.owner,
449
+ packageVersion: materialized.manifest.packageVersion,
450
+ origin: materialized.manifest.origin,
451
+ abi: materialized.manifest.abi,
452
+ artifactSha256: materialized.artifactRecord.sha256,
453
+ sourceIncluded: materialized.sourceRecord.included,
454
+ sourceSha256: materialized.sourceRecord.treeSha256,
455
+ ...(materialized.manifest.version === versions.guestUnitV2 ? {
456
+ manifestVersion: materialized.manifest.version,
457
+ memoryIdentity: materialized.manifest.memory.identity,
458
+ frame: plan.unit.frame,
459
+ toolchain: plan.unit.toolchain
460
+ } : {}),
461
+ provenance: Object.freeze({
462
+ packageManifest: materialized.provenance.packageManifest,
463
+ lockfile: materialized.provenance.lockfile,
464
+ ...(materialized.manifest.version === versions.guestUnitV2
465
+ ? { buildScript: materialized.provenance.buildScript }
466
+ : {}),
467
+ source: materialized.provenance.source
468
+ })
469
+ })
470
+ ]),
471
+ inputs: Object.freeze({
472
+ primary: records.primary,
473
+ memoryOwner: records.owner,
474
+ guest: records.guest
475
+ }),
476
+ materialization: Object.freeze({
477
+ directory: materialized.relativeDirectory,
478
+ cache: 'content-addressed',
479
+ hashVerifiedBeforeUse: true
480
+ }),
481
+ inspection: Object.freeze({
482
+ before: Object.freeze({
483
+ primary: reportInspection(before.primary),
484
+ memoryOwner: reportInspection(before.owner),
485
+ guest: reportInspection(before.guest)
486
+ }),
487
+ composed: reportInspection(afterLink),
488
+ after: reportInspection(final)
489
+ }),
490
+ composition: Object.freeze({
491
+ kind: plan.composition.kind,
492
+ implementation: Object.freeze({
493
+ name: toolchain.name,
494
+ version: toolchain.version,
495
+ tool: linker.name,
496
+ sha256: linker.sha256
497
+ }),
498
+ deterministic: true,
499
+ outputSha256: records.composed.sha256
500
+ }),
501
+ optimization: Object.freeze({
502
+ postLink: true,
503
+ implementation: Object.freeze({
504
+ name: toolchain.name,
505
+ version: toolchain.version,
506
+ tool: optimizer.name,
507
+ sha256: optimizer.sha256
508
+ }),
509
+ posture,
510
+ deterministic: true,
511
+ inputSha256: records.composed.sha256,
512
+ outputSha256: records.final.sha256
513
+ }),
514
+ ...(plan.version === versions.guestUnitPlanV2 ? {
515
+ memoryLayout: Object.freeze({
516
+ beforeOptimization: memoryLayoutProof(plan, before, afterLink, 'before-optimization'),
517
+ afterOptimization: memoryLayoutProof(plan, before, final, 'after-optimization')
518
+ })
519
+ } : {}),
520
+ finalArtifact: Object.freeze({
521
+ file: 'canonical-native.wasm',
522
+ bytes: records.final.bytes,
523
+ sha256: records.final.sha256
524
+ }),
525
+ fallback: Object.freeze({ enabled: false, used: false }),
526
+ evidenceClassification: evidenceClassification(),
527
+ redaction: redactionRecord()
528
+ });
529
+ }
530
+
531
+ function ensureOutputDirectory(directory) {
532
+ try {
533
+ fs.mkdirSync(directory, { recursive: true });
534
+ return fs.realpathSync(directory);
535
+ } catch {
536
+ fail(diagnosticCodes.invalid, 'Guest-link output directory could not be created.');
537
+ }
538
+ }
539
+
540
+ function assertFinalArtifactIdentity(file, audit) {
541
+ if (
542
+ !audit
543
+ || (audit.version !== versions.finalWasmAudit && audit.version !== versions.finalWasmAuditV2)
544
+ || audit.status !== 'passed'
545
+ || !audit.artifact
546
+ ) {
547
+ fail(diagnosticCodes.finalAuditFailed, 'Final Wasm audit identity is missing or unsupported.');
548
+ }
549
+ let observed;
550
+ try {
551
+ observed = fileRecord(fs.realpathSync(file));
552
+ } catch (error) {
553
+ wrap(error, diagnosticCodes.finalAuditFailed, 'Final Wasm artifact could not be read for identity verification.');
554
+ }
555
+ if (
556
+ observed.bytes !== audit.artifact.bytes
557
+ || observed.sha256 !== audit.artifact.sha256
558
+ ) {
559
+ fail(diagnosticCodes.finalAuditFailed, 'Final Wasm bytes do not match their completed audit.', {
560
+ expected: {
561
+ bytes: audit.artifact.bytes,
562
+ sha256: audit.artifact.sha256
563
+ },
564
+ observed
565
+ });
566
+ }
567
+ return Object.freeze(observed);
568
+ }
569
+
570
+ function assertSelectedToolchain(plan, manifest, toolchain) {
571
+ if (plan.version !== versions.guestUnitPlanV2) return;
572
+ const optimizer = toolchain.tools.find((entry) => entry.name === 'wasm-opt');
573
+ if (
574
+ !optimizer
575
+ || toolchain.version !== manifest.toolchain.versions.binaryen
576
+ || toolchain.version !== plan.unit.toolchain.binaryenVersion
577
+ || optimizer.sha256 !== manifest.provenance.binaryenWasmOptSha256
578
+ || optimizer.sha256 !== plan.unit.toolchain.wasmOptSha256
579
+ ) {
580
+ fail(
581
+ diagnosticCodes.optimizationFailed,
582
+ 'Pinned Binaryen identity does not match the selected ES256 guest plan.'
583
+ );
584
+ }
585
+ }
586
+
587
+ function realizeGuestLinkPlan(options) {
588
+ const plan = normalizeGuestUnitPlan(options.plan);
589
+ const outputDirectory = ensureOutputDirectory(options.outputDirectory);
590
+ const workDirectory = ensureOutputDirectory(path.join(outputDirectory, '.guest-link-work'));
591
+ const posture = plan.version === versions.guestUnitPlanV2
592
+ ? plan.optimization.posture
593
+ : options.optimizationPosture || 'native-size';
594
+ if (
595
+ plan.version === versions.guestUnitPlanV2
596
+ && options.optimizationPosture !== undefined
597
+ && options.optimizationPosture !== posture
598
+ ) {
599
+ fail(
600
+ diagnosticCodes.optimizationFailed,
601
+ 'Guest-link optimization option does not match the exact v2 realization plan.'
602
+ );
603
+ }
604
+ const optimizationArgs = optimizationArguments(posture);
605
+ const toolchain = binaryenIdentity();
606
+
607
+ const materialized = materializePackagePrebuilt({
608
+ plan,
609
+ packageRoot: options.packageRoot,
610
+ manifestFile: options.manifestFile,
611
+ projectRoot: options.projectRoot,
612
+ synchronizedPackages: options.synchronizedPackages,
613
+ workDirectory
614
+ });
615
+ assertSelectedToolchain(plan, materialized.manifest, toolchain);
616
+
617
+ let primaryFile;
618
+ let ownerFile;
619
+ try {
620
+ primaryFile = fs.realpathSync(options.primaryFile);
621
+ ownerFile = fs.realpathSync(options.memoryOwnerFile);
622
+ if (!fs.statSync(primaryFile).isFile() || !fs.statSync(ownerFile).isFile()) throw new Error('not files');
623
+ } catch {
624
+ fail(diagnosticCodes.invalid, 'Guest-link primary and memory-owner inputs must be existing files.');
625
+ }
626
+ const before = Object.freeze({
627
+ primary: inspectWasmFile(primaryFile, { label: 'primary-input', workDirectory }),
628
+ owner: inspectWasmFile(ownerFile, { label: 'memory-owner-input', workDirectory }),
629
+ guest: materialized.inspection
630
+ });
631
+ assertPrimaryAndOwner(plan, materialized.manifest, before.primary, before.owner);
632
+
633
+ const composedFile = path.join(outputDirectory, 'guest-link-composed.wasm');
634
+ try {
635
+ runTool('wasm-merge', [
636
+ ownerFile,
637
+ plan.composition.memoryOwnerModule,
638
+ primaryFile,
639
+ plan.composition.primaryModule,
640
+ materialized.artifactFile,
641
+ materialized.manifest.module,
642
+ '--mvp-features',
643
+ '-o',
644
+ composedFile
645
+ ], {
646
+ cwd: workDirectory,
647
+ failureCode: diagnosticCodes.linkFailed,
648
+ failureMessage: `Guest unit ${materialized.manifest.id} could not be composed.`
649
+ });
650
+ } catch (error) {
651
+ wrap(error, diagnosticCodes.linkFailed, `Guest unit ${materialized.manifest.id} could not be composed.`);
652
+ }
653
+ const afterLink = inspectWasmFile(composedFile, {
654
+ label: 'composed-input',
655
+ workDirectory,
656
+ failureCode: diagnosticCodes.linkFailed
657
+ });
658
+ try {
659
+ assertFinal(plan, before, afterLink);
660
+ } catch (error) {
661
+ if (error && error.code === diagnosticCodes.finalAuditFailed) {
662
+ fail(diagnosticCodes.linkFailed, 'Composed Wasm violates the validated input contract.', { cause: error.message });
663
+ }
664
+ throw error;
665
+ }
666
+
667
+ const finalFile = path.join(outputDirectory, 'canonical-native.wasm');
668
+ runTool('wasm-opt', [
669
+ composedFile,
670
+ ...optimizationArgs,
671
+ '-o',
672
+ finalFile
673
+ ], {
674
+ cwd: workDirectory,
675
+ failureCode: diagnosticCodes.optimizationFailed,
676
+ failureMessage: 'Post-link whole-module optimization failed.'
677
+ });
678
+ const final = inspectWasmFile(finalFile, {
679
+ label: 'optimized-final',
680
+ workDirectory,
681
+ failureCode: diagnosticCodes.finalAuditFailed,
682
+ featureFailureCode: diagnosticCodes.finalAuditFailed
683
+ });
684
+ assertFinal(plan, before, final);
685
+ const finalWatFile = path.join(workDirectory, 'optimized-final.wat');
686
+
687
+ const records = Object.freeze({
688
+ primary: fileRecord(primaryFile),
689
+ owner: fileRecord(ownerFile),
690
+ guest: fileRecord(materialized.artifactFile),
691
+ composed: fileRecord(composedFile),
692
+ final: fileRecord(finalFile)
693
+ });
694
+ const audit = finalAudit(plan, before, afterLink, final, records.final);
695
+ const report = guestLinkReport(plan, materialized, before, afterLink, final, records, toolchain, posture);
696
+ const reportFile = path.join(outputDirectory, 'guest-link-report.json');
697
+ const auditFile = path.join(outputDirectory, 'final-wasm-audit.json');
698
+ writeAtomic(reportFile, stableJson(report));
699
+ writeAtomic(auditFile, stableJson(audit));
700
+
701
+ return Object.freeze({
702
+ plan,
703
+ materialization: Object.freeze({
704
+ directory: materialized.relativeDirectory,
705
+ reused: materialized.reused,
706
+ artifactFile: materialized.artifactFile,
707
+ manifestFile: materialized.manifestFile
708
+ }),
709
+ files: Object.freeze({
710
+ composed: composedFile,
711
+ final: finalFile,
712
+ finalWat: finalWatFile,
713
+ guestLinkReport: reportFile,
714
+ finalWasmAudit: auditFile
715
+ }),
716
+ report,
717
+ audit,
718
+ toolchain
719
+ });
720
+ }
721
+
722
+ module.exports = Object.freeze({
723
+ realizeGuestLinkPlan,
724
+ assertPrimaryAndOwner,
725
+ assertFinal,
726
+ assertFinalArtifactIdentity,
727
+ memoryLayoutProof,
728
+ segmentFacts
729
+ });