space-data-module-sdk 0.8.11 → 0.8.13

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.
Files changed (40) hide show
  1. package/README.md +92 -0
  2. package/bin/space-data-module.js +85 -1
  3. package/docs/module-publication-standard.md +7 -3
  4. package/docs/propagator-abi.md +477 -0
  5. package/include/orbpro/orbpro_propagator_abi.h +312 -0
  6. package/package.json +7 -1
  7. package/schemas/PluginManifest.fbs +46 -1
  8. package/schemas/orbpro/Propagator.fbs +161 -3
  9. package/src/browser.js +11 -0
  10. package/src/bundle/index.js +1 -0
  11. package/src/bundle/sigdomain.js +22 -0
  12. package/src/capabilities.js +91 -0
  13. package/src/compliance/index.js +8 -0
  14. package/src/compliance/pluginCompliance.js +76 -32
  15. package/src/flow/flowCompiler.js +231 -3
  16. package/src/flow/flowRuntimeHost.js +26 -0
  17. package/src/flow/isomorphicFlowHost.js +8 -0
  18. package/src/generated/orbpro/manifest/plugin-family.d.ts +9 -1
  19. package/src/generated/orbpro/manifest/plugin-family.js +8 -0
  20. package/src/generated/orbpro/manifest/plugin-family.ts +8 -0
  21. package/src/generated/orbpro/propagator-abi.js +118 -0
  22. package/src/generated/orbpro/propagator-abi.ts +199 -0
  23. package/src/host/browserModuleHarness.js +26 -0
  24. package/src/host/isomorphicLoader.js +57 -11
  25. package/src/host/runtimeTargetGate.js +256 -0
  26. package/src/host/workerModuleHarness.js +7 -0
  27. package/src/index.d.ts +47 -0
  28. package/src/index.js +11 -0
  29. package/src/manifest/normalize.js +113 -4
  30. package/src/scaffold/copyTemplate.js +71 -0
  31. package/src/scaffold/index.js +150 -0
  32. package/src/scaffold/tokens.js +90 -0
  33. package/src/testing/parityBrowserRunner.js +11 -0
  34. package/src/testing/parityGate.js +287 -27
  35. package/templates/propagator-module/README.md +99 -0
  36. package/templates/propagator-module/build.js +103 -0
  37. package/templates/propagator-module/package.json +19 -0
  38. package/templates/propagator-module/plugin-manifest.json +66 -0
  39. package/templates/propagator-module/src/__MODULE_NAME_SNAKE__.cpp +450 -0
  40. package/templates/propagator-module/tests/module.build.test.mjs +103 -0
package/README.md CHANGED
@@ -241,6 +241,98 @@ treats that as the explicit "one binary for both" profile. That pair now
241
241
  defaults to a shared `single-thread` artifact so the compiled wasm can be loaded
242
242
  unchanged by the browser harness and the WasmEdge harness.
243
243
 
244
+ ### Composed flows derive their targets
245
+
246
+ A COMPOSED flow artifact (`space-data-module flow compile`) does not declare
247
+ its own `runtimeTargets` — it **derives** them, inside the universe a composed
248
+ artifact can reach at all (`browser` + `wasmedge`; the flow runtime template is
249
+ never a standalone `wasi` command, a `node` package, a `desktop` bundle or an
250
+ `edge` worker). Two things narrow that universe:
251
+
252
+ 1. **Every part's declaration**, tested with `runtimeTargetSatisfies` — THE
253
+ SAME function every loader uses, so a declaration cannot mean one thing to
254
+ the compiler and another at the door. A flow runs only where all of its
255
+ parts run. A part that declares nothing constrains nothing. `wasi` is the
256
+ strict portability baseline rather than a fourth runtime, so a part
257
+ declaring it admits both legs — unless that part's own capabilities say
258
+ otherwise (`pipe` is in both the standalone-WASI subset and the
259
+ browser-incompatible set).
260
+ 2. **The capability union.** A capability in `BrowserIncompatibleCapabilityIds`
261
+ (`wallet_sign`, `tcp`, `storage_write`, …) drops `browser` regardless of what
262
+ anything declared, because the composition provably cannot run there. This is
263
+ the proof-based half: it catches the commonest manifest shape, a plugin that
264
+ carries such a capability and simply omits `runtimeTargets`.
265
+
266
+ Outcomes:
267
+
268
+ - All parts isomorphic, no browser-incompatible capability → the flow keeps
269
+ `["browser", "wasmedge"]`.
270
+ - One WasmEdge-only part, or one browser-incompatible capability → the flow is
271
+ `["wasmedge"]`, its `buildArtifacts[].target` says `wasmedge`, and a
272
+ `narrowed-runtime-targets` warning names what cost it the browser. Losing a
273
+ runtime is never silent.
274
+ - No shared runtime → `empty-runtime-target-intersection`, a hard error naming
275
+ the constraining plugins.
276
+
277
+ This is what makes a host-only capability usable from a flow at all: the
278
+ composed manifest is compliance-validated before the bake, and a
279
+ browser-incompatible capability next to a `browser` target is a hard
280
+ `capability-runtime-conflict`. Deriving the target set keeps that rule intact —
281
+ the conflict still fires whenever a browser target is genuinely claimed — while
282
+ letting the legitimate WasmEdge-only composition exist. `wallet_sign` gates
283
+ `keyslot.unwrap`, so before this the hardcoded pair meant no flow could unwrap a
284
+ host-held key at all.
285
+
286
+ ### Both legs refuse an artifact that is not theirs
287
+
288
+ A legitimate single-leg artifact now exists, so every loader that stands for a
289
+ runtime leg refuses one that declares itself out of scope — by name, quoting
290
+ the declaration, never a silent skip and never a trap five frames deep inside a
291
+ capability that leg cannot serve. The shared assert is
292
+ `src/host/runtimeTargetGate.js`:
293
+
294
+ | Loader | Leg |
295
+ | --- | --- |
296
+ | `createBrowserModuleHarness` | `browser` |
297
+ | `createWorkerModuleHarness` | `browser`, checked before any worker is spawned |
298
+ | `createIsomorphicFlowRuntimeHost` | `browser` (it mounts children in the browser harness) |
299
+ | `createFlowRuntimeHost` | stated via `runtimeTarget`; defaults to `browser` in a real browser, ungated elsewhere |
300
+ | `loadModule({runtimeKind: "wasmedge"})` | `wasmedge` |
301
+
302
+ Both declarations are consulted — the caller-supplied manifest and the
303
+ artifact's own embedded `$PLG` — and **the embedded one wins on conflict**,
304
+ because that is what the artifact's signature covers. The refusal throws a
305
+ `RuntimeTargetError` carrying `declaredTargets`, `leg` and `declarationSource`.
306
+
307
+ A declaration that names the leg outright is trusted as written; the `wasi`
308
+ baseline, which is an inference the SDK makes on the author's behalf, is
309
+ additionally capability-checked. Declaration → trust the author; inference →
310
+ prove it.
311
+
312
+ **Consumer migration.** `createFlowRuntimeHost` is runtime-agnostic and only
313
+ detects a real browser. If you drive a composed flow from Node — a flow test, a
314
+ server-side runner — state the leg, or the mirror gate is principle rather than
315
+ fact:
316
+
317
+ ```js
318
+ const host = await createFlowRuntimeHost({
319
+ wasmSource: bytes,
320
+ runtimeTarget: "wasmedge",
321
+ });
322
+ ```
323
+
324
+ The tri-runtime parity gate reads the same declaration: a lane the artifact
325
+ declared itself out of is scored `out-of-declared-scope`, skipped before it is
326
+ launched, and recorded as evidence rather than compared. Without that, a
327
+ correct refusal on the browser lane would have been counted as a P1
328
+ cross-runtime divergence.
329
+
330
+ Scoping is not a way to be certified without being run. An artifact in the
331
+ gate's certified set that scopes itself out of every active lane fails
332
+ (`artifact-out-of-every-lane`), and one that leaves fewer than two lanes to
333
+ compare fails too (`artifact-not-cross-runtime-comparable`) — a single lane
334
+ proves no parity, and one manifest string must never disarm the gate.
335
+
244
336
  ## WasmEdge Pthreads
245
337
 
246
338
  `space-data-module-sdk` is also the source of truth for module thread-model
@@ -34,6 +34,8 @@ async function main(argv) {
34
34
  return runCheck(rest);
35
35
  case "compile":
36
36
  return runCompile(rest);
37
+ case "init":
38
+ return runInit(rest);
37
39
  case "flow":
38
40
  return runFlow(rest);
39
41
  case "parity":
@@ -98,6 +100,18 @@ function parseArgs(argv) {
98
100
  case "--out":
99
101
  options.outputPath = path.resolve(requireValue(argv, ++index, value));
100
102
  break;
103
+ case "--family":
104
+ options.family = requireValue(argv, ++index, value);
105
+ break;
106
+ case "--name":
107
+ options.name = requireValue(argv, ++index, value);
108
+ break;
109
+ case "--plugin-id":
110
+ options.pluginId = requireValue(argv, ++index, value);
111
+ break;
112
+ case "--force":
113
+ options.force = true;
114
+ break;
101
115
  case "--recipient-public-key":
102
116
  options.recipientPublicKeyHex = requireValue(argv, ++index, value);
103
117
  break;
@@ -170,6 +184,28 @@ function parseArgs(argv) {
170
184
  });
171
185
  break;
172
186
  }
187
+ case "--artifact-lanes": {
188
+ // --artifact-lanes <id>=<lane,lane> — the GATE'S claim about which
189
+ // lanes an artifact owes evidence on. Without this the strongest of the
190
+ // three scoping rules ("the artifact does not pick its examiners") was
191
+ // reachable only from a gate manifest file, i.e. never for the
192
+ // cross-repo artifacts injected with --artifact, which are exactly the
193
+ // ones whose declarations this repo does not own.
194
+ const spec = requireValue(argv, ++index, value);
195
+ const eq = spec.indexOf("=");
196
+ if (eq < 1) {
197
+ throw new Error(
198
+ `--artifact-lanes expects <id>=<lane,lane>, got "${spec}"`,
199
+ );
200
+ }
201
+ options.expectedLanesById = options.expectedLanesById ?? {};
202
+ options.expectedLanesById[spec.slice(0, eq)] = spec
203
+ .slice(eq + 1)
204
+ .split(",")
205
+ .map((lane) => lane.trim())
206
+ .filter(Boolean);
207
+ break;
208
+ }
173
209
  case "--require-native-wasmedge":
174
210
  options.requireNativeWasmEdge = true;
175
211
  break;
@@ -197,6 +233,8 @@ function printUsage() {
197
233
  space-data-module check --repo-root .
198
234
  space-data-module check --manifest ./manifest.json --wasm ./dist/module.wasm
199
235
  space-data-module compile --manifest ./manifest.json --source ./src/module.c --out ./dist/module.wasm
236
+ space-data-module init --family propagator --name my-propagator
237
+ space-data-module init --family propagator --name my-propagator --out ./modules/my-propagator [--plugin-id com.orbpro.my.propagator] [--force] [--json]
200
238
  space-data-module parity --wasm ./dist/isomorphic/module.wasm --fixture ./fixtures/parity/basic.json
201
239
  space-data-module parity --wasm ./dist/isomorphic/module.wasm --fixture ./f.json --lanes browser,wasmedge,docker-wasmedge [--json]
202
240
  space-data-module parity ... --self-test-divergence docker-wasmedge (fire drill: prove the diff fails loudly)
@@ -264,6 +302,7 @@ async function runFlow(argv) {
264
302
  console.log(` ${issue.severity.toUpperCase()} ${issue.code}: ${issue.message}`);
265
303
  }
266
304
  console.log(` capabilities: [${check.capabilities.join(", ")}]`);
305
+ console.log(` runtimeTargets: [${(check.runtimeTargets ?? []).join(", ")}]`);
267
306
  for (const node of check.nodes) {
268
307
  console.log(` node ${node.nodeId}: ${node.pluginId}:${node.methodId} (${node.dispatchModel})`);
269
308
  }
@@ -307,6 +346,9 @@ async function runFlow(argv) {
307
346
  } else {
308
347
  console.log(`Wrote ${result.outputs.moduleWasmPath}`);
309
348
  console.log(` capabilities: [${result.check.capabilities.join(", ")}]`);
349
+ console.log(
350
+ ` runtimeTargets: [${(result.manifest?.runtimeTargets ?? result.check.runtimeTargets ?? []).join(", ")}]`,
351
+ );
310
352
  for (const node of result.check.nodes) {
311
353
  console.log(` node ${node.nodeId}: ${node.pluginId}:${node.methodId} (${node.dispatchModel})`);
312
354
  }
@@ -379,7 +421,13 @@ async function runParityGateCommand(argv) {
379
421
  );
380
422
  const report = await runParityGate({
381
423
  manifestPath: options.gateManifestPath,
382
- extraArtifacts: options.extraArtifacts,
424
+ extraArtifacts: (options.extraArtifacts ?? []).map((artifact) => ({
425
+ ...artifact,
426
+ ...(options.expectedLanesById?.[artifact.id]
427
+ ? { expectedLanes: options.expectedLanesById[artifact.id] }
428
+ : {}),
429
+ })),
430
+ expectedLanesById: options.expectedLanesById,
383
431
  lanes: options.lanes,
384
432
  timeoutMs: options.timeoutMs,
385
433
  chromeBinary: options.chromeBinary,
@@ -500,6 +548,42 @@ async function runCompile(argv) {
500
548
  return result.report.ok ? 0 : 1;
501
549
  }
502
550
 
551
+ // space-data-module init --family propagator --name <module-name>
552
+ // [--out <dir>] [--plugin-id <id>] [--force] [--json]
553
+ //
554
+ // Scaffolds a new SDN WASM module skeleton from templates/<family>-module/.
555
+ // An unrecognized --family FAILS LOUDLY (see src/scaffold/index.js) — there
556
+ // is no generic fallback template.
557
+ async function runInit(argv) {
558
+ const options = parseArgs(argv);
559
+ if (!options.family) {
560
+ throw new Error("init requires --family <family> (e.g. --family propagator).");
561
+ }
562
+ if (!options.name) {
563
+ throw new Error("init requires --name <module-name>.");
564
+ }
565
+ const { scaffoldModule } = await import("../src/scaffold/index.js");
566
+ const result = await scaffoldModule({
567
+ family: options.family,
568
+ name: options.name,
569
+ outDir: options.outputPath ?? undefined,
570
+ pluginId: options.pluginId,
571
+ force: options.force === true,
572
+ });
573
+ if (options.json) {
574
+ console.log(JSON.stringify(result, null, 2));
575
+ } else {
576
+ console.log(
577
+ `Scaffolded ${result.family} module "${result.name}" into ${result.outDir}`,
578
+ );
579
+ console.log(` pluginId=${result.pluginId}`);
580
+ for (const file of result.files) {
581
+ console.log(` created ${file}`);
582
+ }
583
+ }
584
+ return result.ok ? 0 : 1;
585
+ }
586
+
503
587
  async function runProtect(argv) {
504
588
  const options = parseArgs(argv);
505
589
  if (!options.manifestPath || !options.wasmPath) {
@@ -128,7 +128,9 @@ instantiates and the node's capability policy identifies by content hash.
128
128
 
129
129
  The domain prefix exists because the node's publisher key is the NODE key, and
130
130
  that one bonded key signs several unrelated kinds of statement (dataset
131
- publications, module artifacts, andreserved update manifests). If every
131
+ publications, module artifacts, update SIGNALS the advisory pub/sub nudge a
132
+ publisher pushes so every install upgrades itself in place, which has a live
133
+ producer — and, still reserved, update manifests). If every
132
134
  statement were a bare SHA-256 digest, a caller who could reach the node's
133
135
  signing endpoint could submit the bytes of one kind of document and staple the
134
136
  returned signature onto another. An ASCII domain label, a `NUL` that cannot
@@ -137,8 +139,10 @@ in a disjoint message space with no length ambiguity anywhere in the preimage.
137
139
 
138
140
  The domain registry is **closed**: a verifier refuses any label that is not
139
141
  registered, and a module verifier additionally refuses any registered label
140
- other than `SDN-MODULE-PUBLICATION-V1` — so a signature minted for a signed
141
- update can never be replayed into a module trailer. Adding a domain is a
142
+ other than `SDN-MODULE-PUBLICATION-V1` — so a signature minted for an update
143
+ manifest or an update signal can never be replayed into a module trailer, which
144
+ the shared vectors pin as `foreign-registered-domain` and
145
+ `update-signal-domain-is-refused-for-a-module`. Adding a domain is a
142
146
  reviewed change in every implementation at once
143
147
  (`src/bundle/sigdomain.js` here; `internal/sigdomain` and its kubo twin in the
144
148
  node), never a request parameter.