space-data-module-sdk 0.8.9 → 0.8.10

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,467 @@
1
+ /**
2
+ * Declared host-contract surfaces and forbidden import classes.
3
+ *
4
+ * The isomorphism law says a module's differences are absorbed ONLY in SDK
5
+ * host shims — never in module code, and never by a runtime-specific glue
6
+ * layer baked into the artifact. That makes an artifact's IMPORT SECTION the
7
+ * primary, checkable statement of what host it demands:
8
+ *
9
+ * - every import must be a member of the surface the artifact DECLARES;
10
+ * - no import may belong to a FORBIDDEN class (emscripten glue: EH
11
+ * trampolines, `__cxa_*`, JS-library `__syscall_*`, `emscripten_*`
12
+ * runtime hooks, or the minified `a`-module shape emcc emits for the
13
+ * browser target). Those are `emcc`-shaped artifacts — browser-only,
14
+ * not instantiable on a plain WasmEdge host — which the standing module
15
+ * contract auto-rejects.
16
+ *
17
+ * This module is pure/structural and runs anywhere. It is NOT a substitute
18
+ * for real instantiation (see parityGate.js, which does both); it is what
19
+ * lets a real instantiation failure be CLASSIFIED — "this artifact wants a
20
+ * capability the contract grants but this lane's runner does not supply" is a
21
+ * completely different fact from "this artifact wants emscripten glue", and a
22
+ * gate that cannot tell them apart is a gate that lies.
23
+ */
24
+
25
+ /** WASI preview1 — every lane provides this (browser via the SDK shim). */
26
+ export const WASI_PREVIEW1_MODULE = "wasi_snapshot_preview1";
27
+
28
+ /**
29
+ * wasi-threads: modules spawn threads by IMPORTING `wasi.thread-spawn` and
30
+ * EXPORTING `wasi_thread_start` over a SHARED linear memory imported as
31
+ * `env.memory`. This is the clang `wasm32-wasip1-threads` shape — the ONLY
32
+ * sanctioned threading shape. `emcc -pthread` produces a different, browser-
33
+ * only shape and is rejected by the forbidden classes below.
34
+ */
35
+ export const WASI_THREADS_IMPORTS = Object.freeze([
36
+ "wasi.thread-spawn",
37
+ "env.memory",
38
+ ]);
39
+
40
+ /**
41
+ * The sanctioned synchronous hostcall bridge. This ONE import module carries
42
+ * the entire generic hook set (`http`/`tcp`/`wallet_sign`/`keyslot.sign`/
43
+ * clock/fs) as operations inside a binary envelope — that is why an SDK module
44
+ * needs no per-capability imports, and why any NEW private import is a NEW
45
+ * HOST CAPABILITY (an owner decision, never a PR).
46
+ *
47
+ * Supplied identically by both lanes: the browser/Node JS harness
48
+ * (`src/host/abi.js:10`) and the Go host bridge WasmEdge embeds
49
+ * (`kubo/sdn/modulert/hostbridge.go`). Names must match byte-for-byte in both.
50
+ */
51
+ export const HOSTCALL_IMPORT_MODULE = "space_data_module_host";
52
+ export const HOSTCALL_IMPORTS = Object.freeze([
53
+ `${HOSTCALL_IMPORT_MODULE}.call`,
54
+ `${HOSTCALL_IMPORT_MODULE}.response_len`,
55
+ `${HOSTCALL_IMPORT_MODULE}.read_response`,
56
+ `${HOSTCALL_IMPORT_MODULE}.clear_response`,
57
+ `${HOSTCALL_IMPORT_MODULE}.last_status_code`,
58
+ // Compiled flow-runtime artifacts dispatch the current invocation back
59
+ // through the same bridge (src/runtime/compiledRuntimeAbi.json).
60
+ `${HOSTCALL_IMPORT_MODULE}.dispatch_current_invocation`,
61
+ // Legacy compiled-flow artifacts import the same entry on `sdn_flow_host`;
62
+ // the SDK stubs it in both lanes (src/flow/flowRuntimeHost.js:241).
63
+ "sdn_flow_host.dispatch_current_invocation",
64
+ ]);
65
+
66
+ /**
67
+ * The seven FlatSQL VFS host functions, exactly as the engine imports them.
68
+ * Offsets are f64 (never i64): emscripten legalizes i64 across the JS
69
+ * boundary for the browser target and not for STANDALONE_WASM, which would
70
+ * give one import two different signatures in the two lanes.
71
+ */
72
+ export const FLATSQL_IO_IMPORTS = Object.freeze([
73
+ "env.flatsql_io_open",
74
+ "env.flatsql_io_read",
75
+ "env.flatsql_io_write",
76
+ "env.flatsql_io_truncate",
77
+ "env.flatsql_io_sync",
78
+ "env.flatsql_io_size",
79
+ "env.flatsql_io_close",
80
+ ]);
81
+
82
+ export const FLATSQL_IO_SIGNATURES = Object.freeze({
83
+ flatsql_io_open: { params: ["i32", "i32", "i32"], result: "i32" },
84
+ flatsql_io_read: { params: ["i32", "i32", "i32", "f64"], result: "i32" },
85
+ flatsql_io_write: { params: ["i32", "i32", "i32", "f64"], result: "i32" },
86
+ flatsql_io_truncate: { params: ["i32", "f64"], result: "i32" },
87
+ flatsql_io_sync: { params: ["i32"], result: "i32" },
88
+ flatsql_io_size: { params: ["i32"], result: "f64" },
89
+ flatsql_io_close: { params: ["i32"], result: "i32" },
90
+ });
91
+
92
+ /**
93
+ * Named host-contract surfaces. `wasiAny: true` means "any function on the
94
+ * WASI preview1 module is in-surface" — the WASI set is large, stable, and
95
+ * supplied wholesale by every lane; enumerating it would only rot.
96
+ */
97
+ export const HOST_SURFACES = Object.freeze({
98
+ /** An SDK module: WASI + wasi-threads + the ONE sanctioned hostcall bridge.
99
+ * Nothing else — the generic hook set rides inside that bridge, so private
100
+ * per-capability imports are always a contract violation. */
101
+ module: Object.freeze({
102
+ id: "module",
103
+ wasiAny: true,
104
+ extra: Object.freeze([...WASI_THREADS_IMPORTS, ...HOSTCALL_IMPORTS]),
105
+ description:
106
+ "WASI preview1 + wasi-threads (clang wasm32-wasip1-threads) + the space_data_module_host bridge",
107
+ }),
108
+ /** A WASI-only module: no hostcall bridge at all. The strictest surface. */
109
+ "module-standalone": Object.freeze({
110
+ id: "module-standalone",
111
+ wasiAny: true,
112
+ extra: WASI_THREADS_IMPORTS,
113
+ description: "WASI preview1 + wasi-threads ONLY (no hostcall bridge)",
114
+ }),
115
+ /** The FlatSQL engine artifact: WASI + the seven declared VFS functions. */
116
+ "flatsql-engine": Object.freeze({
117
+ id: "flatsql-engine",
118
+ wasiAny: true,
119
+ extra: Object.freeze([...WASI_THREADS_IMPORTS, ...FLATSQL_IO_IMPORTS]),
120
+ description: "WASI preview1 + the seven declared flatsql_io_* VFS imports",
121
+ }),
122
+ });
123
+
124
+ /**
125
+ * Forbidden import classes. Matching ANY of these is an auto-reject, in every
126
+ * lane, whether or not the lanes agree with each other: agreement that an
127
+ * artifact is broken everywhere is not parity.
128
+ */
129
+ export const FORBIDDEN_IMPORT_CLASSES = Object.freeze([
130
+ Object.freeze({
131
+ id: "emscripten-eh",
132
+ test: (module, name) =>
133
+ module === "env" &&
134
+ (/^invoke_[a-z]+$/.test(name) ||
135
+ name === "__resumeException" ||
136
+ name === "llvm_eh_typeid_for" ||
137
+ name.startsWith("__cxa_")),
138
+ reason:
139
+ "emscripten exception-handling glue (invoke_* trampolines / __cxa_* / __resumeException). Modules are EH-free; this artifact was built with emcc, not clang wasm32-wasip1-threads.",
140
+ }),
141
+ Object.freeze({
142
+ id: "emscripten-syscall",
143
+ test: (module, name) => module === "env" && name.startsWith("__syscall_"),
144
+ reason:
145
+ "emscripten JS-library syscall shims (__syscall_*). A WASI artifact reaches the filesystem through WASI or a declared VFS capability, never through JS syscalls.",
146
+ }),
147
+ Object.freeze({
148
+ id: "emscripten-runtime",
149
+ test: (module, name) =>
150
+ module === "env" &&
151
+ name.startsWith("emscripten_") &&
152
+ !name.startsWith("emscripten_notify_construct"),
153
+ reason:
154
+ "emscripten runtime hooks (emscripten_*, e.g. emscripten_resize_heap / emscripten_notify_memory_growth). These resolve only against the emscripten JS runtime — browser-only by construction.",
155
+ }),
156
+ Object.freeze({
157
+ id: "emscripten-minified",
158
+ // The emcc browser target emits a single-letter import module ("a") with
159
+ // single/double-letter member names. Nothing hand-written looks like this.
160
+ test: (module, name) => /^[a-z]$/.test(module) && /^[A-Za-z_$]{1,3}$/.test(name),
161
+ reason:
162
+ "minified emscripten browser artifact (single-letter import module). This is the emcc browser build, not the isomorphic artifact.",
163
+ }),
164
+ ]);
165
+
166
+ // --- Minimal wasm binary reader (type + import sections) ----------------------
167
+ //
168
+ // WebAssembly.Module.imports() reports names and kinds but NOT descriptors, and
169
+ // a probe that guesses a shared-memory descriptor or a function arity is a
170
+ // probe that can fail for reasons unrelated to the artifact. Read the real
171
+ // thing: exact signatures let both lanes synthesize EXACTLY the declared host
172
+ // surface, so a LinkError means what it says.
173
+
174
+ const WASM_VALTYPE = new Map([
175
+ [0x7f, "i32"],
176
+ [0x7e, "i64"],
177
+ [0x7d, "f32"],
178
+ [0x7c, "f64"],
179
+ [0x7b, "v128"],
180
+ [0x70, "funcref"],
181
+ [0x6f, "externref"],
182
+ ]);
183
+
184
+ function makeReader(bytes) {
185
+ let offset = 0;
186
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
187
+ return {
188
+ get offset() {
189
+ return offset;
190
+ },
191
+ set offset(value) {
192
+ offset = value;
193
+ },
194
+ get done() {
195
+ return offset >= bytes.length;
196
+ },
197
+ u8() {
198
+ return bytes[offset++];
199
+ },
200
+ u32() {
201
+ const value = view.getUint32(offset, true);
202
+ offset += 4;
203
+ return value;
204
+ },
205
+ varuint() {
206
+ let result = 0;
207
+ let shift = 0;
208
+ let byte;
209
+ do {
210
+ byte = bytes[offset++];
211
+ result |= (byte & 0x7f) << shift;
212
+ shift += 7;
213
+ } while (byte & 0x80);
214
+ return result >>> 0;
215
+ },
216
+ bytes(length) {
217
+ const slice = bytes.subarray(offset, offset + length);
218
+ offset += length;
219
+ return slice;
220
+ },
221
+ name() {
222
+ const length = this.varuint();
223
+ return new TextDecoder().decode(this.bytes(length));
224
+ },
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Read import descriptors with exact detail:
230
+ * function -> {kind:"function", params:[valtype], results:[valtype]}
231
+ * memory -> {kind:"memory", initial, maximum?, shared}
232
+ * table -> {kind:"table", element, initial, maximum?}
233
+ * global -> {kind:"global", valtype, mutable}
234
+ */
235
+ export function readWasmImportDescriptors(bytes) {
236
+ const reader = makeReader(bytes);
237
+ const magic = reader.u32();
238
+ const version = reader.u32();
239
+ if (magic !== 0x6d736100) throw new Error("not a wasm binary (bad magic)");
240
+ if (version !== 1) throw new Error(`unsupported wasm version ${version}`);
241
+
242
+ const types = [];
243
+ const imports = [];
244
+
245
+ while (!reader.done) {
246
+ const sectionId = reader.u8();
247
+ const sectionLength = reader.varuint();
248
+ const sectionEnd = reader.offset + sectionLength;
249
+ if (sectionId === 1) {
250
+ const count = reader.varuint();
251
+ for (let index = 0; index < count; index += 1) {
252
+ const form = reader.u8(); // 0x60 func
253
+ if (form !== 0x60) {
254
+ types.push(null);
255
+ reader.offset = sectionEnd;
256
+ break;
257
+ }
258
+ const paramCount = reader.varuint();
259
+ const params = [];
260
+ for (let p = 0; p < paramCount; p += 1) {
261
+ params.push(WASM_VALTYPE.get(reader.u8()) ?? "unknown");
262
+ }
263
+ const resultCount = reader.varuint();
264
+ const results = [];
265
+ for (let r = 0; r < resultCount; r += 1) {
266
+ results.push(WASM_VALTYPE.get(reader.u8()) ?? "unknown");
267
+ }
268
+ types.push({ params, results });
269
+ }
270
+ } else if (sectionId === 2) {
271
+ const count = reader.varuint();
272
+ for (let index = 0; index < count; index += 1) {
273
+ const moduleName = reader.name();
274
+ const fieldName = reader.name();
275
+ const kindByte = reader.u8();
276
+ if (kindByte === 0x00) {
277
+ const typeIndex = reader.varuint();
278
+ const type = types[typeIndex] ?? { params: [], results: [] };
279
+ imports.push({
280
+ module: moduleName,
281
+ name: fieldName,
282
+ kind: "function",
283
+ params: type.params,
284
+ results: type.results,
285
+ });
286
+ } else if (kindByte === 0x01) {
287
+ const element = reader.u8();
288
+ const limitsFlag = reader.varuint();
289
+ const initial = reader.varuint();
290
+ const maximum = limitsFlag & 0x01 ? reader.varuint() : undefined;
291
+ imports.push({
292
+ module: moduleName,
293
+ name: fieldName,
294
+ kind: "table",
295
+ element: element === 0x70 ? "anyfunc" : "externref",
296
+ initial,
297
+ maximum,
298
+ });
299
+ } else if (kindByte === 0x02) {
300
+ const limitsFlag = reader.varuint();
301
+ const initial = reader.varuint();
302
+ const maximum = limitsFlag & 0x01 ? reader.varuint() : undefined;
303
+ imports.push({
304
+ module: moduleName,
305
+ name: fieldName,
306
+ kind: "memory",
307
+ initial,
308
+ maximum,
309
+ shared: Boolean(limitsFlag & 0x02),
310
+ });
311
+ } else if (kindByte === 0x03) {
312
+ const valtype = WASM_VALTYPE.get(reader.u8()) ?? "unknown";
313
+ const mutable = reader.u8() === 1;
314
+ imports.push({
315
+ module: moduleName,
316
+ name: fieldName,
317
+ kind: "global",
318
+ valtype,
319
+ mutable,
320
+ });
321
+ } else {
322
+ throw new Error(`unknown import kind 0x${kindByte.toString(16)}`);
323
+ }
324
+ }
325
+ // Import section fully read; the type section preceded it, so stop.
326
+ reader.offset = sectionEnd;
327
+ break;
328
+ }
329
+ reader.offset = sectionEnd;
330
+ }
331
+
332
+ return imports;
333
+ }
334
+
335
+ /**
336
+ * Parse just the import section. Deliberately NOT `new WebAssembly.Module()`:
337
+ * compiling a 1.8 MB engine to read its import list is wasteful, and the
338
+ * structural verdict must be computable for artifacts a given engine refuses
339
+ * (that refusal is exactly what we are classifying).
340
+ */
341
+ export function readWasmImports(bytes) {
342
+ return readWasmImportDescriptors(bytes).map((entry) => ({
343
+ module: entry.module,
344
+ name: entry.name,
345
+ kind: entry.kind,
346
+ }));
347
+ }
348
+
349
+ export function readWasmExportNames(bytes) {
350
+ const module = new WebAssembly.Module(bytes);
351
+ return WebAssembly.Module.exports(module).map((entry) => entry.name);
352
+ }
353
+
354
+ export function resolveHostSurface(surfaceId) {
355
+ const surface = HOST_SURFACES[String(surfaceId)];
356
+ if (!surface) {
357
+ throw new Error(
358
+ `Unknown host-contract surface "${surfaceId}". Known: ${Object.keys(HOST_SURFACES).join(", ")}.`,
359
+ );
360
+ }
361
+ return surface;
362
+ }
363
+
364
+ export function importKey(entry) {
365
+ return `${entry.module}.${entry.name}`;
366
+ }
367
+
368
+ /**
369
+ * Classify an artifact's imports against a declared surface.
370
+ *
371
+ * Returns:
372
+ * {
373
+ * verdict: "in-surface" | "outside-surface" | "forbidden" | "malformed",
374
+ * imports, importCount, importDigestSource,
375
+ * forbidden: [{import, classId, reason}],
376
+ * outsideSurface: [key],
377
+ * capabilityImports: [key] // in-surface, non-WASI (needs a host shim)
378
+ * }
379
+ *
380
+ * "forbidden" outranks "outside-surface": naming the auto-reject class is the
381
+ * useful fact.
382
+ */
383
+ export function classifyArtifactImports(bytes, surfaceId) {
384
+ const surface = resolveHostSurface(surfaceId);
385
+ let imports;
386
+ try {
387
+ imports = readWasmImports(bytes);
388
+ } catch (error) {
389
+ return {
390
+ verdict: "malformed",
391
+ surface: surface.id,
392
+ error: error?.message ?? String(error),
393
+ imports: [],
394
+ importCount: 0,
395
+ forbidden: [],
396
+ outsideSurface: [],
397
+ capabilityImports: [],
398
+ };
399
+ }
400
+
401
+ const allowedExtra = new Set(surface.extra);
402
+ const forbidden = [];
403
+ const outsideSurface = [];
404
+ const capabilityImports = [];
405
+
406
+ for (const entry of imports) {
407
+ const key = importKey(entry);
408
+ const forbiddenClass = FORBIDDEN_IMPORT_CLASSES.find((klass) =>
409
+ klass.test(entry.module, entry.name),
410
+ );
411
+ if (forbiddenClass) {
412
+ forbidden.push({
413
+ import: key,
414
+ classId: forbiddenClass.id,
415
+ reason: forbiddenClass.reason,
416
+ });
417
+ continue;
418
+ }
419
+ if (surface.wasiAny && entry.module === WASI_PREVIEW1_MODULE) continue;
420
+ if (allowedExtra.has(key)) {
421
+ capabilityImports.push(key);
422
+ continue;
423
+ }
424
+ outsideSurface.push(key);
425
+ }
426
+
427
+ let verdict = "in-surface";
428
+ if (forbidden.length > 0) verdict = "forbidden";
429
+ else if (outsideSurface.length > 0) verdict = "outside-surface";
430
+
431
+ return {
432
+ verdict,
433
+ surface: surface.id,
434
+ imports: imports.map(importKey),
435
+ importCount: imports.length,
436
+ forbidden,
437
+ outsideSurface,
438
+ capabilityImports,
439
+ };
440
+ }
441
+
442
+ /**
443
+ * Summarize a classification in one line — used verbatim in gate reports so
444
+ * the receipt names the defect class, not just "failed".
445
+ */
446
+ export function describeClassification(classification) {
447
+ switch (classification.verdict) {
448
+ case "malformed":
449
+ return `malformed wasm (${classification.error})`;
450
+ case "forbidden": {
451
+ const classes = [
452
+ ...new Set(classification.forbidden.map((item) => item.classId)),
453
+ ].join(", ");
454
+ const sample = classification.forbidden
455
+ .slice(0, 3)
456
+ .map((item) => item.import)
457
+ .join(", ");
458
+ return `FORBIDDEN import class [${classes}] — ${classification.forbidden.length} import(s), e.g. ${sample}`;
459
+ }
460
+ case "outside-surface":
461
+ return `imports outside declared surface "${classification.surface}": ${classification.outsideSurface.slice(0, 6).join(", ")}`;
462
+ default:
463
+ return classification.capabilityImports.length > 0
464
+ ? `in-surface (WASI + declared capabilities: ${classification.capabilityImports.length})`
465
+ : "in-surface (WASI only)";
466
+ }
467
+ }
@@ -48,6 +48,38 @@ export {
48
48
  runDockerWasmEdgeLane,
49
49
  runNativeWasmEdgeLane,
50
50
  } from "./parityLanes.js";
51
+ export {
52
+ FLATSQL_IO_IMPORTS,
53
+ FLATSQL_IO_SIGNATURES,
54
+ FORBIDDEN_IMPORT_CLASSES,
55
+ HOSTCALL_IMPORTS,
56
+ HOSTCALL_IMPORT_MODULE,
57
+ HOST_SURFACES,
58
+ WASI_THREADS_IMPORTS,
59
+ classifyArtifactImports,
60
+ describeClassification,
61
+ readWasmImportDescriptors,
62
+ readWasmImports,
63
+ resolveHostSurface,
64
+ } from "./hostContract.js";
65
+ export {
66
+ normalizeWasmEdgeOutcome,
67
+ splitWasmEdgeDiagnostics,
68
+ } from "./wasmedgeOutput.js";
69
+ export {
70
+ ContractVerdict,
71
+ DEFAULT_GATE_MANIFEST,
72
+ LANE_EVIDENCE,
73
+ classifyWasmEdgeProbe,
74
+ deriveContractVerdict,
75
+ detectNativeWasmEdge,
76
+ formatGateReport,
77
+ gateReceiptDigest,
78
+ lanesAgree,
79
+ loadGateManifest,
80
+ packageRootDir,
81
+ runParityGate,
82
+ } from "./parityGate.js";
51
83
 
52
84
  const CapabilitySurfaceMatrix = Object.freeze({
53
85
  logging: Object.freeze({