space-data-module-sdk 0.8.10 → 0.8.12
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 +92 -0
- package/bin/space-data-module.js +33 -1
- package/package.json +2 -1
- package/src/browser.js +11 -0
- package/src/capabilities.js +39 -0
- package/src/compliance/index.js +2 -0
- package/src/compliance/pluginCompliance.js +2 -18
- package/src/flow/flowCompiler.js +215 -3
- package/src/flow/flowRuntimeHost.js +26 -0
- package/src/flow/isomorphicFlowHost.js +8 -0
- package/src/host/browserModuleHarness.js +26 -0
- package/src/host/isomorphicLoader.js +57 -11
- package/src/host/runtimeTargetGate.js +256 -0
- package/src/host/workerModuleHarness.js +7 -0
- package/src/index.d.ts +47 -0
- package/src/index.js +11 -0
- package/src/standards/catalogCore.js +28 -1
- package/src/testing/parityBrowserRunner.js +11 -0
- package/src/testing/parityGate.js +351 -38
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
|
package/bin/space-data-module.js
CHANGED
|
@@ -170,6 +170,28 @@ function parseArgs(argv) {
|
|
|
170
170
|
});
|
|
171
171
|
break;
|
|
172
172
|
}
|
|
173
|
+
case "--artifact-lanes": {
|
|
174
|
+
// --artifact-lanes <id>=<lane,lane> — the GATE'S claim about which
|
|
175
|
+
// lanes an artifact owes evidence on. Without this the strongest of the
|
|
176
|
+
// three scoping rules ("the artifact does not pick its examiners") was
|
|
177
|
+
// reachable only from a gate manifest file, i.e. never for the
|
|
178
|
+
// cross-repo artifacts injected with --artifact, which are exactly the
|
|
179
|
+
// ones whose declarations this repo does not own.
|
|
180
|
+
const spec = requireValue(argv, ++index, value);
|
|
181
|
+
const eq = spec.indexOf("=");
|
|
182
|
+
if (eq < 1) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`--artifact-lanes expects <id>=<lane,lane>, got "${spec}"`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
options.expectedLanesById = options.expectedLanesById ?? {};
|
|
188
|
+
options.expectedLanesById[spec.slice(0, eq)] = spec
|
|
189
|
+
.slice(eq + 1)
|
|
190
|
+
.split(",")
|
|
191
|
+
.map((lane) => lane.trim())
|
|
192
|
+
.filter(Boolean);
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
173
195
|
case "--require-native-wasmedge":
|
|
174
196
|
options.requireNativeWasmEdge = true;
|
|
175
197
|
break;
|
|
@@ -264,6 +286,7 @@ async function runFlow(argv) {
|
|
|
264
286
|
console.log(` ${issue.severity.toUpperCase()} ${issue.code}: ${issue.message}`);
|
|
265
287
|
}
|
|
266
288
|
console.log(` capabilities: [${check.capabilities.join(", ")}]`);
|
|
289
|
+
console.log(` runtimeTargets: [${(check.runtimeTargets ?? []).join(", ")}]`);
|
|
267
290
|
for (const node of check.nodes) {
|
|
268
291
|
console.log(` node ${node.nodeId}: ${node.pluginId}:${node.methodId} (${node.dispatchModel})`);
|
|
269
292
|
}
|
|
@@ -307,6 +330,9 @@ async function runFlow(argv) {
|
|
|
307
330
|
} else {
|
|
308
331
|
console.log(`Wrote ${result.outputs.moduleWasmPath}`);
|
|
309
332
|
console.log(` capabilities: [${result.check.capabilities.join(", ")}]`);
|
|
333
|
+
console.log(
|
|
334
|
+
` runtimeTargets: [${(result.manifest?.runtimeTargets ?? result.check.runtimeTargets ?? []).join(", ")}]`,
|
|
335
|
+
);
|
|
310
336
|
for (const node of result.check.nodes) {
|
|
311
337
|
console.log(` node ${node.nodeId}: ${node.pluginId}:${node.methodId} (${node.dispatchModel})`);
|
|
312
338
|
}
|
|
@@ -379,7 +405,13 @@ async function runParityGateCommand(argv) {
|
|
|
379
405
|
);
|
|
380
406
|
const report = await runParityGate({
|
|
381
407
|
manifestPath: options.gateManifestPath,
|
|
382
|
-
extraArtifacts: options.extraArtifacts
|
|
408
|
+
extraArtifacts: (options.extraArtifacts ?? []).map((artifact) => ({
|
|
409
|
+
...artifact,
|
|
410
|
+
...(options.expectedLanesById?.[artifact.id]
|
|
411
|
+
? { expectedLanes: options.expectedLanesById[artifact.id] }
|
|
412
|
+
: {}),
|
|
413
|
+
})),
|
|
414
|
+
expectedLanesById: options.expectedLanesById,
|
|
383
415
|
lanes: options.lanes,
|
|
384
416
|
timeoutMs: options.timeoutMs,
|
|
385
417
|
chromeBinary: options.chromeBinary,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "space-data-module-sdk",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.12",
|
|
4
4
|
"description": "Module SDK for building, validating, signing, and deploying WebAssembly modules on the Space Data Network.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./src/index.d.ts",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"default": "./src/host/isomorphicLoader.js"
|
|
56
56
|
},
|
|
57
57
|
"./host/browser-module": "./src/host/browserModuleHarness.js",
|
|
58
|
+
"./host/runtime-target-gate": "./src/host/runtimeTargetGate.js",
|
|
58
59
|
"./host/node-builtin": "./src/host/nodeBuiltinSpecifier.js",
|
|
59
60
|
"./host/worker-module": "./src/host/workerModuleHarness.js",
|
|
60
61
|
"./testing/browser": "./src/testing/browser.js",
|
package/src/browser.js
CHANGED
|
@@ -23,10 +23,21 @@ export * from "./host/isomorphicLoaderBrowser.js";
|
|
|
23
23
|
// Browser module hosts. These are runtime surface and live in src/host/;
|
|
24
24
|
// nothing under src/testing/ is reachable from this entry.
|
|
25
25
|
export {
|
|
26
|
+
assertBrowserRuntimeTarget,
|
|
26
27
|
createBrowserModuleHarness,
|
|
27
28
|
detectArtifactProfile,
|
|
28
29
|
zeroWasmBytes,
|
|
29
30
|
isSharedArrayBufferLike,
|
|
30
31
|
} from "./host/browserModuleHarness.js";
|
|
32
|
+
// The runtime-target gate is reachable so a consumer can catch the refusal by
|
|
33
|
+
// CLASS (`error instanceof RuntimeTargetError`) rather than by matching a
|
|
34
|
+
// message string, and can ask the same question the loaders ask before it
|
|
35
|
+
// offers a module to a leg.
|
|
36
|
+
export {
|
|
37
|
+
RuntimeTargetError,
|
|
38
|
+
runtimeTargetSatisfies,
|
|
39
|
+
resolveRuntimeTargetRefusal,
|
|
40
|
+
embeddedRuntimeTargets,
|
|
41
|
+
} from "./host/runtimeTargetGate.js";
|
|
31
42
|
export { createWorkerModuleHarness } from "./host/workerModuleHarness.js";
|
|
32
43
|
export { createModuleFlatBufferStreamPump } from "./host/moduleFlatbufferStreamPump.js";
|
package/src/capabilities.js
CHANGED
|
@@ -47,3 +47,42 @@ export const StandaloneWasiCapabilityIds = Object.freeze([
|
|
|
47
47
|
"filesystem",
|
|
48
48
|
"pipe",
|
|
49
49
|
]);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Capabilities a PORTABLE artifact may not claim against the canonical browser
|
|
53
|
+
* runtime target. The browser HOST can route several of these when the
|
|
54
|
+
* embedder injects an adapter (see BrowserHostSupportedCapabilities) — this
|
|
55
|
+
* list is the DECLARATION policy, not the dispatch table: a published module
|
|
56
|
+
* cannot assume its embedder wired one.
|
|
57
|
+
*
|
|
58
|
+
* It lives here, beside the other capability vocabularies, rather than inside
|
|
59
|
+
* the compliance module, because two browser-facing surfaces need it: the flow
|
|
60
|
+
* compiler (which subtracts `browser` from a composed artifact's derived
|
|
61
|
+
* runtimeTargets on exactly this list, so the compiler never stamps a target
|
|
62
|
+
* its own compliance pass would reject) and the runtime-target gate (which
|
|
63
|
+
* must not admit an artifact to a leg on the `wasi` portability baseline while
|
|
64
|
+
* it carries a capability that leg cannot serve).
|
|
65
|
+
*/
|
|
66
|
+
export const BrowserIncompatibleCapabilityIds = Object.freeze([
|
|
67
|
+
"pipe",
|
|
68
|
+
"network",
|
|
69
|
+
"tcp",
|
|
70
|
+
"udp",
|
|
71
|
+
"mqtt",
|
|
72
|
+
"tls",
|
|
73
|
+
"database",
|
|
74
|
+
"storage_write",
|
|
75
|
+
"protocol_dial",
|
|
76
|
+
"protocol_handle",
|
|
77
|
+
"process_exec",
|
|
78
|
+
"wallet_sign",
|
|
79
|
+
"ipfs",
|
|
80
|
+
"scene_access",
|
|
81
|
+
"entity_access",
|
|
82
|
+
"render_hooks",
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
/** Which capabilities each runtime leg cannot serve. */
|
|
86
|
+
export const LegIncompatibleCapabilityIds = Object.freeze({
|
|
87
|
+
browser: BrowserIncompatibleCapabilityIds,
|
|
88
|
+
});
|
package/src/compliance/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
validatePluginManifest,
|
|
12
12
|
} from "./pluginCompliance.js";
|
|
13
13
|
import {
|
|
14
|
+
BrowserIncompatibleCapabilityIds,
|
|
14
15
|
RecommendedCapabilityIds,
|
|
15
16
|
StandaloneWasiCapabilityIds,
|
|
16
17
|
} from "../capabilities.js";
|
|
@@ -48,6 +49,7 @@ export async function validateArtifactWithStandards(options = {}) {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
export {
|
|
52
|
+
BrowserIncompatibleCapabilityIds,
|
|
51
53
|
findManifestFiles,
|
|
52
54
|
getWasmExportNames,
|
|
53
55
|
getWasmExportNamesFromFile,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
RuntimeTarget,
|
|
14
14
|
} from "../runtime/constants.js";
|
|
15
15
|
import {
|
|
16
|
+
BrowserIncompatibleCapabilityIds,
|
|
16
17
|
RecommendedCapabilityIds,
|
|
17
18
|
StandaloneWasiCapabilityIds,
|
|
18
19
|
} from "../capabilities.js";
|
|
@@ -49,24 +50,7 @@ const ExternalInterfaceDirectionSet = new Set(
|
|
|
49
50
|
const ExternalInterfaceKindSet = new Set(Object.values(ExternalInterfaceKind));
|
|
50
51
|
const ProtocolRoleSet = new Set(Object.values(ProtocolRole));
|
|
51
52
|
const ProtocolTransportKindSet = new Set(Object.values(ProtocolTransportKind));
|
|
52
|
-
const BrowserIncompatibleCapabilitySet = new Set(
|
|
53
|
-
"pipe",
|
|
54
|
-
"network",
|
|
55
|
-
"tcp",
|
|
56
|
-
"udp",
|
|
57
|
-
"mqtt",
|
|
58
|
-
"tls",
|
|
59
|
-
"database",
|
|
60
|
-
"storage_write",
|
|
61
|
-
"protocol_dial",
|
|
62
|
-
"protocol_handle",
|
|
63
|
-
"process_exec",
|
|
64
|
-
"wallet_sign",
|
|
65
|
-
"ipfs",
|
|
66
|
-
"scene_access",
|
|
67
|
-
"entity_access",
|
|
68
|
-
"render_hooks",
|
|
69
|
-
]);
|
|
53
|
+
const BrowserIncompatibleCapabilitySet = new Set(BrowserIncompatibleCapabilityIds);
|
|
70
54
|
const StandaloneWasiProtocolTransportKindSet = new Set([
|
|
71
55
|
ProtocolTransportKind.WASI_PIPE,
|
|
72
56
|
]);
|
package/src/flow/flowCompiler.js
CHANGED
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
} from "../compiler/pthreadArtifactGuard.js";
|
|
57
57
|
import { resolveWasiThreadsToolchain } from "../compiler/wasiThreadsToolchain.js";
|
|
58
58
|
import {
|
|
59
|
+
BrowserIncompatibleCapabilityIds,
|
|
59
60
|
validateArtifactWithStandards,
|
|
60
61
|
validatePluginManifest,
|
|
61
62
|
} from "../compliance/index.js";
|
|
@@ -66,6 +67,7 @@ import {
|
|
|
66
67
|
payloadSchemaIdentitiesEqual,
|
|
67
68
|
} from "../manifest/index.js";
|
|
68
69
|
import { appendWasmCustomSection } from "../bundle/wasm.js";
|
|
70
|
+
import { runtimeTargetSatisfies } from "../host/runtimeTargetGate.js";
|
|
69
71
|
import { SDS_MANIFEST_SECTION_NAME } from "../bundle/constants.js";
|
|
70
72
|
import { verifyModuleArtifact } from "../bundle/signing.js";
|
|
71
73
|
import { DOMAIN_MODULE_PUBLICATION_V1 } from "../bundle/sigdomain.js";
|
|
@@ -162,6 +164,16 @@ export function flowEngineLinkage(flow) {
|
|
|
162
164
|
);
|
|
163
165
|
}
|
|
164
166
|
|
|
167
|
+
// The runtimes a COMPOSED flow artifact can possibly reach. The flow runtime
|
|
168
|
+
// template is baked once, as the isomorphic browser/WasmEdge artifact — it is
|
|
169
|
+
// never a standalone `wasi` command, a `node` package, a `desktop` bundle, or
|
|
170
|
+
// an `edge` worker, so those targets are not in the universe a composition can
|
|
171
|
+
// claim. A node's own declaration can only ever SUBTRACT from this set.
|
|
172
|
+
const COMPOSED_FLOW_RUNTIME_TARGET_UNIVERSE = Object.freeze([
|
|
173
|
+
"browser",
|
|
174
|
+
"wasmedge",
|
|
175
|
+
]);
|
|
176
|
+
|
|
165
177
|
const GUEST_LINK_OBJECT_FILENAME = "module-link.o";
|
|
166
178
|
const GUEST_LINK_METADATA_FILENAME = "metadata.json";
|
|
167
179
|
const GUEST_LINK_LINKED_DIRNAME = "guest-link-linked";
|
|
@@ -882,7 +894,9 @@ function collectComponentDependencies({ nodePluginIds, dependencies, issues, cap
|
|
|
882
894
|
"warning",
|
|
883
895
|
"unresolved-component-dependency",
|
|
884
896
|
`Component dependency "${declared.pluginId}" (declared by "${declaredBy}") is not resolvable from the ` +
|
|
885
|
-
"dependency set; it is propagated into the bundle DEPENDENCIES but its capabilities
|
|
897
|
+
"dependency set; it is propagated into the bundle DEPENDENCIES but neither its capabilities nor its " +
|
|
898
|
+
"runtimeTargets can be verified — so it constrains neither the flow's capability union nor its " +
|
|
899
|
+
"derived runtime targets, and the composed artifact may claim a runtime this component cannot reach.",
|
|
886
900
|
"flow.dependencies",
|
|
887
901
|
);
|
|
888
902
|
}
|
|
@@ -890,6 +904,174 @@ function collectComponentDependencies({ nodePluginIds, dependencies, issues, cap
|
|
|
890
904
|
return [...components.values()].sort((a, b) => a.pluginId.localeCompare(b.pluginId));
|
|
891
905
|
}
|
|
892
906
|
|
|
907
|
+
const BROWSER_RUNTIME_TARGET = "browser";
|
|
908
|
+
const BrowserIncompatibleCapabilitySet = new Set(
|
|
909
|
+
BrowserIncompatibleCapabilityIds,
|
|
910
|
+
);
|
|
911
|
+
|
|
912
|
+
function declaredRuntimeTargets(manifest) {
|
|
913
|
+
const declared = Array.isArray(manifest?.runtimeTargets)
|
|
914
|
+
? manifest.runtimeTargets
|
|
915
|
+
.map((target) => String(target ?? "").trim().toLowerCase())
|
|
916
|
+
.filter(Boolean)
|
|
917
|
+
: [];
|
|
918
|
+
return declared.length > 0 ? declared : null;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* collectFlowRuntimeTargets derives the COMPOSED artifact's runtimeTargets from
|
|
923
|
+
* its constituents instead of asserting a fixed pair.
|
|
924
|
+
*
|
|
925
|
+
* A flow runs only where ALL of its parts run, so the composition's target set
|
|
926
|
+
* is the INTERSECTION of the declared target sets of every node plugin and
|
|
927
|
+
* every resolved component dependency, taken inside the universe a composed
|
|
928
|
+
* artifact can reach at all (browser + WasmEdge). A part that declares no
|
|
929
|
+
* targets is unconstraining — it says nothing, so it subtracts nothing. The
|
|
930
|
+
* admission test is `runtimeTargetSatisfies`, THE SAME ONE every loader uses,
|
|
931
|
+
* so a declaration cannot mean one thing to the compiler and another at the
|
|
932
|
+
* door.
|
|
933
|
+
*
|
|
934
|
+
* The CAPABILITY UNION subtracts too. A declared target set is not the only
|
|
935
|
+
* way a part can be non-browser: a plugin that carries `wallet_sign` and
|
|
936
|
+
* simply omits `runtimeTargets` says nothing about where it runs, yet the
|
|
937
|
+
* composition provably cannot run in a browser, because compliance forbids
|
|
938
|
+
* exactly that capability against a browser target. Deriving only from the
|
|
939
|
+
* declarations left the commonest manifest shape re-creating the original
|
|
940
|
+
* defect — the compiler stamping a target its own compliance pass then
|
|
941
|
+
* rejects. So `browser` is also dropped whenever the flow's capability union
|
|
942
|
+
* meets `BrowserIncompatibleCapabilityIds`, and the diagnostic names the
|
|
943
|
+
* capability that cost it.
|
|
944
|
+
*
|
|
945
|
+
* This is the difference between a truthful manifest and a wish. The hardcoded
|
|
946
|
+
* ["browser","wasmedge"] made every composition claim the browser, which made
|
|
947
|
+
* `capability-runtime-conflict` fire on any flow containing a node with a
|
|
948
|
+
* browser-incompatible capability (wallet_sign, tcp, storage_write, …) — so a
|
|
949
|
+
* WasmEdge-only flow was not merely mislabelled, it was IMPOSSIBLE TO BAKE.
|
|
950
|
+
* Deriving the set keeps the real invariant intact (a browser-incompatible
|
|
951
|
+
* capability and a browser target still cannot coexist) while letting the
|
|
952
|
+
* legitimate WasmEdge-only composition exist.
|
|
953
|
+
*/
|
|
954
|
+
function collectFlowRuntimeTargets({
|
|
955
|
+
resolvedNodes,
|
|
956
|
+
componentDependencies,
|
|
957
|
+
dependencies,
|
|
958
|
+
capabilities,
|
|
959
|
+
issues,
|
|
960
|
+
}) {
|
|
961
|
+
let targets = new Set(COMPOSED_FLOW_RUNTIME_TARGET_UNIVERSE);
|
|
962
|
+
// pluginId -> the declared set, for the diagnostic when nothing survives.
|
|
963
|
+
const constraints = new Map();
|
|
964
|
+
// target -> why it is gone. Losing a runtime is never silent.
|
|
965
|
+
const droppedBecause = new Map();
|
|
966
|
+
|
|
967
|
+
const narrow = (allowed, reasonFor) => {
|
|
968
|
+
for (const target of [...targets]) {
|
|
969
|
+
if (allowed(target)) continue;
|
|
970
|
+
targets.delete(target);
|
|
971
|
+
if (!droppedBecause.has(target)) {
|
|
972
|
+
droppedBecause.set(target, reasonFor(target));
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
const consider = (pluginId, manifest) => {
|
|
978
|
+
if (!pluginId || constraints.has(pluginId)) return;
|
|
979
|
+
const declared = declaredRuntimeTargets(manifest);
|
|
980
|
+
if (!declared) return;
|
|
981
|
+
constraints.set(pluginId, [...declared].sort());
|
|
982
|
+
// ONE RULE, shared with the loaders (`runtimeTargetSatisfies`). Two rules
|
|
983
|
+
// for one field is how a manifest comes to mean different things to the
|
|
984
|
+
// compiler and to the door: a `["wasi","wasmedge"]` part had the browser
|
|
985
|
+
// leg stripped from every flow it joined while the browser harness happily
|
|
986
|
+
// admitted that same declaration. `wasi` is the portability baseline, so
|
|
987
|
+
// it admits both legs unless the part's own capabilities say otherwise;
|
|
988
|
+
// proof-based narrowing is the capability-union pass below.
|
|
989
|
+
narrow(
|
|
990
|
+
(target) =>
|
|
991
|
+
runtimeTargetSatisfies(declared, target, manifest?.capabilities),
|
|
992
|
+
() =>
|
|
993
|
+
`"${pluginId}" declares runtimeTargets [${[...declared].sort().join(", ")}]`,
|
|
994
|
+
);
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
for (const entry of resolvedNodes) {
|
|
998
|
+
if (!entry?.dependency) continue;
|
|
999
|
+
consider(entry.node?.pluginId, entry.dependency.manifest);
|
|
1000
|
+
}
|
|
1001
|
+
for (const component of componentDependencies ?? []) {
|
|
1002
|
+
if (!component?.resolved) continue;
|
|
1003
|
+
consider(
|
|
1004
|
+
component.pluginId,
|
|
1005
|
+
dependencies.get(component.pluginId)?.manifest,
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const browserIncompatible = [...(capabilities ?? [])]
|
|
1010
|
+
.filter((capability) => BrowserIncompatibleCapabilitySet.has(capability))
|
|
1011
|
+
.sort();
|
|
1012
|
+
if (browserIncompatible.length > 0) {
|
|
1013
|
+
narrow(
|
|
1014
|
+
(target) => target !== BROWSER_RUNTIME_TARGET,
|
|
1015
|
+
() =>
|
|
1016
|
+
`the flow's capability union contains ${browserIncompatible
|
|
1017
|
+
.map((capability) => `"${capability}"`)
|
|
1018
|
+
.join(", ")}, which the canonical browser runtime target cannot serve`,
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const ordered = COMPOSED_FLOW_RUNTIME_TARGET_UNIVERSE.filter((target) =>
|
|
1023
|
+
targets.has(target),
|
|
1024
|
+
);
|
|
1025
|
+
const reasons = [...droppedBecause.entries()]
|
|
1026
|
+
.map(([target, why]) => `${target} (${why})`)
|
|
1027
|
+
.join("; ");
|
|
1028
|
+
if (ordered.length === 0) {
|
|
1029
|
+
pushIssue(
|
|
1030
|
+
issues,
|
|
1031
|
+
"error",
|
|
1032
|
+
"empty-runtime-target-intersection",
|
|
1033
|
+
"No runtime runs every part of this flow: every candidate target was ruled " +
|
|
1034
|
+
`out — ${reasons}. A composed flow can only target the runtimes shared by ` +
|
|
1035
|
+
"all of its nodes and resolved component dependencies; split the flow, or " +
|
|
1036
|
+
"widen the constraining plugin's runtimeTargets.",
|
|
1037
|
+
"flow.nodes",
|
|
1038
|
+
);
|
|
1039
|
+
} else if (droppedBecause.size > 0) {
|
|
1040
|
+
pushIssue(
|
|
1041
|
+
issues,
|
|
1042
|
+
"warning",
|
|
1043
|
+
"narrowed-runtime-targets",
|
|
1044
|
+
`This flow composes to runtimeTargets [${ordered.join(", ")}] — narrower than ` +
|
|
1045
|
+
`the isomorphic pair. Dropped: ${reasons}. The artifact is still written to ` +
|
|
1046
|
+
"dist/isomorphic/module.wasm, but it is NOT loadable on the dropped runtime " +
|
|
1047
|
+
"and every harness for that leg will refuse it by name.",
|
|
1048
|
+
"flow.nodes",
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
return ordered;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Rebuild the derived target set from a check record produced by an older SDK
|
|
1056
|
+
* (one with no `runtimeTargets` field). Issues are discarded here on purpose —
|
|
1057
|
+
* the caller re-raises the only one that matters, an empty result, as a throw.
|
|
1058
|
+
*/
|
|
1059
|
+
function recomputeFlowRuntimeTargets(check, dependencies) {
|
|
1060
|
+
return collectFlowRuntimeTargets({
|
|
1061
|
+
resolvedNodes: (check?.nodes ?? [])
|
|
1062
|
+
.filter((node) => node.dispatchModel !== "host")
|
|
1063
|
+
.map((node) => ({
|
|
1064
|
+
node,
|
|
1065
|
+
dependency: dependencies.get(node.pluginId) ?? null,
|
|
1066
|
+
}))
|
|
1067
|
+
.filter((entry) => entry.dependency),
|
|
1068
|
+
componentDependencies: check?.componentDependencies,
|
|
1069
|
+
dependencies,
|
|
1070
|
+
capabilities: check?.capabilities ?? [],
|
|
1071
|
+
issues: [],
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
|
|
893
1075
|
/**
|
|
894
1076
|
* findFlowCycles returns every elementary cycle reachable in the node graph
|
|
895
1077
|
* as { nodeIds, edges } (iterative DFS with back-edge extraction; each cycle
|
|
@@ -1773,6 +1955,14 @@ export function checkFlowProgram({ flow, dependencies = new Map() } = {}) {
|
|
|
1773
1955
|
capabilities,
|
|
1774
1956
|
});
|
|
1775
1957
|
|
|
1958
|
+
const runtimeTargets = collectFlowRuntimeTargets({
|
|
1959
|
+
resolvedNodes,
|
|
1960
|
+
componentDependencies,
|
|
1961
|
+
dependencies,
|
|
1962
|
+
capabilities,
|
|
1963
|
+
issues,
|
|
1964
|
+
});
|
|
1965
|
+
|
|
1776
1966
|
const errors = issues.filter((issue) => issue.severity === "error");
|
|
1777
1967
|
return {
|
|
1778
1968
|
ok: errors.length === 0,
|
|
@@ -1780,6 +1970,7 @@ export function checkFlowProgram({ flow, dependencies = new Map() } = {}) {
|
|
|
1780
1970
|
errors,
|
|
1781
1971
|
warnings: issues.filter((issue) => issue.severity === "warning"),
|
|
1782
1972
|
capabilities: [...capabilities].sort(),
|
|
1973
|
+
runtimeTargets,
|
|
1783
1974
|
engineLinkage,
|
|
1784
1975
|
threadModel,
|
|
1785
1976
|
componentDependencies,
|
|
@@ -1908,6 +2099,23 @@ export function buildFlowModuleManifest({ flow, check, dependencies }) {
|
|
|
1908
2099
|
const checkedNodeById = new Map(
|
|
1909
2100
|
check.nodes.map((node) => [node.nodeId, node]),
|
|
1910
2101
|
);
|
|
2102
|
+
// `check.runtimeTargets` is the derived intersection. It is absent only when
|
|
2103
|
+
// a caller hands in a check record built by an older SDK; recompute rather
|
|
2104
|
+
// than silently re-asserting the historical pair — and THROW on the empty
|
|
2105
|
+
// intersection instead of swallowing it. A check record has an issue sink;
|
|
2106
|
+
// this path does not, and an artifact that declares no runtime at all is
|
|
2107
|
+
// read as unconstrained by every loader downstream, which is the most
|
|
2108
|
+
// permissive possible answer to the least certain question.
|
|
2109
|
+
const runtimeTargets = Array.isArray(check.runtimeTargets)
|
|
2110
|
+
? [...check.runtimeTargets]
|
|
2111
|
+
: recomputeFlowRuntimeTargets(check, dependencies);
|
|
2112
|
+
if (runtimeTargets.length === 0) {
|
|
2113
|
+
throw new Error(
|
|
2114
|
+
`Flow "${flow?.programId ?? "(unknown)"}" has no runtime target: no runtime runs ` +
|
|
2115
|
+
"every one of its parts. Refusing to build a manifest that declares nothing — " +
|
|
2116
|
+
"run checkFlowProgram and read the empty-runtime-target-intersection error.",
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
1911
2119
|
|
|
1912
2120
|
const hostModelNodeIds = new Set(
|
|
1913
2121
|
check.nodes.filter((node) => node.dispatchModel === "host").map((node) => node.nodeId),
|
|
@@ -2032,13 +2240,17 @@ export function buildFlowModuleManifest({ flow, check, dependencies }) {
|
|
|
2032
2240
|
externalInterfaces,
|
|
2033
2241
|
methods,
|
|
2034
2242
|
schemasUsed,
|
|
2035
|
-
|
|
2243
|
+
// DERIVED, never asserted: the runtimes shared by every node plugin and
|
|
2244
|
+
// resolved component dependency (see collectFlowRuntimeTargets). A flow of
|
|
2245
|
+
// fully isomorphic nodes keeps both targets; a flow containing one
|
|
2246
|
+
// WasmEdge-only node is a WasmEdge-only flow and says so.
|
|
2247
|
+
runtimeTargets,
|
|
2036
2248
|
buildArtifacts: [
|
|
2037
2249
|
{
|
|
2038
2250
|
artifactId: `${cIdent(flow.programId)}-flow-runtime`,
|
|
2039
2251
|
kind: "wasm",
|
|
2040
2252
|
path: "dist/isomorphic/module.wasm",
|
|
2041
|
-
target: "
|
|
2253
|
+
target: runtimeTargets.join(","),
|
|
2042
2254
|
},
|
|
2043
2255
|
],
|
|
2044
2256
|
flowNodes: nodes.map((node) => {
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { createBrowserWasiShim } from "../host/wasiShim.js";
|
|
18
|
+
import {
|
|
19
|
+
assertArtifactRuntimeTarget,
|
|
20
|
+
detectBrowserLeg,
|
|
21
|
+
} from "../host/runtimeTargetGate.js";
|
|
18
22
|
import {
|
|
19
23
|
isPayloadSchemaHashValid,
|
|
20
24
|
normalizePayloadSchemaHash,
|
|
@@ -211,6 +215,28 @@ export async function createFlowRuntimeHost(options = {}) {
|
|
|
211
215
|
wasmModule = await WebAssembly.compile(bytes);
|
|
212
216
|
}
|
|
213
217
|
|
|
218
|
+
// THE COMPOSED FLOW'S DOOR. A composed artifact now derives its
|
|
219
|
+
// runtimeTargets from its parts, so a WasmEdge-only flow is a real thing —
|
|
220
|
+
// and this host is how one gets loaded. It is genuinely runtime-agnostic
|
|
221
|
+
// (the same function backs the Node/WasmEdge leg), so the leg is stated by
|
|
222
|
+
// the caller; absent that, a real browser is detected and gated. Passing
|
|
223
|
+
// runtimeTarget: null opts out explicitly, which is not the same as
|
|
224
|
+
// forgetting.
|
|
225
|
+
const declaredLeg =
|
|
226
|
+
options.runtimeTarget === undefined
|
|
227
|
+
? detectBrowserLeg()
|
|
228
|
+
? "browser"
|
|
229
|
+
: null
|
|
230
|
+
: options.runtimeTarget;
|
|
231
|
+
if (declaredLeg) {
|
|
232
|
+
assertArtifactRuntimeTarget({
|
|
233
|
+
wasmModule,
|
|
234
|
+
manifest: options.manifest,
|
|
235
|
+
leg: String(declaredLeg).trim().toLowerCase(),
|
|
236
|
+
what: "composed flow artifact",
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
214
240
|
const wasi = createBrowserWasiShim({
|
|
215
241
|
args: options.args ?? ["flow-runtime"],
|
|
216
242
|
env: options.env ?? {},
|
|
@@ -220,6 +220,14 @@ export async function createIsomorphicFlowRuntimeHost(options = {}) {
|
|
|
220
220
|
);
|
|
221
221
|
const parent = await createFlowRuntimeHost({
|
|
222
222
|
wasmSource: parentArtifactBytes,
|
|
223
|
+
// UNCONDITIONAL, with no opt-out. This host mounts every isomorphic child
|
|
224
|
+
// through the BROWSER module harness, whose gate cannot be turned off — so
|
|
225
|
+
// an escape hatch here would only ever produce an ungated parent with
|
|
226
|
+
// gated children, and a half-gate is not a gate. There is no legitimate
|
|
227
|
+
// non-browser use of this host precisely because of that mount; callers
|
|
228
|
+
// that want the runtime-agnostic host call createFlowRuntimeHost, which
|
|
229
|
+
// does take a stated leg.
|
|
230
|
+
runtimeTarget: "browser",
|
|
223
231
|
args: options.args,
|
|
224
232
|
env: options.env,
|
|
225
233
|
logOutput: options.logOutput,
|
|
@@ -49,6 +49,10 @@ import {
|
|
|
49
49
|
resolveModuleSignaturePolicy,
|
|
50
50
|
verifyModuleArtifact,
|
|
51
51
|
} from "../bundle/signing.js";
|
|
52
|
+
import {
|
|
53
|
+
assertArtifactRuntimeTarget,
|
|
54
|
+
RuntimeTargetError,
|
|
55
|
+
} from "./runtimeTargetGate.js";
|
|
52
56
|
// Artifact byte reduction is an ARTIFACT concern shared by all three runtimes,
|
|
53
57
|
// so it lives on `space-data-module-sdk/bundle`. Re-exported here because a
|
|
54
58
|
// caller loading a module in the browser needs it in the same breath.
|
|
@@ -259,6 +263,27 @@ function createImportedMemory(options = {}) {
|
|
|
259
263
|
return new WebAssembly.Memory(descriptor);
|
|
260
264
|
}
|
|
261
265
|
|
|
266
|
+
const BROWSER_RUNTIME_TARGET = "browser";
|
|
267
|
+
|
|
268
|
+
export { RuntimeTargetError };
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* TRI-RUNTIME ISOMORPHISM, ENFORCED AT THE DOOR.
|
|
272
|
+
*
|
|
273
|
+
* This harness IS the browser leg of the tri-runtime contract, wherever the
|
|
274
|
+
* JavaScript happens to be running — asking it to load an artifact that does
|
|
275
|
+
* not declare the browser is asking for a divergence. The shared gate in
|
|
276
|
+
* `runtimeTargetGate.js` does the work; both declarations are consulted and
|
|
277
|
+
* the artifact's own embedded record wins on conflict.
|
|
278
|
+
*/
|
|
279
|
+
export function assertBrowserRuntimeTarget(wasmModule, manifest) {
|
|
280
|
+
assertArtifactRuntimeTarget({
|
|
281
|
+
wasmModule,
|
|
282
|
+
manifest,
|
|
283
|
+
leg: BROWSER_RUNTIME_TARGET,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
262
287
|
function resolveManifestSurface(manifest) {
|
|
263
288
|
const surfaces = Array.isArray(manifest?.invokeSurfaces)
|
|
264
289
|
? manifest.invokeSurfaces
|
|
@@ -471,6 +496,7 @@ export async function createBrowserModuleHarness(options = {}) {
|
|
|
471
496
|
ownedArtifactBytes = null;
|
|
472
497
|
wasmSource = null;
|
|
473
498
|
options.wasmSource = null;
|
|
499
|
+
assertBrowserRuntimeTarget(wasmModule, options.manifest);
|
|
474
500
|
const moduleImports = WebAssembly.Module.imports(wasmModule);
|
|
475
501
|
const needsHostBridge = moduleImports.some(
|
|
476
502
|
(entry) => entry.module === DEFAULT_HOSTCALL_IMPORT_MODULE,
|