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.
- package/README.md +92 -0
- package/bin/space-data-module.js +85 -1
- package/docs/module-publication-standard.md +7 -3
- package/docs/propagator-abi.md +477 -0
- package/include/orbpro/orbpro_propagator_abi.h +312 -0
- package/package.json +7 -1
- package/schemas/PluginManifest.fbs +46 -1
- package/schemas/orbpro/Propagator.fbs +161 -3
- package/src/browser.js +11 -0
- package/src/bundle/index.js +1 -0
- package/src/bundle/sigdomain.js +22 -0
- package/src/capabilities.js +91 -0
- package/src/compliance/index.js +8 -0
- package/src/compliance/pluginCompliance.js +76 -32
- package/src/flow/flowCompiler.js +231 -3
- package/src/flow/flowRuntimeHost.js +26 -0
- package/src/flow/isomorphicFlowHost.js +8 -0
- package/src/generated/orbpro/manifest/plugin-family.d.ts +9 -1
- package/src/generated/orbpro/manifest/plugin-family.js +8 -0
- package/src/generated/orbpro/manifest/plugin-family.ts +8 -0
- package/src/generated/orbpro/propagator-abi.js +118 -0
- package/src/generated/orbpro/propagator-abi.ts +199 -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/manifest/normalize.js +113 -4
- package/src/scaffold/copyTemplate.js +71 -0
- package/src/scaffold/index.js +150 -0
- package/src/scaffold/tokens.js +90 -0
- package/src/testing/parityBrowserRunner.js +11 -0
- package/src/testing/parityGate.js +287 -27
- package/templates/propagator-module/README.md +99 -0
- package/templates/propagator-module/build.js +103 -0
- package/templates/propagator-module/package.json +19 -0
- package/templates/propagator-module/plugin-manifest.json +66 -0
- package/templates/propagator-module/src/__MODULE_NAME_SNAKE__.cpp +450 -0
- package/templates/propagator-module/tests/module.build.test.mjs +103 -0
package/src/capabilities.js
CHANGED
|
@@ -47,3 +47,94 @@ 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
|
+
]);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* ENGINE-HOSTED capabilities: the three that reach into a live 3D engine —
|
|
84
|
+
* its scene graph, its entity collection, its render loop.
|
|
85
|
+
*
|
|
86
|
+
* These used to sit in `BrowserIncompatibleCapabilityIds`, which was exactly
|
|
87
|
+
* backwards and produced a compliance paradox: they are also in
|
|
88
|
+
* `RecommendedCapabilityIds`, so a module was simultaneously told to declare
|
|
89
|
+
* them and refused for declaring them alongside the only runtime where they
|
|
90
|
+
* can possibly work. The flow compiler compounded it by unconditionally
|
|
91
|
+
* subtracting `browser` from any composed flow whose capability union touched
|
|
92
|
+
* one — meaning no flow that needs the engine could ever target the runtime
|
|
93
|
+
* the engine lives in.
|
|
94
|
+
*
|
|
95
|
+
* The truth, established by sweep:
|
|
96
|
+
* - The ONE implementation of `scene_access` in the stack is OrbPro's
|
|
97
|
+
* `ProviderAccessPort` + the SDK's `providerAccessEngineAdapter`, both of
|
|
98
|
+
* which require a live Cesium/OrbPro `Scene`. That is a BROWSER object.
|
|
99
|
+
* - `NodeHostSupportedCapabilities` contains none of the three, and
|
|
100
|
+
* `assertCapability()` fails them closed with `host-capability-unsupported`.
|
|
101
|
+
* - `entity_access` and `render_hooks` have no serving implementation on any
|
|
102
|
+
* runtime yet — only manifest-vocabulary codec entries. They are declared
|
|
103
|
+
* engine-hosted here because that is where they can ever be served, not
|
|
104
|
+
* because they are wired today.
|
|
105
|
+
*
|
|
106
|
+
* So they are wasmedge-incompatible, not browser-incompatible. (The `wasi`
|
|
107
|
+
* portability baseline already refuses them via
|
|
108
|
+
* `StandaloneWasiCapabilityIds` — nothing outside that five-member subset is
|
|
109
|
+
* admitted to a standalone WASI artifact.)
|
|
110
|
+
*
|
|
111
|
+
* Ruling: graph/findings/official-harness-shapes.md §8.4
|
|
112
|
+
* Task: graph/tasks/harness-w0-immediate-fixes.md (W0.4)
|
|
113
|
+
*/
|
|
114
|
+
export const EngineHostedCapabilityIds = Object.freeze([
|
|
115
|
+
"scene_access",
|
|
116
|
+
"entity_access",
|
|
117
|
+
"render_hooks",
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Capabilities the headless WasmEdge leg cannot serve. There is no scene, no
|
|
122
|
+
* entity collection and no render loop in a headless server runtime, and no
|
|
123
|
+
* adapter can invent one.
|
|
124
|
+
*/
|
|
125
|
+
export const WasmEdgeIncompatibleCapabilityIds = Object.freeze([
|
|
126
|
+
...EngineHostedCapabilityIds,
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Which capabilities each runtime leg cannot serve.
|
|
131
|
+
*
|
|
132
|
+
* Consumed by `runtimeTargetGate.js` (load-time admission), the flow compiler's
|
|
133
|
+
* `collectFlowRuntimeTargets` (derived runtimeTargets), and
|
|
134
|
+
* `pluginCompliance.js` (publish-time declaration policy) — all three read
|
|
135
|
+
* THIS table, so a leg's policy is stated once.
|
|
136
|
+
*/
|
|
137
|
+
export const LegIncompatibleCapabilityIds = Object.freeze({
|
|
138
|
+
browser: BrowserIncompatibleCapabilityIds,
|
|
139
|
+
wasmedge: WasmEdgeIncompatibleCapabilityIds,
|
|
140
|
+
});
|
package/src/compliance/index.js
CHANGED
|
@@ -11,8 +11,12 @@ import {
|
|
|
11
11
|
validatePluginManifest,
|
|
12
12
|
} from "./pluginCompliance.js";
|
|
13
13
|
import {
|
|
14
|
+
BrowserIncompatibleCapabilityIds,
|
|
15
|
+
EngineHostedCapabilityIds,
|
|
16
|
+
LegIncompatibleCapabilityIds,
|
|
14
17
|
RecommendedCapabilityIds,
|
|
15
18
|
StandaloneWasiCapabilityIds,
|
|
19
|
+
WasmEdgeIncompatibleCapabilityIds,
|
|
16
20
|
} from "../capabilities.js";
|
|
17
21
|
import { validateManifestAgainstStandardsCatalog } from "../standards/index.js";
|
|
18
22
|
|
|
@@ -48,6 +52,10 @@ export async function validateArtifactWithStandards(options = {}) {
|
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
export {
|
|
55
|
+
BrowserIncompatibleCapabilityIds,
|
|
56
|
+
EngineHostedCapabilityIds,
|
|
57
|
+
LegIncompatibleCapabilityIds,
|
|
58
|
+
WasmEdgeIncompatibleCapabilityIds,
|
|
51
59
|
findManifestFiles,
|
|
52
60
|
getWasmExportNames,
|
|
53
61
|
getWasmExportNamesFromFile,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
RuntimeTarget,
|
|
14
14
|
} from "../runtime/constants.js";
|
|
15
15
|
import {
|
|
16
|
+
LegIncompatibleCapabilityIds,
|
|
16
17
|
RecommendedCapabilityIds,
|
|
17
18
|
StandaloneWasiCapabilityIds,
|
|
18
19
|
} from "../capabilities.js";
|
|
@@ -25,6 +26,10 @@ import {
|
|
|
25
26
|
isPayloadSchemaHashValid,
|
|
26
27
|
payloadSchemaIdentitiesEqual,
|
|
27
28
|
} from "../manifest/typeRefs.js";
|
|
29
|
+
import {
|
|
30
|
+
PluginFamilyNames,
|
|
31
|
+
pluginFamilyByName,
|
|
32
|
+
} from "../manifest/normalize.js";
|
|
28
33
|
import { isLegacyWildcardPort } from "./legacyWildcardPorts.js";
|
|
29
34
|
import { SDS_MANIFEST_SECTION_NAME } from "../bundle/constants.js";
|
|
30
35
|
import {
|
|
@@ -49,24 +54,18 @@ const ExternalInterfaceDirectionSet = new Set(
|
|
|
49
54
|
const ExternalInterfaceKindSet = new Set(Object.values(ExternalInterfaceKind));
|
|
50
55
|
const ProtocolRoleSet = new Set(Object.values(ProtocolRole));
|
|
51
56
|
const ProtocolTransportKindSet = new Set(Object.values(ProtocolTransportKind));
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"wallet_sign",
|
|
65
|
-
"ipfs",
|
|
66
|
-
"scene_access",
|
|
67
|
-
"entity_access",
|
|
68
|
-
"render_hooks",
|
|
69
|
-
]);
|
|
57
|
+
// One entry per runtime leg that has a declaration policy, built from the
|
|
58
|
+
// single table in capabilities.js. This used to be a browser-only constant,
|
|
59
|
+
// which is why an engine-hosted capability could only ever be described as
|
|
60
|
+
// "not available in the browser" — the exact inverse of the truth.
|
|
61
|
+
const LegIncompatibleCapabilitySets = Object.freeze(
|
|
62
|
+
Object.fromEntries(
|
|
63
|
+
Object.entries(LegIncompatibleCapabilityIds).map(([leg, ids]) => [
|
|
64
|
+
leg,
|
|
65
|
+
new Set(ids),
|
|
66
|
+
]),
|
|
67
|
+
),
|
|
68
|
+
);
|
|
70
69
|
const StandaloneWasiProtocolTransportKindSet = new Set([
|
|
71
70
|
ProtocolTransportKind.WASI_PIPE,
|
|
72
71
|
]);
|
|
@@ -142,6 +141,45 @@ function validateStringField(issues, value, location, label) {
|
|
|
142
141
|
return true;
|
|
143
142
|
}
|
|
144
143
|
|
|
144
|
+
/**
|
|
145
|
+
* `pluginFamily` is an ENUM, not a free string.
|
|
146
|
+
*
|
|
147
|
+
* This used to be `validateStringField`, so any spelling passed compliance and
|
|
148
|
+
* `normalizePluginFamily` then silently coerced it to ANALYSIS — the two
|
|
149
|
+
* halves of the same defect. Compliance now enforces the same vocabulary the
|
|
150
|
+
* normalizer refuses on, so a bad family is caught at check time with the
|
|
151
|
+
* valid set named, instead of at load time as a mislabelled module.
|
|
152
|
+
*
|
|
153
|
+
* Ruling: graph/findings/official-harness-shapes.md §4.7 / §8.3
|
|
154
|
+
*/
|
|
155
|
+
function validatePluginFamilyField(issues, value, location) {
|
|
156
|
+
if (!isNonEmptyString(value)) {
|
|
157
|
+
pushIssue(
|
|
158
|
+
issues,
|
|
159
|
+
"error",
|
|
160
|
+
"missing-string",
|
|
161
|
+
"pluginFamily must be a non-empty string.",
|
|
162
|
+
location,
|
|
163
|
+
);
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
const normalized = value.trim().toLowerCase();
|
|
167
|
+
if (pluginFamilyByName[normalized] === undefined) {
|
|
168
|
+
pushIssue(
|
|
169
|
+
issues,
|
|
170
|
+
"error",
|
|
171
|
+
"unknown-plugin-family",
|
|
172
|
+
`pluginFamily "${value}" is not a member of the plugin family vocabulary. ` +
|
|
173
|
+
`Valid families: ${PluginFamilyNames.join(", ")}. ` +
|
|
174
|
+
`A new family is appended to schemas/PluginManifest.fbs (a projection ` +
|
|
175
|
+
`of the authoritative SDS pluginCategory), never invented in a manifest.`,
|
|
176
|
+
location,
|
|
177
|
+
);
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
145
183
|
function validateCapabilityEntry(capability, issues, location) {
|
|
146
184
|
if (isNonEmptyString(capability)) {
|
|
147
185
|
return capability;
|
|
@@ -1428,21 +1466,27 @@ function validateRuntimeTargets(runtimeTargets, declaredCapabilities, issues, so
|
|
|
1428
1466
|
);
|
|
1429
1467
|
}
|
|
1430
1468
|
}
|
|
1431
|
-
if (
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
for (const
|
|
1436
|
-
|
|
1469
|
+
if (Array.isArray(declaredCapabilities)) {
|
|
1470
|
+
// Check EVERY declared leg that has a policy, not just the browser. An
|
|
1471
|
+
// engine-hosted capability (scene_access/entity_access/render_hooks) is
|
|
1472
|
+
// browser-ONLY, so the conflict it can raise is against `wasmedge`.
|
|
1473
|
+
for (const target of seenTargets) {
|
|
1474
|
+
const incompatible = LegIncompatibleCapabilitySets[target];
|
|
1475
|
+
if (!incompatible) {
|
|
1437
1476
|
continue;
|
|
1438
1477
|
}
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1478
|
+
for (const capability of declaredCapabilities) {
|
|
1479
|
+
if (!incompatible.has(capability)) {
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
pushIssue(
|
|
1483
|
+
issues,
|
|
1484
|
+
"error",
|
|
1485
|
+
"capability-runtime-conflict",
|
|
1486
|
+
`Capability "${capability}" is not available in the canonical ${target} runtime target.`,
|
|
1487
|
+
`${sourceName}.capabilities`,
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1446
1490
|
}
|
|
1447
1491
|
}
|
|
1448
1492
|
}
|
|
@@ -1750,7 +1794,7 @@ export function validatePluginManifest(manifest, options = {}) {
|
|
|
1750
1794
|
validateStringField(issues, manifest.pluginId, `${sourceName}.pluginId`, "pluginId");
|
|
1751
1795
|
validateStringField(issues, manifest.name, `${sourceName}.name`, "name");
|
|
1752
1796
|
validateStringField(issues, manifest.version, `${sourceName}.version`, "version");
|
|
1753
|
-
|
|
1797
|
+
validatePluginFamilyField(issues, manifest.pluginFamily, `${sourceName}.pluginFamily`);
|
|
1754
1798
|
|
|
1755
1799
|
const rawDeclaredCapabilities = manifest.capabilities;
|
|
1756
1800
|
let declaredCapabilities = null;
|
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
|
+
LegIncompatibleCapabilityIds,
|
|
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,190 @@ function collectComponentDependencies({ nodePluginIds, dependencies, issues, cap
|
|
|
890
904
|
return [...components.values()].sort((a, b) => a.pluginId.localeCompare(b.pluginId));
|
|
891
905
|
}
|
|
892
906
|
|
|
907
|
+
// Per-leg incompatibility, read from the ONE table in capabilities.js rather
|
|
908
|
+
// than a browser-only constant. An engine-hosted capability
|
|
909
|
+
// (scene_access/entity_access/render_hooks) is browser-ONLY, so the leg it
|
|
910
|
+
// narrows away is `wasmedge`; the old browser-only form could only ever
|
|
911
|
+
// subtract `browser`, which is how a flow that needs the engine ended up
|
|
912
|
+
// unable to target the runtime the engine lives in.
|
|
913
|
+
const LegIncompatibleCapabilitySets = Object.freeze(
|
|
914
|
+
Object.fromEntries(
|
|
915
|
+
Object.entries(LegIncompatibleCapabilityIds).map(([leg, ids]) => [
|
|
916
|
+
leg,
|
|
917
|
+
new Set(ids),
|
|
918
|
+
]),
|
|
919
|
+
),
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
function declaredRuntimeTargets(manifest) {
|
|
923
|
+
const declared = Array.isArray(manifest?.runtimeTargets)
|
|
924
|
+
? manifest.runtimeTargets
|
|
925
|
+
.map((target) => String(target ?? "").trim().toLowerCase())
|
|
926
|
+
.filter(Boolean)
|
|
927
|
+
: [];
|
|
928
|
+
return declared.length > 0 ? declared : null;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* collectFlowRuntimeTargets derives the COMPOSED artifact's runtimeTargets from
|
|
933
|
+
* its constituents instead of asserting a fixed pair.
|
|
934
|
+
*
|
|
935
|
+
* A flow runs only where ALL of its parts run, so the composition's target set
|
|
936
|
+
* is the INTERSECTION of the declared target sets of every node plugin and
|
|
937
|
+
* every resolved component dependency, taken inside the universe a composed
|
|
938
|
+
* artifact can reach at all (browser + WasmEdge). A part that declares no
|
|
939
|
+
* targets is unconstraining — it says nothing, so it subtracts nothing. The
|
|
940
|
+
* admission test is `runtimeTargetSatisfies`, THE SAME ONE every loader uses,
|
|
941
|
+
* so a declaration cannot mean one thing to the compiler and another at the
|
|
942
|
+
* door.
|
|
943
|
+
*
|
|
944
|
+
* The CAPABILITY UNION subtracts too. A declared target set is not the only
|
|
945
|
+
* way a part can be non-browser: a plugin that carries `wallet_sign` and
|
|
946
|
+
* simply omits `runtimeTargets` says nothing about where it runs, yet the
|
|
947
|
+
* composition provably cannot run in a browser, because compliance forbids
|
|
948
|
+
* exactly that capability against a browser target. Deriving only from the
|
|
949
|
+
* declarations left the commonest manifest shape re-creating the original
|
|
950
|
+
* defect — the compiler stamping a target its own compliance pass then
|
|
951
|
+
* rejects. So a leg is also dropped whenever the flow's capability union meets
|
|
952
|
+
* that leg's entry in `LegIncompatibleCapabilityIds`, and the diagnostic names
|
|
953
|
+
* the capability that cost it. That table is per-leg, not browser-only: the
|
|
954
|
+
* engine-hosted capabilities (scene_access/entity_access/render_hooks) narrow
|
|
955
|
+
* away `wasmedge`, in the other direction.
|
|
956
|
+
*
|
|
957
|
+
* This is the difference between a truthful manifest and a wish. The hardcoded
|
|
958
|
+
* ["browser","wasmedge"] made every composition claim the browser, which made
|
|
959
|
+
* `capability-runtime-conflict` fire on any flow containing a node with a
|
|
960
|
+
* browser-incompatible capability (wallet_sign, tcp, storage_write, …) — so a
|
|
961
|
+
* WasmEdge-only flow was not merely mislabelled, it was IMPOSSIBLE TO BAKE.
|
|
962
|
+
* Deriving the set keeps the real invariant intact (a browser-incompatible
|
|
963
|
+
* capability and a browser target still cannot coexist) while letting the
|
|
964
|
+
* legitimate WasmEdge-only composition exist.
|
|
965
|
+
*/
|
|
966
|
+
function collectFlowRuntimeTargets({
|
|
967
|
+
resolvedNodes,
|
|
968
|
+
componentDependencies,
|
|
969
|
+
dependencies,
|
|
970
|
+
capabilities,
|
|
971
|
+
issues,
|
|
972
|
+
}) {
|
|
973
|
+
let targets = new Set(COMPOSED_FLOW_RUNTIME_TARGET_UNIVERSE);
|
|
974
|
+
// pluginId -> the declared set, for the diagnostic when nothing survives.
|
|
975
|
+
const constraints = new Map();
|
|
976
|
+
// target -> why it is gone. Losing a runtime is never silent.
|
|
977
|
+
const droppedBecause = new Map();
|
|
978
|
+
|
|
979
|
+
const narrow = (allowed, reasonFor) => {
|
|
980
|
+
for (const target of [...targets]) {
|
|
981
|
+
if (allowed(target)) continue;
|
|
982
|
+
targets.delete(target);
|
|
983
|
+
if (!droppedBecause.has(target)) {
|
|
984
|
+
droppedBecause.set(target, reasonFor(target));
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
const consider = (pluginId, manifest) => {
|
|
990
|
+
if (!pluginId || constraints.has(pluginId)) return;
|
|
991
|
+
const declared = declaredRuntimeTargets(manifest);
|
|
992
|
+
if (!declared) return;
|
|
993
|
+
constraints.set(pluginId, [...declared].sort());
|
|
994
|
+
// ONE RULE, shared with the loaders (`runtimeTargetSatisfies`). Two rules
|
|
995
|
+
// for one field is how a manifest comes to mean different things to the
|
|
996
|
+
// compiler and to the door: a `["wasi","wasmedge"]` part had the browser
|
|
997
|
+
// leg stripped from every flow it joined while the browser harness happily
|
|
998
|
+
// admitted that same declaration. `wasi` is the portability baseline, so
|
|
999
|
+
// it admits both legs unless the part's own capabilities say otherwise;
|
|
1000
|
+
// proof-based narrowing is the capability-union pass below.
|
|
1001
|
+
narrow(
|
|
1002
|
+
(target) =>
|
|
1003
|
+
runtimeTargetSatisfies(declared, target, manifest?.capabilities),
|
|
1004
|
+
() =>
|
|
1005
|
+
`"${pluginId}" declares runtimeTargets [${[...declared].sort().join(", ")}]`,
|
|
1006
|
+
);
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
for (const entry of resolvedNodes) {
|
|
1010
|
+
if (!entry?.dependency) continue;
|
|
1011
|
+
consider(entry.node?.pluginId, entry.dependency.manifest);
|
|
1012
|
+
}
|
|
1013
|
+
for (const component of componentDependencies ?? []) {
|
|
1014
|
+
if (!component?.resolved) continue;
|
|
1015
|
+
consider(
|
|
1016
|
+
component.pluginId,
|
|
1017
|
+
dependencies.get(component.pluginId)?.manifest,
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const unionCapabilities = [...(capabilities ?? [])];
|
|
1022
|
+
for (const leg of COMPOSED_FLOW_RUNTIME_TARGET_UNIVERSE) {
|
|
1023
|
+
const incompatible = LegIncompatibleCapabilitySets[leg];
|
|
1024
|
+
if (!incompatible) continue;
|
|
1025
|
+
const offending = unionCapabilities
|
|
1026
|
+
.filter((capability) => incompatible.has(capability))
|
|
1027
|
+
.sort();
|
|
1028
|
+
if (offending.length === 0) continue;
|
|
1029
|
+
narrow(
|
|
1030
|
+
(target) => target !== leg,
|
|
1031
|
+
() =>
|
|
1032
|
+
`the flow's capability union contains ${offending
|
|
1033
|
+
.map((capability) => `"${capability}"`)
|
|
1034
|
+
.join(", ")}, which the canonical ${leg} runtime target cannot serve`,
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const ordered = COMPOSED_FLOW_RUNTIME_TARGET_UNIVERSE.filter((target) =>
|
|
1039
|
+
targets.has(target),
|
|
1040
|
+
);
|
|
1041
|
+
const reasons = [...droppedBecause.entries()]
|
|
1042
|
+
.map(([target, why]) => `${target} (${why})`)
|
|
1043
|
+
.join("; ");
|
|
1044
|
+
if (ordered.length === 0) {
|
|
1045
|
+
pushIssue(
|
|
1046
|
+
issues,
|
|
1047
|
+
"error",
|
|
1048
|
+
"empty-runtime-target-intersection",
|
|
1049
|
+
"No runtime runs every part of this flow: every candidate target was ruled " +
|
|
1050
|
+
`out — ${reasons}. A composed flow can only target the runtimes shared by ` +
|
|
1051
|
+
"all of its nodes and resolved component dependencies; split the flow, or " +
|
|
1052
|
+
"widen the constraining plugin's runtimeTargets.",
|
|
1053
|
+
"flow.nodes",
|
|
1054
|
+
);
|
|
1055
|
+
} else if (droppedBecause.size > 0) {
|
|
1056
|
+
pushIssue(
|
|
1057
|
+
issues,
|
|
1058
|
+
"warning",
|
|
1059
|
+
"narrowed-runtime-targets",
|
|
1060
|
+
`This flow composes to runtimeTargets [${ordered.join(", ")}] — narrower than ` +
|
|
1061
|
+
`the isomorphic pair. Dropped: ${reasons}. The artifact is still written to ` +
|
|
1062
|
+
"dist/isomorphic/module.wasm, but it is NOT loadable on the dropped runtime " +
|
|
1063
|
+
"and every harness for that leg will refuse it by name.",
|
|
1064
|
+
"flow.nodes",
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
return ordered;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Rebuild the derived target set from a check record produced by an older SDK
|
|
1072
|
+
* (one with no `runtimeTargets` field). Issues are discarded here on purpose —
|
|
1073
|
+
* the caller re-raises the only one that matters, an empty result, as a throw.
|
|
1074
|
+
*/
|
|
1075
|
+
function recomputeFlowRuntimeTargets(check, dependencies) {
|
|
1076
|
+
return collectFlowRuntimeTargets({
|
|
1077
|
+
resolvedNodes: (check?.nodes ?? [])
|
|
1078
|
+
.filter((node) => node.dispatchModel !== "host")
|
|
1079
|
+
.map((node) => ({
|
|
1080
|
+
node,
|
|
1081
|
+
dependency: dependencies.get(node.pluginId) ?? null,
|
|
1082
|
+
}))
|
|
1083
|
+
.filter((entry) => entry.dependency),
|
|
1084
|
+
componentDependencies: check?.componentDependencies,
|
|
1085
|
+
dependencies,
|
|
1086
|
+
capabilities: check?.capabilities ?? [],
|
|
1087
|
+
issues: [],
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
|
|
893
1091
|
/**
|
|
894
1092
|
* findFlowCycles returns every elementary cycle reachable in the node graph
|
|
895
1093
|
* as { nodeIds, edges } (iterative DFS with back-edge extraction; each cycle
|
|
@@ -1773,6 +1971,14 @@ export function checkFlowProgram({ flow, dependencies = new Map() } = {}) {
|
|
|
1773
1971
|
capabilities,
|
|
1774
1972
|
});
|
|
1775
1973
|
|
|
1974
|
+
const runtimeTargets = collectFlowRuntimeTargets({
|
|
1975
|
+
resolvedNodes,
|
|
1976
|
+
componentDependencies,
|
|
1977
|
+
dependencies,
|
|
1978
|
+
capabilities,
|
|
1979
|
+
issues,
|
|
1980
|
+
});
|
|
1981
|
+
|
|
1776
1982
|
const errors = issues.filter((issue) => issue.severity === "error");
|
|
1777
1983
|
return {
|
|
1778
1984
|
ok: errors.length === 0,
|
|
@@ -1780,6 +1986,7 @@ export function checkFlowProgram({ flow, dependencies = new Map() } = {}) {
|
|
|
1780
1986
|
errors,
|
|
1781
1987
|
warnings: issues.filter((issue) => issue.severity === "warning"),
|
|
1782
1988
|
capabilities: [...capabilities].sort(),
|
|
1989
|
+
runtimeTargets,
|
|
1783
1990
|
engineLinkage,
|
|
1784
1991
|
threadModel,
|
|
1785
1992
|
componentDependencies,
|
|
@@ -1908,6 +2115,23 @@ export function buildFlowModuleManifest({ flow, check, dependencies }) {
|
|
|
1908
2115
|
const checkedNodeById = new Map(
|
|
1909
2116
|
check.nodes.map((node) => [node.nodeId, node]),
|
|
1910
2117
|
);
|
|
2118
|
+
// `check.runtimeTargets` is the derived intersection. It is absent only when
|
|
2119
|
+
// a caller hands in a check record built by an older SDK; recompute rather
|
|
2120
|
+
// than silently re-asserting the historical pair — and THROW on the empty
|
|
2121
|
+
// intersection instead of swallowing it. A check record has an issue sink;
|
|
2122
|
+
// this path does not, and an artifact that declares no runtime at all is
|
|
2123
|
+
// read as unconstrained by every loader downstream, which is the most
|
|
2124
|
+
// permissive possible answer to the least certain question.
|
|
2125
|
+
const runtimeTargets = Array.isArray(check.runtimeTargets)
|
|
2126
|
+
? [...check.runtimeTargets]
|
|
2127
|
+
: recomputeFlowRuntimeTargets(check, dependencies);
|
|
2128
|
+
if (runtimeTargets.length === 0) {
|
|
2129
|
+
throw new Error(
|
|
2130
|
+
`Flow "${flow?.programId ?? "(unknown)"}" has no runtime target: no runtime runs ` +
|
|
2131
|
+
"every one of its parts. Refusing to build a manifest that declares nothing — " +
|
|
2132
|
+
"run checkFlowProgram and read the empty-runtime-target-intersection error.",
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
1911
2135
|
|
|
1912
2136
|
const hostModelNodeIds = new Set(
|
|
1913
2137
|
check.nodes.filter((node) => node.dispatchModel === "host").map((node) => node.nodeId),
|
|
@@ -2032,13 +2256,17 @@ export function buildFlowModuleManifest({ flow, check, dependencies }) {
|
|
|
2032
2256
|
externalInterfaces,
|
|
2033
2257
|
methods,
|
|
2034
2258
|
schemasUsed,
|
|
2035
|
-
|
|
2259
|
+
// DERIVED, never asserted: the runtimes shared by every node plugin and
|
|
2260
|
+
// resolved component dependency (see collectFlowRuntimeTargets). A flow of
|
|
2261
|
+
// fully isomorphic nodes keeps both targets; a flow containing one
|
|
2262
|
+
// WasmEdge-only node is a WasmEdge-only flow and says so.
|
|
2263
|
+
runtimeTargets,
|
|
2036
2264
|
buildArtifacts: [
|
|
2037
2265
|
{
|
|
2038
2266
|
artifactId: `${cIdent(flow.programId)}-flow-runtime`,
|
|
2039
2267
|
kind: "wasm",
|
|
2040
2268
|
path: "dist/isomorphic/module.wasm",
|
|
2041
|
-
target: "
|
|
2269
|
+
target: runtimeTargets.join(","),
|
|
2042
2270
|
},
|
|
2043
2271
|
],
|
|
2044
2272
|
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,
|
|
@@ -12,6 +12,14 @@ export declare enum PluginFamily {
|
|
|
12
12
|
SDF = 7,
|
|
13
13
|
INFRASTRUCTURE = 8,
|
|
14
14
|
FLOW = 9,
|
|
15
|
-
BRIDGE = 10
|
|
15
|
+
BRIDGE = 10,
|
|
16
|
+
MANEUVER = 11,
|
|
17
|
+
ORBIT_DETERMINATION = 12,
|
|
18
|
+
FOUNDATION = 13,
|
|
19
|
+
PARSER = 14,
|
|
20
|
+
VALIDATOR = 15,
|
|
21
|
+
EXPORTER = 16,
|
|
22
|
+
PUBLISHER = 17,
|
|
23
|
+
BASILISK = 18
|
|
16
24
|
}
|
|
17
25
|
//# sourceMappingURL=plugin-family.d.ts.map
|
|
@@ -16,5 +16,13 @@ export var PluginFamily;
|
|
|
16
16
|
PluginFamily[PluginFamily["INFRASTRUCTURE"] = 8] = "INFRASTRUCTURE";
|
|
17
17
|
PluginFamily[PluginFamily["FLOW"] = 9] = "FLOW";
|
|
18
18
|
PluginFamily[PluginFamily["BRIDGE"] = 10] = "BRIDGE";
|
|
19
|
+
PluginFamily[PluginFamily["MANEUVER"] = 11] = "MANEUVER";
|
|
20
|
+
PluginFamily[PluginFamily["ORBIT_DETERMINATION"] = 12] = "ORBIT_DETERMINATION";
|
|
21
|
+
PluginFamily[PluginFamily["FOUNDATION"] = 13] = "FOUNDATION";
|
|
22
|
+
PluginFamily[PluginFamily["PARSER"] = 14] = "PARSER";
|
|
23
|
+
PluginFamily[PluginFamily["VALIDATOR"] = 15] = "VALIDATOR";
|
|
24
|
+
PluginFamily[PluginFamily["EXPORTER"] = 16] = "EXPORTER";
|
|
25
|
+
PluginFamily[PluginFamily["PUBLISHER"] = 17] = "PUBLISHER";
|
|
26
|
+
PluginFamily[PluginFamily["BASILISK"] = 18] = "BASILISK";
|
|
19
27
|
})(PluginFamily || (PluginFamily = {}));
|
|
20
28
|
//# sourceMappingURL=plugin-family.js.map
|