@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,424 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const {
6
+ normalizeGuestUnitManifest,
7
+ normalizeGuestUnitPlan,
8
+ assertPlanMatchesManifest
9
+ } = require('./contracts.js');
10
+ const {
11
+ versions,
12
+ diagnosticCodes,
13
+ memoryAbi,
14
+ memoryAbiV2
15
+ } = require('./constants.js');
16
+ const { fail, wrap } = require('./errors.js');
17
+ const {
18
+ fileRecord,
19
+ sourceTreeRecord,
20
+ resolveExistingContained,
21
+ ensureContainedDirectory,
22
+ inside
23
+ } = require('./files.js');
24
+ const { inspectWasmFile, surfaceImport, surfaceExport } = require('./inspect.js');
25
+
26
+ function compare(value, expected, code, message, details) {
27
+ if (Buffer.isBuffer(value) && Buffer.isBuffer(expected)) {
28
+ if (!value.equals(expected)) fail(code, message, details);
29
+ return;
30
+ }
31
+ if (JSON.stringify(value) !== JSON.stringify(expected)) fail(code, message, details);
32
+ }
33
+
34
+ function normalizedManifestExports(manifest) {
35
+ return manifest.exports.map((entry) => Object.freeze({
36
+ name: entry.name,
37
+ kind: entry.kind,
38
+ parameters: entry.parameters,
39
+ results: entry.results
40
+ }));
41
+ }
42
+
43
+ function memoryContract(manifest) {
44
+ return manifest.memory.identity === versions.memoryAbiV2 ? memoryAbiV2 : memoryAbi;
45
+ }
46
+
47
+ function planArtifactSha256(plan) {
48
+ return plan.version === versions.guestUnitPlanV2
49
+ ? plan.unit.artifact.sha256
50
+ : plan.unit.artifactSha256;
51
+ }
52
+
53
+ function resolveManifestContained(packageRoot, manifestSourceFile, relative, field, expectedKind = 'file') {
54
+ if (typeof relative !== 'string' || !relative || relative.includes('\\') || path.isAbsolute(relative)) {
55
+ fail(diagnosticCodes.invalid, `${field} must be a normalized manifest-relative path.`);
56
+ }
57
+ const candidate = path.resolve(path.dirname(manifestSourceFile), relative);
58
+ if (!inside(packageRoot, candidate) || !fs.existsSync(candidate)) {
59
+ fail(diagnosticCodes.invalid, `${field} does not resolve to an existing contained ${expectedKind}.`);
60
+ }
61
+ const real = fs.realpathSync(candidate);
62
+ if (!inside(packageRoot, real)) fail(diagnosticCodes.invalid, `${field} escapes its package root.`);
63
+ const stat = fs.statSync(real);
64
+ if ((expectedKind === 'file' && !stat.isFile()) || (expectedKind === 'directory' && !stat.isDirectory())) {
65
+ fail(diagnosticCodes.invalid, `${field} must resolve to a ${expectedKind}.`);
66
+ }
67
+ return real;
68
+ }
69
+
70
+ function resolveManifestAsset(packageRoot, manifestSourceFile, manifest, relative, field, expectedKind = 'file') {
71
+ if (manifest.version === versions.guestUnitV2) {
72
+ return resolveManifestContained(packageRoot, manifestSourceFile, relative, field, expectedKind);
73
+ }
74
+ return resolveExistingContained(packageRoot, relative, field, expectedKind);
75
+ }
76
+
77
+ function assertGuestBinary(manifest, inspection) {
78
+ const contract = memoryContract(manifest);
79
+ compare(
80
+ inspection.imports.map(surfaceImport),
81
+ manifest.imports,
82
+ diagnosticCodes.importMismatch,
83
+ `Guest unit ${manifest.id} binary imports do not match its manifest.`
84
+ );
85
+ compare(
86
+ inspection.exports.map(surfaceExport),
87
+ normalizedManifestExports(manifest),
88
+ diagnosticCodes.exportMismatch,
89
+ `Guest unit ${manifest.id} binary exports do not match its manifest.`
90
+ );
91
+ if (
92
+ inspection.memories !== 1
93
+ || inspection.importedMemories !== 1
94
+ || inspection.definedMemories !== 0
95
+ || JSON.stringify(inspection.memoryTypes) !== JSON.stringify([{
96
+ minimumPages: contract.minimumPages,
97
+ maximumPages: contract.maximumPages,
98
+ shared: false
99
+ }])
100
+ ) {
101
+ fail(diagnosticCodes.memoryMismatch, `Guest unit ${manifest.id} binary violates its memory contract.`);
102
+ }
103
+ if (inspection.hasStart) fail(diagnosticCodes.startMismatch, `Guest unit ${manifest.id} binary has a forbidden start section.`);
104
+ if (inspection.features.length !== 0) fail(diagnosticCodes.featureMismatch, `Guest unit ${manifest.id} binary uses unexpected features.`);
105
+ if (
106
+ inspection.tables !== 0
107
+ || inspection.instructions.memoryGrow !== 0
108
+ || inspection.instructions.memoryCopy !== 0
109
+ || inspection.instructions.memoryFill !== 0
110
+ || inspection.instructions.memoryInit !== 0
111
+ || inspection.instructions.callIndirect !== 0
112
+ ) {
113
+ fail(diagnosticCodes.memoryMismatch, `Guest unit ${manifest.id} binary can mutate, grow, retain, or indirectly dispatch borrowed memory.`);
114
+ }
115
+ if (
116
+ manifest.version === versions.guestUnit
117
+ && (inspection.instructions.globalSet !== 0 || inspection.instructions.store !== 0)
118
+ ) {
119
+ fail(diagnosticCodes.memoryMismatch, `Guest unit ${manifest.id} binary can mutate borrowed-span memory.`);
120
+ }
121
+ if (manifest.version === versions.guestUnitV2) {
122
+ if (
123
+ inspection.globals !== 1
124
+ || inspection.mutableGlobals.length !== 1
125
+ || JSON.stringify(inspection.mutableGlobals[0]) !== JSON.stringify({
126
+ name: 'global$0',
127
+ type: 'i32',
128
+ initializer: { instruction: 'i32.const', value: contract.layout.rustStack.endExclusive }
129
+ })
130
+ || inspection.instructions.globalSet !== 64
131
+ || inspection.instructions.store !== 500
132
+ ) {
133
+ fail(
134
+ diagnosticCodes.memoryMismatch,
135
+ `Guest unit ${manifest.id} binary does not match the reviewed stack-only mutable-state posture.`
136
+ );
137
+ }
138
+ }
139
+ for (const segment of inspection.dataSegments) {
140
+ if (
141
+ segment.memoryIndex !== 0
142
+ || segment.offset < contract.layout.rustStatic.start
143
+ || segment.endExclusive > contract.layout.rustStatic.endExclusive
144
+ ) {
145
+ fail(diagnosticCodes.memoryMismatch, `Guest unit ${manifest.id} static data escapes its reserved region.`);
146
+ }
147
+ }
148
+ if (
149
+ manifest.version === versions.guestUnitV2
150
+ && JSON.stringify(inspection.dataSegments.map((entry) => ({
151
+ memoryIndex: entry.memoryIndex,
152
+ offset: entry.offset,
153
+ endExclusive: entry.endExclusive,
154
+ bytes: entry.bytes,
155
+ sha256: entry.sha256
156
+ }))) !== JSON.stringify([{
157
+ memoryIndex: 0,
158
+ offset: 131072,
159
+ endExclusive: 131656,
160
+ bytes: 584,
161
+ sha256: '231fcd9a2fc4e8190c8b96c9f89708b8039ea1b801cc564e32fbc14ff44e53ee'
162
+ }])
163
+ ) {
164
+ fail(diagnosticCodes.memoryMismatch, `Guest unit ${manifest.id} static data does not match the reviewed layout.`);
165
+ }
166
+ }
167
+
168
+ function synchronizedVersion(packages, name) {
169
+ if (!Array.isArray(packages)) fail(diagnosticCodes.ownerMismatch, 'Guest trust requires the synchronized package catalog.');
170
+ const matches = packages.filter((entry) => entry && entry.name === name);
171
+ if (matches.length !== 1) fail(diagnosticCodes.ownerMismatch, `Guest owner ${name} is not uniquely synchronized.`);
172
+ return matches[0].version;
173
+ }
174
+
175
+ function verifyPackageOwner(packageRoot, manifestSourceFile, manifest, synchronizedPackages) {
176
+ const packageManifestFile = resolveManifestAsset(
177
+ packageRoot,
178
+ manifestSourceFile,
179
+ manifest,
180
+ manifest.provenance.packageManifest,
181
+ 'guest provenance package manifest'
182
+ );
183
+ const lockfileFile = resolveManifestAsset(
184
+ packageRoot,
185
+ manifestSourceFile,
186
+ manifest,
187
+ manifest.provenance.lockfile,
188
+ 'guest provenance lockfile'
189
+ );
190
+ let packageManifest;
191
+ try {
192
+ packageManifest = JSON.parse(fs.readFileSync(packageManifestFile, 'utf8'));
193
+ } catch (error) {
194
+ wrap(error, diagnosticCodes.ownerMismatch, 'Guest owner package manifest is invalid.');
195
+ }
196
+ const synchronized = synchronizedVersion(synchronizedPackages, manifest.owner);
197
+ if (
198
+ packageManifest.name !== manifest.owner
199
+ || packageManifest.version !== manifest.packageVersion
200
+ || synchronized !== manifest.packageVersion
201
+ ) {
202
+ fail(diagnosticCodes.ownerMismatch, `Guest unit ${manifest.id} owner or version does not match synchronized package identity.`, {
203
+ owner: manifest.owner,
204
+ packageVersion: manifest.packageVersion,
205
+ observedOwner: packageManifest.name,
206
+ observedVersion: packageManifest.version,
207
+ synchronizedVersion: synchronized
208
+ });
209
+ }
210
+ const lockfileRecord = fileRecord(lockfileFile);
211
+ if (
212
+ manifest.version === versions.guestUnitV2
213
+ && lockfileRecord.sha256 !== manifest.provenance.cargoLockSha256
214
+ ) {
215
+ fail(diagnosticCodes.hashMismatch, `Guest unit ${manifest.id} lockfile does not match its declared identity.`);
216
+ }
217
+ let buildScriptFile;
218
+ let buildScriptRecord;
219
+ if (manifest.version === versions.guestUnitV2) {
220
+ buildScriptFile = resolveManifestAsset(
221
+ packageRoot,
222
+ manifestSourceFile,
223
+ manifest,
224
+ manifest.provenance.buildScript,
225
+ 'guest provenance maintainer build script'
226
+ );
227
+ buildScriptRecord = fileRecord(buildScriptFile);
228
+ if (buildScriptRecord.sha256 !== manifest.provenance.buildScriptSha256) {
229
+ fail(
230
+ diagnosticCodes.hashMismatch,
231
+ `Guest unit ${manifest.id} maintainer build script does not match its declared identity.`
232
+ );
233
+ }
234
+ }
235
+ return Object.freeze({
236
+ packageManifestFile,
237
+ lockfileFile,
238
+ buildScriptFile,
239
+ packageManifestRecord: fileRecord(packageManifestFile),
240
+ lockfileRecord,
241
+ buildScriptRecord
242
+ });
243
+ }
244
+
245
+ function verifyExistingMaterialization(directory, artifactBytes, manifestBytes, artifactName, manifestName) {
246
+ const entries = fs.readdirSync(directory).sort();
247
+ compare(entries, [manifestName, artifactName].sort(), diagnosticCodes.materializationFailed, 'Existing guest materialization contains unexpected files.');
248
+ const artifactFile = path.join(directory, artifactName);
249
+ const manifestFile = path.join(directory, manifestName);
250
+ for (const file of [artifactFile, manifestFile]) {
251
+ const stat = fs.lstatSync(file);
252
+ if (stat.isSymbolicLink() || !stat.isFile()) {
253
+ fail(diagnosticCodes.materializationFailed, 'Existing guest materialization must contain regular files.');
254
+ }
255
+ }
256
+ compare(fs.readFileSync(artifactFile), artifactBytes, diagnosticCodes.materializationFailed, 'Existing guest artifact bytes differ from the content address.');
257
+ compare(fs.readFileSync(manifestFile), manifestBytes, diagnosticCodes.materializationFailed, 'Existing guest manifest bytes differ from the content address.');
258
+ }
259
+
260
+ function materializePackagePrebuilt(options) {
261
+ const plan = normalizeGuestUnitPlan(options.plan);
262
+ let packageRoot;
263
+ try {
264
+ packageRoot = fs.realpathSync(options.packageRoot);
265
+ if (!fs.statSync(packageRoot).isDirectory()) throw new Error('not a directory');
266
+ } catch {
267
+ fail(diagnosticCodes.invalid, 'Guest package root must be an existing directory.');
268
+ }
269
+ const manifestSourceFile = resolveExistingContained(packageRoot, options.manifestFile, 'guest unit manifest');
270
+ const manifestBytes = fs.readFileSync(manifestSourceFile);
271
+ const manifestRecord = fileRecord(manifestSourceFile);
272
+ if (manifestRecord.sha256 !== plan.unit.manifestSha256) {
273
+ fail(diagnosticCodes.hashMismatch, `Guest unit ${plan.unit.id} manifest hash does not match its plan.`);
274
+ }
275
+ let parsed;
276
+ try {
277
+ parsed = JSON.parse(manifestBytes.toString('utf8'));
278
+ } catch (error) {
279
+ wrap(error, diagnosticCodes.invalid, `Guest unit ${plan.unit.id} manifest is not valid JSON.`);
280
+ }
281
+ const manifest = normalizeGuestUnitManifest(parsed);
282
+ assertPlanMatchesManifest(plan, manifest);
283
+ const owner = verifyPackageOwner(
284
+ packageRoot,
285
+ manifestSourceFile,
286
+ manifest,
287
+ options.synchronizedPackages
288
+ );
289
+
290
+ const artifactSourceFile = resolveManifestAsset(
291
+ packageRoot,
292
+ manifestSourceFile,
293
+ manifest,
294
+ manifest.artifact.file,
295
+ 'guest unit artifact'
296
+ );
297
+ const artifactRecord = fileRecord(artifactSourceFile);
298
+ if (
299
+ artifactRecord.bytes !== manifest.artifact.bytes
300
+ || artifactRecord.sha256 !== manifest.artifact.sha256
301
+ || artifactRecord.sha256 !== planArtifactSha256(plan)
302
+ ) {
303
+ fail(diagnosticCodes.hashMismatch, `Guest unit ${manifest.id} artifact bytes do not match their declared identity.`, {
304
+ expectedBytes: manifest.artifact.bytes,
305
+ expectedSha256: manifest.artifact.sha256,
306
+ observedBytes: artifactRecord.bytes,
307
+ observedSha256: artifactRecord.sha256
308
+ });
309
+ }
310
+
311
+ let sourceRecord = Object.freeze({ included: false, treeSha256: null, files: 0, bytes: 0 });
312
+ if (manifest.source) {
313
+ const sourceDirectory = resolveManifestAsset(
314
+ packageRoot,
315
+ manifestSourceFile,
316
+ manifest,
317
+ manifest.source.directory,
318
+ 'guest unit source',
319
+ 'directory'
320
+ );
321
+ if (!inside(sourceDirectory, owner.lockfileFile)) {
322
+ fail(diagnosticCodes.invalid, `Guest unit ${manifest.id} lockfile must be included in its source provenance directory.`);
323
+ }
324
+ const tree = sourceTreeRecord(sourceDirectory);
325
+ if (
326
+ tree.sha256 !== manifest.source.treeSha256
327
+ || tree.sha256 !== plan.unit.source.treeSha256
328
+ || (
329
+ manifest.version === versions.guestUnitV2
330
+ && tree.sha256 !== manifest.provenance.sourceTreeSha256
331
+ )
332
+ ) {
333
+ fail(diagnosticCodes.hashMismatch, `Guest unit ${manifest.id} source tree does not match its declared identity.`);
334
+ }
335
+ sourceRecord = Object.freeze({
336
+ included: true,
337
+ treeSha256: tree.sha256,
338
+ algorithm: tree.algorithm,
339
+ files: tree.files.length,
340
+ bytes: tree.bytes
341
+ });
342
+ }
343
+
344
+ const inspection = inspectWasmFile(artifactSourceFile, {
345
+ label: `${manifest.id}-packaged`,
346
+ workDirectory: options.workDirectory
347
+ });
348
+ assertGuestBinary(manifest, inspection);
349
+
350
+ let projectRoot;
351
+ try {
352
+ projectRoot = fs.realpathSync(options.projectRoot);
353
+ if (!fs.statSync(projectRoot).isDirectory()) throw new Error('not a directory');
354
+ } catch {
355
+ fail(diagnosticCodes.materializationFailed, 'Guest project root must be an existing directory.');
356
+ }
357
+ const parentRelative = path.posix.dirname(plan.materialization.directory);
358
+ const parent = ensureContainedDirectory(projectRoot, parentRelative);
359
+ const destination = path.join(parent, path.posix.basename(plan.materialization.directory));
360
+ if (!inside(projectRoot, destination)) fail(diagnosticCodes.materializationFailed, 'Guest materialization destination escapes the project root.');
361
+ const artifactBytes = fs.readFileSync(artifactSourceFile);
362
+ let reused = false;
363
+ if (fs.existsSync(destination)) {
364
+ const realDestination = fs.realpathSync(destination);
365
+ if (!inside(projectRoot, realDestination) || !fs.statSync(realDestination).isDirectory()) {
366
+ fail(diagnosticCodes.materializationFailed, 'Existing guest materialization is not a contained directory.');
367
+ }
368
+ verifyExistingMaterialization(
369
+ realDestination,
370
+ artifactBytes,
371
+ manifestBytes,
372
+ plan.materialization.artifact,
373
+ plan.materialization.manifest
374
+ );
375
+ reused = true;
376
+ } else {
377
+ const staging = fs.mkdtempSync(path.join(parent, `.${path.basename(destination)}.staging-`));
378
+ try {
379
+ fs.writeFileSync(path.join(staging, plan.materialization.artifact), artifactBytes);
380
+ fs.writeFileSync(path.join(staging, plan.materialization.manifest), manifestBytes);
381
+ fs.renameSync(staging, destination);
382
+ } catch (error) {
383
+ if (fs.existsSync(staging)) fs.rmSync(staging, { recursive: true, force: true });
384
+ if (error && error.code === 'EEXIST' && fs.existsSync(destination)) {
385
+ verifyExistingMaterialization(
386
+ destination,
387
+ artifactBytes,
388
+ manifestBytes,
389
+ plan.materialization.artifact,
390
+ plan.materialization.manifest
391
+ );
392
+ reused = true;
393
+ } else {
394
+ wrap(error, diagnosticCodes.materializationFailed, `Guest unit ${manifest.id} could not be materialized.`);
395
+ }
396
+ }
397
+ }
398
+
399
+ return Object.freeze({
400
+ plan,
401
+ manifest,
402
+ manifestRecord,
403
+ artifactRecord,
404
+ sourceRecord,
405
+ provenance: Object.freeze({
406
+ packageManifest: owner.packageManifestRecord,
407
+ lockfile: owner.lockfileRecord,
408
+ ...(manifest.version === versions.guestUnitV2
409
+ ? { buildScript: owner.buildScriptRecord }
410
+ : {}),
411
+ source: sourceRecord
412
+ }),
413
+ inspection,
414
+ artifactFile: path.join(destination, plan.materialization.artifact),
415
+ manifestFile: path.join(destination, plan.materialization.manifest),
416
+ relativeDirectory: plan.materialization.directory,
417
+ reused
418
+ });
419
+ }
420
+
421
+ module.exports = Object.freeze({
422
+ assertGuestBinary,
423
+ materializePackagePrebuilt
424
+ });
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { fileRecord, writeAtomic } = require('./files.js');
6
+
7
+ // MVP core Wasm containing exactly `(memory 32 32)` exported as `memory`.
8
+ // Keeping these audited bytes in the link package avoids a shell/tool hook in
9
+ // the compiler and makes the memory owner independent of the contributing package.
10
+ const MEMORY_OWNER_BYTES = Buffer.from([
11
+ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
12
+ 0x05, 0x04, 0x01, 0x01, 0x20, 0x20,
13
+ 0x07, 0x0a, 0x01, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00
14
+ ]);
15
+
16
+ function writeMemoryOwnerModule(directory) {
17
+ const target = path.resolve(directory);
18
+ fs.mkdirSync(target, { recursive: true });
19
+ const file = path.join(target, 'guest-memory-owner.wasm');
20
+ if (!fs.existsSync(file) || !fs.readFileSync(file).equals(MEMORY_OWNER_BYTES)) {
21
+ writeAtomic(file, MEMORY_OWNER_BYTES);
22
+ }
23
+ return Object.freeze({ file, ...fileRecord(file) });
24
+ }
25
+
26
+ module.exports = Object.freeze({
27
+ MEMORY_OWNER_BYTES,
28
+ writeMemoryOwnerModule
29
+ });