space-data-module-sdk 0.5.21 → 0.6.0

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 CHANGED
@@ -176,8 +176,15 @@ This repo currently includes:
176
176
  - non-JS bundle reference clients in
177
177
  [`examples/single-file-bundle/go`](./examples/single-file-bundle/go) and
178
178
  [`examples/single-file-bundle/python`](./examples/single-file-bundle/python)
179
- - a reference Node host and sync `sdn_host` bridge for the first hostcall
180
- surface
179
+ - reference Node and browser hosts plus the legacy sync `sdn_host` bridge for
180
+ sync-safe guest hostcalls
181
+
182
+ The current host/runtime contract is two-tiered:
183
+
184
+ - raw guest `sdn_host` imports stay sync-only
185
+ - generic async capabilities such as filesystem, network, IPFS, and protocol
186
+ adapters are exposed through `NodeHost`, `BrowserHost`, and the browser/module
187
+ harness APIs
181
188
 
182
189
  ## Runtime Targets
183
190
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "space-data-module-sdk",
3
- "version": "0.5.21",
3
+ "version": "0.6.0",
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",
@@ -51,7 +51,7 @@
51
51
  ],
52
52
  "scripts": {
53
53
  "test": "node --test",
54
- "test:host-surfaces": "node --test test/node-host.test.js test/host-abi.test.js",
54
+ "test:host-surfaces": "node --test test/node-host.test.js test/host-abi.test.js test/browser-host.test.js test/browser-module-harness.test.js test/testing-harness.test.js",
55
55
  "test:runtime-matrix": "SPACE_DATA_MODULE_SDK_ENABLE_RUNTIME_MATRIX=1 node --test test/runtime-matrix.test.js",
56
56
  "test:stream-ingest": "node --test test/runtime-host-stream-ingest.test.js",
57
57
  "test:module-stream": "node --test test/module-flatbuffer-stream-pump.test.js",
package/src/AGENTS.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # AGENTS
2
2
 
3
- Apply the root `AGENTS.md` first. This file routes source edits to the correct
4
- subsystem.
3
+ Apply the root `AGENTS.md` first. This file is a map for module authors reading
4
+ the SDK source, not a prompt to edit SDK internals by default.
5
5
 
6
- ## Source Routing
6
+ ## Use `src/` As A Reference Map
7
7
 
8
8
  - `src/manifest`, `src/compliance`, `src/standards`: manifest shape, type refs,
9
9
  standards validation, and compliance warnings/errors.
@@ -23,11 +23,10 @@ subsystem.
23
23
  - `src/generated`: generated code. Do not hand-edit unless regeneration is part
24
24
  of the task and you update the generation path or fixtures as needed.
25
25
 
26
- ## Working Rules
26
+ ## Author Rules
27
27
 
28
- - Keep module contracts portable unless the task is explicitly runtime-specific.
29
- - Prefer changes in the smallest subsystem that owns the behavior.
30
- - If a change crosses compiler, host, and docs, update each owner explicitly
31
- rather than hiding policy in one layer.
32
- - Add or update tests near the owning subsystem and then wire them into the
33
- repo-level verification set in `test/AGENTS.md`.
28
+ - Prefer public APIs and examples over editing `src/*`.
29
+ - Read the nearest child `AGENTS.md` to understand which source files define the
30
+ contract you are using.
31
+ - Only edit internals here when the task explicitly says to change the SDK
32
+ standard for everyone.
@@ -1,15 +1,13 @@
1
1
  # AGENTS
2
2
 
3
- You are in `src/auth`, which owns deployment authorization, signing helpers,
4
- and related record handling used by module distribution.
3
+ This directory is the reference implementation for deployment authorization and
4
+ signing helpers used by compliant module distribution.
5
5
 
6
- ## Rules
6
+ ## What Authors Should Use It For
7
7
 
8
8
  - Keep signing and authorization flows deterministic and explicit.
9
9
  - Do not invent app-local signature records when a repo-level contract belongs
10
10
  here.
11
- - When auth changes affect transport or deployment plans, update the related
12
- surfaces together rather than splitting incompatible changes across repos.
13
11
 
14
12
  ## Related Surfaces
15
13
 
@@ -18,8 +16,7 @@ and related record handling used by module distribution.
18
16
  - `test/transport-records.test.js`
19
17
  - `test/deployment-plan.test.js`
20
18
 
21
- ## Check Before You Finish
19
+ ## Note
22
20
 
23
- - `npm test`
24
- - `npm run check:compliance`
25
- - `node --test test/transport-records.test.js test/deployment-plan.test.js`
21
+ Use the exported helpers here rather than inventing your own signing records.
22
+ Only edit this directory when you are intentionally changing the auth contract.
@@ -1,21 +1,17 @@
1
1
  # AGENTS
2
2
 
3
- You are in `src/bundle`, which owns `sds.bundle` and the wasm custom-section
4
- packaging rules.
3
+ This directory is the reference implementation for `sds.bundle` and wasm
4
+ custom-section packaging.
5
5
 
6
- ## Rules
6
+ ## What Authors Should Use It For
7
7
 
8
8
  - `sds.bundle` is the single-file delivery format.
9
9
  - Put bundle data in a wasm custom section; do not append raw bytes after the
10
10
  wasm binary.
11
- - Keep parser, encoder, CLI behavior, vectors, and cross-language fixtures in
12
- sync.
13
- - If the bundle contract changes, regenerate vectors and update the bundle docs
14
- and demos in the same change.
11
+ - Use the public bundle helpers and CLI from this repo rather than inventing a
12
+ custom one-file format.
15
13
 
16
- ## Check Before You Finish
14
+ ## Note
17
15
 
18
- - `npm test`
19
- - `npm run check:compliance`
20
- - `node --test test/module-bundle.test.js test/module-bundle-vectors.test.js test/module-bundle-cli.test.js test/module-bundle-go.test.js test/module-bundle-python.test.js`
21
- - `npm run generate:vectors` when vectors or the contract changed
16
+ Only edit this directory when you are intentionally changing bundle behavior for
17
+ all consumers.
@@ -1,13 +1,18 @@
1
1
  # AGENTS
2
2
 
3
- Apply the root and `src/AGENTS.md` files first.
3
+ Apply the root and `src/AGENTS.md` files first. This directory tells module
4
+ authors how the compiler behaves.
4
5
 
5
- ## Area Ownership
6
+ ## What Authors Should Take From This Directory
6
7
 
7
- This directory owns source-to-wasm compile behavior, manifest embedding, runtime
8
- target inference, toolchain integration, and canonical artifact layout.
8
+ - `compileModuleFromSource(...)` and the CLI are the canonical ways to produce a
9
+ compliant artifact.
10
+ - Manifest embedding and required exports are generated together with the wasm
11
+ bytes.
12
+ - Shared browser/WasmEdge artifacts should land at
13
+ `dist/isomorphic/module.wasm`.
9
14
 
10
- ## Compiler Rules
15
+ ## Compiler Rules Authors Should Follow
11
16
 
12
17
  - Preserve the canonical manifest accessor exports and validate them together
13
18
  with compiled wasm.
@@ -18,18 +23,15 @@ target inference, toolchain integration, and canonical artifact layout.
18
23
  imports.
19
24
  - Prefer repo-local Emscripten or `sdn-emception`. Do not assume Homebrew
20
25
  toolchains.
21
- - If you change runtime-target inference, keep the browser/WasmEdge shared path
22
- on the standalone profile unless the docs and tests are updated deliberately.
23
26
 
24
- ## Key Files
27
+ ## Key Files To Read
25
28
 
26
29
  - `compileModule.js`
27
30
  - `compileModuleFromSource.js`
28
31
  - `emception*.js`
29
32
  - `invokeGlue.js`
30
33
 
31
- ## Verification
34
+ ## Note
32
35
 
33
- - `node --test test/module-sdk.test.js test/compliance.test.js`
34
- - `node --test test/browser-harness.test.js test/isomorphic-loader.test.js`
35
- - `node --test test/compiler-emception-subpath-export.test.js`
36
+ Do not edit compiler internals just to build one module. Only change this
37
+ directory when you are intentionally changing the repo-wide compile contract.
@@ -1,18 +1,16 @@
1
1
  # AGENTS
2
2
 
3
- You are in `src/compliance`, which defines what counts as a compliant module or
3
+ This directory is the reference policy for what counts as a compliant module or
4
4
  artifact.
5
5
 
6
- ## Rules
6
+ ## What Authors Should Use It For
7
7
 
8
- - Treat this directory as policy, not convenience code.
9
- - When tightening validation, update tests and error messages together so the
10
- failure mode is actionable.
11
- - When relaxing validation, make sure the compiler, docs, and examples still
12
- describe the broader contract accurately.
8
+ - Use the CLI and public validation helpers to understand whether a manifest or
9
+ artifact is compliant.
10
+ - Read this directory when you need to know why a module was rejected.
11
+ - Prefer fixing your module manifest or build output before changing policy here.
13
12
 
14
- ## Check Before You Finish
13
+ ## Note
15
14
 
16
- - `npm test`
17
- - `npm run check:compliance`
18
- - `node --test test/module-sdk.test.js test/compliance.test.js`
15
+ Only edit this directory when you are intentionally changing the compliance
16
+ standard for all module authors.
@@ -1,14 +1,17 @@
1
1
  # AGENTS
2
2
 
3
- Apply the root and `src/AGENTS.md` files first.
3
+ Apply the root and `src/AGENTS.md` files first. This directory explains the
4
+ host/runtime boundary that compliant modules can rely on.
4
5
 
5
- ## Area Ownership
6
+ ## What Authors Should Take From This Directory
6
7
 
7
- This directory owns the Node reference host, browser host, browser edge shims,
8
- WASI shims, the sync `sdn_host` ABI bridge, WasmEdge launch planning, and the
9
- isomorphic loader path.
8
+ - The portable shared path is standalone WASI, optionally plus `sdn_host`.
9
+ - Browser helpers here are host-side shims and harnesses, not proof that raw
10
+ WasmEdge-native guest imports are browser-portable.
11
+ - The isomorphic loader and browser harness show how the same artifact is meant
12
+ to run in both places.
10
13
 
11
- ## Host Rules
14
+ ## Host Rules Authors Should Follow
12
15
 
13
16
  - Shared browser/WasmEdge artifacts must stay within standalone WASI plus the
14
17
  optional `sdn_host` bridge. Raw WasmEdge-native extension imports are not
@@ -20,7 +23,7 @@ isomorphic loader path.
20
23
  - Scope host capabilities tightly: filesystem roots, network allowlists, TLS,
21
24
  exec, timers, and crypto should stay explicit.
22
25
 
23
- ## Key Files
26
+ ## Key Files To Read
24
27
 
25
28
  - `abi.js`
26
29
  - `browserEdgeShims.js`
@@ -29,11 +32,7 @@ isomorphic loader path.
29
32
  - `nodeHost.js`
30
33
  - `wasiShim.js`
31
34
 
32
- ## Verification
35
+ ## Note
33
36
 
34
- - `node --test test/node-host.test.js test/host-abi.test.js`
35
- - `node --test test/browser-harness.test.js test/isomorphic-loader.test.js`
36
- - `node --test test/process-invoke.test.js`
37
- - `node --test test/wasmedge-runner-build.test.js`
38
- - `node --test test/wasmedge-runner-runtime.test.js` when WasmEdge behavior is
39
- part of the change
37
+ Prefer using the exported host and harness surfaces rather than editing these
38
+ files unless the task is explicitly to change the SDK host contract.
package/src/host/abi.js CHANGED
@@ -7,7 +7,7 @@ export const DEFAULT_HOSTCALL_IMPORT_MODULE = "sdn_host";
7
7
  export const HOSTCALL_STATUS_OK = 0;
8
8
  export const HOSTCALL_STATUS_ERROR = 1;
9
9
 
10
- export const NodeHostSyncHostcallOperations = Object.freeze([
10
+ export const SyncHostcallOperations = Object.freeze([
11
11
  "host.runtimeTarget",
12
12
  "host.listCapabilities",
13
13
  "host.listSupportedCapabilities",
@@ -22,6 +22,7 @@ export const NodeHostSyncHostcallOperations = Object.freeze([
22
22
  "schedule.next",
23
23
  "filesystem.resolvePath",
24
24
  ]);
25
+ export const NodeHostSyncHostcallOperations = SyncHostcallOperations;
25
26
 
26
27
  function assertNonEmptyString(value, label) {
27
28
  const normalized = String(value ?? "").trim();
@@ -149,7 +150,7 @@ function encodeHostcallValue(value) {
149
150
  return value;
150
151
  }
151
152
 
152
- export function dispatchNodeHostSyncOperation(host, operation, params = null) {
153
+ export function dispatchHostSyncOperation(host, operation, params = null) {
153
154
  const normalized = assertNonEmptyString(operation, "Hostcall operation");
154
155
  switch (normalized) {
155
156
  case "host.runtimeTarget":
@@ -185,12 +186,39 @@ export function dispatchNodeHostSyncOperation(host, operation, params = null) {
185
186
  }
186
187
  }
187
188
 
189
+ export function dispatchNodeHostSyncOperation(host, operation, params = null) {
190
+ return dispatchHostSyncOperation(host, operation, params);
191
+ }
192
+
188
193
  export function createNodeHostSyncDispatcher(host) {
189
194
  if (!host || typeof host !== "object") {
190
195
  throw new TypeError("createNodeHostSyncDispatcher requires a host object.");
191
196
  }
192
197
  return (operation, params = null) =>
193
- dispatchNodeHostSyncOperation(host, operation, params);
198
+ dispatchHostSyncOperation(host, operation, params);
199
+ }
200
+
201
+ export function createHostSyncDispatcher(host) {
202
+ return createNodeHostSyncDispatcher(host);
203
+ }
204
+
205
+ export async function dispatchHostOperation(host, operation, params = null) {
206
+ if (!host || typeof host !== "object") {
207
+ throw new TypeError("dispatchHostOperation requires a host object.");
208
+ }
209
+ const normalized = assertNonEmptyString(operation, "Host operation");
210
+ if (typeof host.invoke === "function") {
211
+ return host.invoke(normalized, params);
212
+ }
213
+ return dispatchHostSyncOperation(host, normalized, params);
214
+ }
215
+
216
+ export function createAsyncHostDispatcher(host) {
217
+ if (!host || typeof host !== "object") {
218
+ throw new TypeError("createAsyncHostDispatcher requires a host object.");
219
+ }
220
+ return (operation, params = null) =>
221
+ dispatchHostOperation(host, operation, params);
194
222
  }
195
223
 
196
224
  export function createJsonHostcallBridge(options = {}) {
@@ -346,6 +346,10 @@ export function createBrowserEdgeShims(options = {}) {
346
346
  WebSocket: options.WebSocket ?? globalThis.WebSocket,
347
347
  crypto: options.crypto ?? globalThis.crypto,
348
348
  performance: options.performance ?? globalThis.performance,
349
+ network: options.network ?? null,
350
+ ipfs: options.ipfs ?? null,
351
+ protocolHandle: options.protocolHandle ?? null,
352
+ protocolDial: options.protocolDial ?? null,
349
353
  filesystem:
350
354
  options.filesystem ??
351
355
  createMemoryFilesystemEdgeShim({
@@ -20,7 +20,11 @@ export const BrowserHostSupportedCapabilities = Object.freeze([
20
20
  "schedule_cron",
21
21
  "http",
22
22
  "websocket",
23
+ "network",
23
24
  "filesystem",
25
+ "ipfs",
26
+ "protocol_handle",
27
+ "protocol_dial",
24
28
  "context_read",
25
29
  "context_write",
26
30
  "crypto_hash",
@@ -39,11 +43,13 @@ export const BrowserHostSupportedOperations = Object.freeze([
39
43
  "clock.monotonicNow",
40
44
  "clock.nowIso",
41
45
  "random.bytes",
46
+ "timers.delay",
42
47
  "schedule.parse",
43
48
  "schedule.matches",
44
49
  "schedule.next",
45
50
  "http.request",
46
51
  "websocket.exchange",
52
+ "network.request",
47
53
  "filesystem.resolvePath",
48
54
  "filesystem.readFile",
49
55
  "filesystem.writeFile",
@@ -53,6 +59,10 @@ export const BrowserHostSupportedOperations = Object.freeze([
53
59
  "filesystem.readdir",
54
60
  "filesystem.stat",
55
61
  "filesystem.rename",
62
+ "ipfs.invoke",
63
+ "protocol_handle.register",
64
+ "protocol_handle.unregister",
65
+ "protocol_dial.dial",
56
66
  "context.get",
57
67
  "context.set",
58
68
  "context.delete",
@@ -73,6 +83,46 @@ export class BrowserHostCapabilityError extends Error {
73
83
  }
74
84
  }
75
85
 
86
+ function normalizeBrowserNetworkTransport(params = {}) {
87
+ const explicit = String(
88
+ params.transport ?? params.kind ?? params.request?.transport ?? "",
89
+ )
90
+ .trim()
91
+ .toLowerCase();
92
+ if (explicit) {
93
+ return explicit;
94
+ }
95
+ const candidateUrl = params.url ?? params.request?.url ?? null;
96
+ if (candidateUrl) {
97
+ const protocol = new URL(candidateUrl, "https://browser-host.invalid")
98
+ .protocol.toLowerCase();
99
+ if (protocol === "http:" || protocol === "https:") {
100
+ return "http";
101
+ }
102
+ if (protocol === "ws:" || protocol === "wss:") {
103
+ return "websocket";
104
+ }
105
+ }
106
+ throw new Error(
107
+ 'network.request requires a transport value such as "http" or "websocket".',
108
+ );
109
+ }
110
+
111
+ async function invokeAdapterMethod(adapter, methodName, params, label) {
112
+ if (!adapter || typeof adapter !== "object") {
113
+ throw new Error(`${label} adapter is not configured for this host.`);
114
+ }
115
+ if (typeof adapter[methodName] === "function") {
116
+ return adapter[methodName](params);
117
+ }
118
+ if (typeof adapter.invoke === "function") {
119
+ return adapter.invoke(methodName, params);
120
+ }
121
+ throw new Error(
122
+ `${label} adapter does not implement "${methodName}" or invoke().`,
123
+ );
124
+ }
125
+
76
126
  export class BrowserHost {
77
127
  constructor(options = {}) {
78
128
  this.runtimeTarget = RuntimeTarget.BROWSER;
@@ -86,6 +136,11 @@ export class BrowserHost {
86
136
  WebSocket: options.WebSocket ?? options.edgeShims?.WebSocket,
87
137
  crypto: options.crypto ?? options.edgeShims?.crypto,
88
138
  performance: options.performance ?? options.edgeShims?.performance,
139
+ network: options.network ?? options.edgeShims?.network,
140
+ ipfs: options.ipfs ?? options.edgeShims?.ipfs,
141
+ protocolHandle:
142
+ options.protocolHandle ?? options.edgeShims?.protocolHandle,
143
+ protocolDial: options.protocolDial ?? options.edgeShims?.protocolDial,
89
144
  filesystem: options.filesystem ?? options.edgeShims?.filesystem,
90
145
  filesystemRoot: options.filesystemRoot ?? options.edgeShims?.filesystemRoot,
91
146
  });
@@ -97,6 +152,10 @@ export class BrowserHost {
97
152
  const fetchImpl = edgeShims.fetch;
98
153
  const WebSocketImpl = edgeShims.WebSocket;
99
154
  const filesystem = edgeShims.filesystem;
155
+ const networkAdapter = edgeShims.network;
156
+ const ipfsAdapter = edgeShims.ipfs;
157
+ const protocolHandleAdapter = edgeShims.protocolHandle;
158
+ const protocolDialAdapter = edgeShims.protocolDial;
100
159
 
101
160
  this._grantedCapabilities = granted;
102
161
  this._contextStore = options.contextStore ?? new Map();
@@ -239,6 +298,73 @@ export class BrowserHost {
239
298
  },
240
299
  });
241
300
 
301
+ this.network = Object.freeze({
302
+ request: async (params = {}) => {
303
+ this.#assertCapability("network", "network.request");
304
+ const transport = normalizeBrowserNetworkTransport(params);
305
+ const request = params.request ?? params;
306
+ if (transport === "http") {
307
+ return this.http.request(request);
308
+ }
309
+ if (transport === "websocket") {
310
+ return this.websocket.exchange(request);
311
+ }
312
+ if (networkAdapter) {
313
+ return invokeAdapterMethod(networkAdapter, "request", {
314
+ ...request,
315
+ transport,
316
+ }, "network");
317
+ }
318
+ throw new Error(
319
+ `Browser host does not support network transport "${transport}".`,
320
+ );
321
+ },
322
+ });
323
+
324
+ this.ipfs = Object.freeze({
325
+ invoke: async (params = {}) => {
326
+ this.#assertCapability("ipfs", "ipfs.invoke");
327
+ const operation = String(params.operation ?? "invoke").trim();
328
+ if (!operation) {
329
+ throw new Error("ipfs.invoke requires a non-empty operation.");
330
+ }
331
+ return invokeAdapterMethod(ipfsAdapter, operation, params, "ipfs");
332
+ },
333
+ });
334
+
335
+ this.protocolHandle = Object.freeze({
336
+ register: async (params = {}) => {
337
+ this.#assertCapability("protocol_handle", "protocol_handle.register");
338
+ return invokeAdapterMethod(
339
+ protocolHandleAdapter,
340
+ "register",
341
+ params,
342
+ "protocol_handle",
343
+ );
344
+ },
345
+ unregister: async (params = {}) => {
346
+ this.#assertCapability("protocol_handle", "protocol_handle.unregister");
347
+ return invokeAdapterMethod(
348
+ protocolHandleAdapter,
349
+ "unregister",
350
+ params,
351
+ "protocol_handle",
352
+ );
353
+ },
354
+ });
355
+
356
+ this.protocolDial = Object.freeze({
357
+ dial: async (params = {}) => {
358
+ this.#assertCapability("protocol_dial", "protocol_dial.dial");
359
+ return invokeAdapterMethod(
360
+ protocolDialAdapter,
361
+ "dial",
362
+ params,
363
+ "protocol_dial",
364
+ );
365
+ },
366
+ });
367
+
242
368
  this.context = Object.freeze({
243
369
  get: (scope, key) => {
244
370
  this.#assertCapability("context_read", "context.get");
@@ -378,17 +504,125 @@ export class BrowserHost {
378
504
  }
379
505
 
380
506
  hasCapability(capability) {
381
- return this._grantedCapabilities.has(capability);
507
+ const normalized = String(capability ?? "").trim();
508
+ return (
509
+ this._grantedCapabilities.has(normalized) ||
510
+ (this._grantedCapabilities.has("network") &&
511
+ ["http", "websocket"].includes(normalized))
512
+ );
382
513
  }
383
514
 
384
515
  assertCapability(capability, operation) {
385
516
  this.#assertCapability(capability, operation);
386
517
  }
387
518
 
519
+ async invoke(operation, params = {}) {
520
+ const normalized = String(operation ?? "").trim();
521
+ switch (normalized) {
522
+ case "host.runtimeTarget":
523
+ return this.runtimeTarget;
524
+ case "host.listCapabilities":
525
+ return this.listCapabilities();
526
+ case "host.listSupportedCapabilities":
527
+ return this.listSupportedCapabilities();
528
+ case "host.listOperations":
529
+ return this.listOperations();
530
+ case "host.hasCapability":
531
+ return this.hasCapability(params.capability);
532
+ case "clock.now":
533
+ return this.clock.now();
534
+ case "clock.monotonicNow":
535
+ return this.clock.monotonicNow();
536
+ case "clock.nowIso":
537
+ return this.clock.nowIso();
538
+ case "random.bytes":
539
+ return this.random.bytes(params.length);
540
+ case "timers.delay":
541
+ return this.timers.delay(params.ms ?? params.delayMs ?? 0);
542
+ case "schedule.parse":
543
+ return this.schedule.parse(params.expression);
544
+ case "schedule.matches":
545
+ return this.schedule.matches(params.expression, params.date);
546
+ case "schedule.next":
547
+ return this.schedule.next(params.expression, params.from);
548
+ case "http.request":
549
+ return this.http.request(params);
550
+ case "websocket.exchange":
551
+ return this.websocket.exchange(params);
552
+ case "network.request":
553
+ return this.network.request(params);
554
+ case "filesystem.resolvePath":
555
+ return this.filesystem.resolvePath(params.path);
556
+ case "filesystem.readFile":
557
+ return this.filesystem.readFile(params.path, {
558
+ encoding: params.encoding,
559
+ });
560
+ case "filesystem.writeFile":
561
+ return this.filesystem.writeFile(params.path, params.value, {
562
+ encoding: params.encoding,
563
+ });
564
+ case "filesystem.appendFile":
565
+ return this.filesystem.appendFile(params.path, params.value, {
566
+ encoding: params.encoding,
567
+ });
568
+ case "filesystem.deleteFile":
569
+ return this.filesystem.deleteFile(params.path);
570
+ case "filesystem.mkdir":
571
+ return this.filesystem.mkdir(params.path, {
572
+ recursive: params.recursive,
573
+ });
574
+ case "filesystem.readdir":
575
+ return this.filesystem.readdir(params.path);
576
+ case "filesystem.stat":
577
+ return this.filesystem.stat(params.path);
578
+ case "filesystem.rename":
579
+ return this.filesystem.rename(params.fromPath, params.toPath);
580
+ case "ipfs.invoke":
581
+ return this.ipfs.invoke(params);
582
+ case "protocol_handle.register":
583
+ return this.protocolHandle.register(params);
584
+ case "protocol_handle.unregister":
585
+ return this.protocolHandle.unregister(params);
586
+ case "protocol_dial.dial":
587
+ return this.protocolDial.dial(params);
588
+ case "context.get":
589
+ return this.context.get(params.scope, params.key);
590
+ case "context.set":
591
+ return this.context.set(params.scope, params.key, params.value);
592
+ case "context.delete":
593
+ return this.context.delete(params.scope, params.key);
594
+ case "context.listKeys":
595
+ return this.context.listKeys(params.scope);
596
+ case "context.listScopes":
597
+ return this.context.listScopes();
598
+ case "crypto.sha256":
599
+ return this.crypto.sha256(params.value ?? params.bytes);
600
+ case "crypto.sha512":
601
+ return this.crypto.sha512(params.value ?? params.bytes);
602
+ case "crypto.aesGcmEncrypt":
603
+ return this.crypto.aesGcmEncrypt(params);
604
+ case "crypto.aesGcmDecrypt":
605
+ return this.crypto.aesGcmDecrypt(params);
606
+ default:
607
+ throw new Error(`Unknown browser host operation "${normalized}".`);
608
+ }
609
+ }
610
+
388
611
  // --- Private ---
389
612
 
390
613
  #assertCapability(capability, operation) {
391
- if (!this._grantedCapabilities.has(capability)) {
614
+ const normalized = String(capability ?? "").trim();
615
+ const networkBackedCapabilities = new Set([
616
+ "http",
617
+ "websocket",
618
+ ]);
619
+ if (
620
+ !this._grantedCapabilities.has(normalized) &&
621
+ !(
622
+ this._grantedCapabilities.has("network") &&
623
+ networkBackedCapabilities.has(normalized)
624
+ )
625
+ ) {
392
626
  throw new BrowserHostCapabilityError(capability, operation);
393
627
  }
394
628
  }
@@ -13,11 +13,25 @@ import {
13
13
  createBrowserModuleHarness,
14
14
  detectArtifactProfile,
15
15
  } from "../testing/browserModuleHarness.js";
16
+ import { dispatchHostOperation } from "./abi.js";
16
17
 
17
18
  const isBrowser =
18
19
  typeof globalThis.window !== "undefined" &&
19
20
  typeof globalThis.document !== "undefined";
20
21
 
22
+ function attachHostDispatch(harness, host) {
23
+ if (!host || typeof host !== "object") {
24
+ return harness;
25
+ }
26
+ return {
27
+ ...harness,
28
+ host,
29
+ async callHost(operation, params = {}) {
30
+ return dispatchHostOperation(host, operation, params);
31
+ },
32
+ };
33
+ }
34
+
21
35
  async function createWasmEdgeCommandHarness(options = {}) {
22
36
  const [
23
37
  { spawn },
@@ -175,14 +189,18 @@ export async function loadModule(options = {}) {
175
189
  (inspection.profile === "standalone" || inspection.profile === "sdn-abi") &&
176
190
  inspection.exports.includes("_start")
177
191
  ) {
178
- return createWasmEdgeCommandHarness(options);
192
+ return attachHostDispatch(
193
+ await createWasmEdgeCommandHarness(options),
194
+ options.host ?? null,
195
+ );
179
196
  }
180
197
  }
181
198
 
182
- return createModuleHarness({
183
- runtime: {
184
- kind: "wasmedge",
185
- wasmPath: source,
199
+ return attachHostDispatch(
200
+ await createModuleHarness({
201
+ runtime: {
202
+ kind: "wasmedge",
203
+ wasmPath: source,
186
204
  wasmEdgeBinary: options.wasmEdgeBinary,
187
205
  wasmEdgeRunnerBinary: options.wasmEdgeRunnerBinary,
188
206
  enableThreads: options.enableThreads,
@@ -191,23 +209,28 @@ export async function loadModule(options = {}) {
191
209
  hostProfile: options.hostProfile,
192
210
  modules: options.modules,
193
211
  defaultModuleId: options.defaultModuleId,
194
- metadata: options.metadata,
195
- },
196
- });
212
+ metadata: options.metadata,
213
+ },
214
+ }),
215
+ options.host ?? null,
216
+ );
197
217
  }
198
218
 
199
- return createModuleHarness({
200
- runtime: {
201
- kind: runtimeKind,
202
- command: options.command ?? runtimeKind,
203
- args: options.args ?? [source],
204
- env: options.env,
205
- cwd: options.cwd,
206
- hostProfile: options.hostProfile,
207
- modules: options.modules,
208
- defaultModuleId: options.defaultModuleId,
209
- },
210
- });
219
+ return attachHostDispatch(
220
+ await createModuleHarness({
221
+ runtime: {
222
+ kind: runtimeKind,
223
+ command: options.command ?? runtimeKind,
224
+ args: options.args ?? [source],
225
+ env: options.env,
226
+ cwd: options.cwd,
227
+ hostProfile: options.hostProfile,
228
+ modules: options.modules,
229
+ defaultModuleId: options.defaultModuleId,
230
+ },
231
+ }),
232
+ options.host ?? null,
233
+ );
211
234
  }
212
235
 
213
236
  /**
@@ -46,10 +46,14 @@ export const NodeHostSupportedCapabilities = Object.freeze([
46
46
  "http",
47
47
  "websocket",
48
48
  "mqtt",
49
+ "network",
49
50
  "filesystem",
50
51
  "tcp",
51
52
  "udp",
52
53
  "tls",
54
+ "ipfs",
55
+ "protocol_handle",
56
+ "protocol_dial",
53
57
  "context_read",
54
58
  "context_write",
55
59
  "crypto_hash",
@@ -75,6 +79,7 @@ export const NodeHostSupportedOperations = Object.freeze([
75
79
  "websocket.exchange",
76
80
  "mqtt.publish",
77
81
  "mqtt.subscribeOnce",
82
+ "network.request",
78
83
  "filesystem.resolvePath",
79
84
  "filesystem.readFile",
80
85
  "filesystem.writeFile",
@@ -87,6 +92,10 @@ export const NodeHostSupportedOperations = Object.freeze([
87
92
  "tcp.request",
88
93
  "udp.request",
89
94
  "tls.request",
95
+ "ipfs.invoke",
96
+ "protocol_handle.register",
97
+ "protocol_handle.unregister",
98
+ "protocol_dial.dial",
90
99
  "exec.execFile",
91
100
  "context.get",
92
101
  "context.set",
@@ -229,6 +238,45 @@ function normalizeAllowedPorts(ports, label, { allowZero = false } = {}) {
229
238
  return normalized;
230
239
  }
231
240
 
241
+ function normalizeNetworkTransport(params = {}) {
242
+ const explicit = String(
243
+ params.transport ?? params.kind ?? params.request?.transport ?? "",
244
+ )
245
+ .trim()
246
+ .toLowerCase();
247
+ if (explicit) {
248
+ return explicit;
249
+ }
250
+ const candidateUrl = params.url ?? params.request?.url ?? null;
251
+ if (candidateUrl) {
252
+ const protocol = new URL(candidateUrl).protocol.toLowerCase();
253
+ if (protocol === "http:" || protocol === "https:") {
254
+ return "http";
255
+ }
256
+ if (protocol === "ws:" || protocol === "wss:") {
257
+ return "websocket";
258
+ }
259
+ }
260
+ throw new Error(
261
+ 'network.request requires a transport value such as "http", "tcp", or "websocket".',
262
+ );
263
+ }
264
+
265
+ async function invokeAdapterMethod(adapter, methodName, params, label) {
266
+ if (!adapter || typeof adapter !== "object") {
267
+ throw new Error(`${label} adapter is not configured for this host.`);
268
+ }
269
+ if (typeof adapter[methodName] === "function") {
270
+ return adapter[methodName](params);
271
+ }
272
+ if (typeof adapter.invoke === "function") {
273
+ return adapter.invoke(methodName, params);
274
+ }
275
+ throw new Error(
276
+ `${label} adapter does not implement "${methodName}" or invoke().`,
277
+ );
278
+ }
279
+
232
280
  function normalizeFilesystemRoot(rootPath) {
233
281
  return path.resolve(String(rootPath ?? process.cwd()));
234
282
  }
@@ -1397,6 +1445,10 @@ export class NodeHost {
1397
1445
  );
1398
1446
  this.fetch = options.fetch ?? globalThis.fetch;
1399
1447
  this.WebSocket = options.WebSocket ?? globalThis.WebSocket;
1448
+ this._networkAdapter = options.network ?? null;
1449
+ this._ipfsAdapter = options.ipfs ?? null;
1450
+ this._protocolHandleAdapter = options.protocolHandle ?? null;
1451
+ this._protocolDialAdapter = options.protocolDial ?? null;
1400
1452
  if (typeof this.fetch !== "function") {
1401
1453
  throw new TypeError(
1402
1454
  "A fetch implementation is required to create a Node host.",
@@ -1550,6 +1602,40 @@ export class NodeHost {
1550
1602
  }),
1551
1603
  });
1552
1604
 
1605
+ this.network = Object.freeze({
1606
+ request: async (params = {}) =>
1607
+ this.#withCapability("network", "network.request", async () => {
1608
+ const transport = normalizeNetworkTransport(params);
1609
+ const request = params.request ?? params;
1610
+ switch (transport) {
1611
+ case "http":
1612
+ return this.http.request(request);
1613
+ case "websocket":
1614
+ return this.websocket.exchange(request);
1615
+ case "mqtt":
1616
+ return request.subscribe === true
1617
+ ? this.mqtt.subscribeOnce(request)
1618
+ : this.mqtt.publish(request);
1619
+ case "tcp":
1620
+ return this.tcp.request(request);
1621
+ case "udp":
1622
+ return this.udp.request(request);
1623
+ case "tls":
1624
+ return this.tls.request(request);
1625
+ default:
1626
+ if (this._networkAdapter) {
1627
+ return invokeAdapterMethod(this._networkAdapter, "request", {
1628
+ ...request,
1629
+ transport,
1630
+ }, "network");
1631
+ }
1632
+ throw new Error(
1633
+ `Node host does not support network transport "${transport}".`,
1634
+ );
1635
+ }
1636
+ }),
1637
+ });
1638
+
1553
1639
  this.mqtt = Object.freeze({
1554
1640
  publish: async (mqttOptions = {}) =>
1555
1641
  this.#withCapability("mqtt", "mqtt.publish", async () => {
@@ -1825,6 +1911,56 @@ export class NodeHost {
1825
1911
  this._contextStore.listScopes(),
1826
1912
  ),
1827
1913
  });
1914
+
1915
+ this.ipfs = Object.freeze({
1916
+ invoke: async (params = {}) =>
1917
+ this.#withCapability("ipfs", "ipfs.invoke", async () => {
1918
+ const operation = String(params.operation ?? "invoke").trim();
1919
+ if (!operation) {
1920
+ throw new Error("ipfs.invoke requires a non-empty operation.");
1921
+ }
1922
+ return invokeAdapterMethod(this._ipfsAdapter, operation, params, "ipfs");
1923
+ }),
1924
+ });
1925
+
1926
+ this.protocolHandle = Object.freeze({
1927
+ register: async (params = {}) =>
1928
+ this.#withCapability(
1929
+ "protocol_handle",
1930
+ "protocol_handle.register",
1931
+ async () =>
1932
+ invokeAdapterMethod(
1933
+ this._protocolHandleAdapter,
1934
+ "register",
1935
+ params,
1936
+ "protocol_handle",
1937
+ ),
1938
+ ),
1939
+ unregister: async (params = {}) =>
1940
+ this.#withCapability(
1941
+ "protocol_handle",
1942
+ "protocol_handle.unregister",
1943
+ async () =>
1944
+ invokeAdapterMethod(
1945
+ this._protocolHandleAdapter,
1946
+ "unregister",
1947
+ params,
1948
+ "protocol_handle",
1949
+ ),
1950
+ ),
1951
+ });
1952
+
1953
+ this.protocolDial = Object.freeze({
1954
+ dial: async (params = {}) =>
1955
+ this.#withCapability("protocol_dial", "protocol_dial.dial", async () =>
1956
+ invokeAdapterMethod(
1957
+ this._protocolDialAdapter,
1958
+ "dial",
1959
+ params,
1960
+ "protocol_dial",
1961
+ ),
1962
+ ),
1963
+ });
1828
1964
  }
1829
1965
 
1830
1966
  listCapabilities() {
@@ -1840,11 +1976,26 @@ export class NodeHost {
1840
1976
  }
1841
1977
 
1842
1978
  hasCapability(capability) {
1843
- return this._grantedCapabilities.has(String(capability ?? "").trim());
1979
+ const normalized = String(capability ?? "").trim();
1980
+ return (
1981
+ this._grantedCapabilities.has(normalized) ||
1982
+ (this._grantedCapabilities.has("network") &&
1983
+ ["http", "websocket", "mqtt", "tcp", "udp", "tls"].includes(
1984
+ normalized,
1985
+ ))
1986
+ );
1844
1987
  }
1845
1988
 
1846
1989
  assertCapability(capability, operation = null) {
1847
1990
  const normalized = assertNonEmptyString(capability, "Capability id");
1991
+ const networkBackedCapabilities = new Set([
1992
+ "http",
1993
+ "websocket",
1994
+ "mqtt",
1995
+ "tcp",
1996
+ "udp",
1997
+ "tls",
1998
+ ]);
1848
1999
  if (!this._supportedCapabilities.has(normalized)) {
1849
2000
  throw new HostCapabilityError(
1850
2001
  `Capability "${normalized}" is not supported by the reference Node host.`,
@@ -1855,7 +2006,13 @@ export class NodeHost {
1855
2006
  },
1856
2007
  );
1857
2008
  }
1858
- if (!this._grantedCapabilities.has(normalized)) {
2009
+ if (
2010
+ !this._grantedCapabilities.has(normalized) &&
2011
+ !(
2012
+ this._grantedCapabilities.has("network") &&
2013
+ networkBackedCapabilities.has(normalized)
2014
+ )
2015
+ ) {
1859
2016
  throw new HostCapabilityError(
1860
2017
  `Capability "${normalized}" is not granted for this Node host.`,
1861
2018
  {
@@ -1871,6 +2028,16 @@ export class NodeHost {
1871
2028
  async invoke(operation, params = {}) {
1872
2029
  const normalized = assertNonEmptyString(operation, "Host operation");
1873
2030
  switch (normalized) {
2031
+ case "host.runtimeTarget":
2032
+ return this.runtimeTarget;
2033
+ case "host.listCapabilities":
2034
+ return this.listCapabilities();
2035
+ case "host.listSupportedCapabilities":
2036
+ return this.listSupportedCapabilities();
2037
+ case "host.listOperations":
2038
+ return this.listOperations();
2039
+ case "host.hasCapability":
2040
+ return this.hasCapability(params.capability);
1874
2041
  case "clock.now":
1875
2042
  return this.clock.now();
1876
2043
  case "clock.monotonicNow":
@@ -1897,6 +2064,8 @@ export class NodeHost {
1897
2064
  return this.mqtt.publish(params);
1898
2065
  case "mqtt.subscribeOnce":
1899
2066
  return this.mqtt.subscribeOnce(params);
2067
+ case "network.request":
2068
+ return this.network.request(params);
1900
2069
  case "filesystem.resolvePath":
1901
2070
  return this.filesystem.resolvePath(params.path);
1902
2071
  case "filesystem.readFile":
@@ -1929,6 +2098,14 @@ export class NodeHost {
1929
2098
  return this.udp.request(params);
1930
2099
  case "tls.request":
1931
2100
  return this.tls.request(params);
2101
+ case "ipfs.invoke":
2102
+ return this.ipfs.invoke(params);
2103
+ case "protocol_handle.register":
2104
+ return this.protocolHandle.register(params);
2105
+ case "protocol_handle.unregister":
2106
+ return this.protocolHandle.unregister(params);
2107
+ case "protocol_dial.dial":
2108
+ return this.protocolDial.dial(params);
1932
2109
  case "exec.execFile":
1933
2110
  return this.exec.execFile(params);
1934
2111
  case "context.get":
@@ -1,18 +1,15 @@
1
1
  # AGENTS
2
2
 
3
- You are in `src/manifest`, which owns the manifest schema codecs and
4
- normalization rules.
3
+ This directory is the reference implementation for manifest encoding,
4
+ normalization, and round-trip behavior.
5
5
 
6
- ## Rules
6
+ ## What Authors Should Use It For
7
7
 
8
8
  - Keep manifest encode/decode round-trips stable.
9
9
  - Prefer canonical SDS schema names and file identifiers; do not add repo-local
10
10
  aliases when a standards name already exists.
11
- - If you change a manifest field or normalization rule, update the compiler,
12
- compliance checks, and any affected examples/tests together.
13
11
 
14
- ## Check Before You Finish
12
+ ## Note
15
13
 
16
- - `npm test`
17
- - `npm run check:compliance`
18
- - `node --test test/module-sdk.test.js test/compliance.test.js`
14
+ Use the public manifest helpers from this repo to build manifests. Only edit
15
+ this directory when you are intentionally changing the manifest standard.
@@ -1,12 +1,15 @@
1
1
  # AGENTS
2
2
 
3
- Apply the root and `src/AGENTS.md` files first.
3
+ Apply the root and `src/AGENTS.md` files first. This directory shows the
4
+ host-owned storage and ingest path available to module authors.
4
5
 
5
- ## Area Ownership
6
+ ## What Authors Should Take From This Directory
6
7
 
7
- This directory owns the canonical runtime-host storage model: row handles,
8
- region handles, FlatSQL-backed storage, and binary FlatBuffer ingest on the host
9
- side.
8
+ - Host-owned durable identity is `(schemaFileId, rowId)` for rows and
9
+ `(regionId, recordIndex)` for aligned-binary regions.
10
+ - The canonical ingest path is direct FlatBuffer bytes, not JSON.
11
+ - If the host owns persistence, use these helpers. If a module owns state, keep
12
+ the stream binary and use the resident-module pump path from `src/testing`.
10
13
 
11
14
  ## Storage And Streaming Rules
12
15
 
@@ -19,14 +22,13 @@ side.
19
22
  - If a resident module owns state, keep the stream binary and push into the
20
23
  module through the harness/pump path rather than inventing JSON wrappers.
21
24
 
22
- ## Key Files
25
+ ## Key Files To Read
23
26
 
24
27
  - `flatbufferStreamIngestor.js`
25
28
  - `flatsqlRuntimeStore.js`
26
29
  - `index.js`
27
30
 
28
- ## Verification
31
+ ## Note
29
32
 
30
- - `npm run test:stream-ingest`
31
- - `node --test test/flatsql-local-node.test.js`
32
- - `npm run benchmark:stream-1gib` for large-stream changes
33
+ Do not edit this directory just to store data for one module. Use the exported
34
+ helpers unless you are intentionally changing the runtime-host contract.
@@ -3,15 +3,71 @@ import { createFlatBufferStreamIngestor } from "./flatbufferStreamIngestor.js";
3
3
  import { createRuntimeRegionStore } from "./runtimeRegionStore.js";
4
4
  import { createModuleRegistry } from "./moduleRegistry.js";
5
5
 
6
+ function normalizeCapabilityId(capability) {
7
+ const normalized = String(capability ?? "").trim();
8
+ if (!normalized) {
9
+ throw new TypeError("Capability id is required.");
10
+ }
11
+ return normalized;
12
+ }
13
+
14
+ function createCapabilityRegistry(capabilities = {}) {
15
+ const entries =
16
+ capabilities instanceof Map
17
+ ? capabilities.entries()
18
+ : Object.entries(capabilities ?? {});
19
+ const registry = new Map();
20
+ for (const [capability, adapter] of entries) {
21
+ registry.set(normalizeCapabilityId(capability), adapter);
22
+ }
23
+ return registry;
24
+ }
25
+
6
26
  export function createRuntimeHost(options = {}) {
7
27
  const rows = options.rows ?? createFlatSqlRuntimeStore();
8
28
  const regions = options.regions ?? createRuntimeRegionStore();
9
29
  const moduleRegistry = options.moduleRegistry ?? createModuleRegistry();
30
+ const capabilities = createCapabilityRegistry(options.capabilities);
10
31
 
11
32
  return {
12
33
  rows,
13
34
  regions,
14
35
  moduleRegistry,
36
+ listCapabilities() {
37
+ return Array.from(capabilities.keys()).sort();
38
+ },
39
+ getCapability(capability) {
40
+ return capabilities.get(normalizeCapabilityId(capability)) ?? null;
41
+ },
42
+ registerCapability(capability, adapter) {
43
+ const normalized = normalizeCapabilityId(capability);
44
+ capabilities.set(normalized, adapter);
45
+ return adapter;
46
+ },
47
+ unregisterCapability(capability) {
48
+ return capabilities.delete(normalizeCapabilityId(capability));
49
+ },
50
+ async invokeCapability(operation, params = {}) {
51
+ const normalized = normalizeCapabilityId(operation);
52
+ const separator = normalized.indexOf(".");
53
+ const capabilityId =
54
+ separator >= 0 ? normalized.slice(0, separator) : normalized;
55
+ const methodId =
56
+ separator >= 0 ? normalized.slice(separator + 1) : "invoke";
57
+ const adapter = capabilities.get(capabilityId);
58
+ if (!adapter || typeof adapter !== "object") {
59
+ throw new Error(`Runtime host capability "${capabilityId}" is not registered.`);
60
+ }
61
+ if (typeof adapter[methodId] === "function") {
62
+ return adapter[methodId](params);
63
+ }
64
+ if (typeof adapter.invoke === "function") {
65
+ return adapter.invoke(methodId, params);
66
+ }
67
+ throw new Error(
68
+ `Runtime host capability "${capabilityId}" does not implement "${methodId}" or invoke().`,
69
+ );
70
+ },
15
71
  };
16
72
  }
17
73
 
@@ -1,12 +1,15 @@
1
1
  # AGENTS
2
2
 
3
- Apply the root and `src/AGENTS.md` files first.
3
+ Apply the root and `src/AGENTS.md` files first. This directory contains the
4
+ author-facing harnesses and streaming helpers.
4
5
 
5
- ## Area Ownership
6
+ ## What Authors Should Use From Here
6
7
 
7
- This directory owns SDK test harnesses and runtime-facing helper surfaces used
8
- by examples and downstream consumers: browser harnesses, generic process invoke
9
- clients, runtime-matrix helpers, and resident-module FlatBuffer pumps.
8
+ - `createBrowserModuleHarness(...)` is the browser-side proof path for shared
9
+ standalone artifacts.
10
+ - `createModuleHarness(...)` is the generic process-side harness.
11
+ - `createModuleFlatBufferStreamPump(...)` is the canonical no-JSON path for
12
+ streaming size-prefixed FlatBuffer frames into a resident module instance.
10
13
 
11
14
  ## Harness Rules
12
15
 
@@ -19,15 +22,13 @@ clients, runtime-matrix helpers, and resident-module FlatBuffer pumps.
19
22
  - Avoid hiding stateful behavior inside one-off demos; if a harness contract is
20
23
  real, test it here.
21
24
 
22
- ## Key Files
25
+ ## Key Files To Read
23
26
 
24
27
  - `browserModuleHarness.js`
25
28
  - `moduleFlatbufferStreamPump.js`
26
29
  - `processInvoke.js`
27
30
 
28
- ## Verification
31
+ ## Note
29
32
 
30
- - `node --test test/browser-harness.test.js`
31
- - `node --test test/isomorphic-loader.test.js`
32
- - `npm run test:module-stream`
33
- - `node --test test/process-invoke.test.js`
33
+ Use these helpers from your module repo or app harnesses. Only edit them when
34
+ you are intentionally changing the SDK testing/runtime contract.
@@ -16,7 +16,8 @@ import { createBrowserWasiShim, WasiExitError } from "../host/wasiShim.js";
16
16
  import { createBrowserHost } from "../host/browserHost.js";
17
17
  import {
18
18
  createJsonHostcallBridge,
19
- createNodeHostSyncDispatcher,
19
+ createHostSyncDispatcher,
20
+ dispatchHostOperation,
20
21
  DEFAULT_HOSTCALL_IMPORT_MODULE,
21
22
  } from "../host/abi.js";
22
23
  import {
@@ -90,7 +91,7 @@ async function instantiateBrowserModule(options = {}) {
90
91
  let instance = null;
91
92
  let bridge = null;
92
93
  if (needsHostBridge) {
93
- const dispatch = createNodeHostSyncDispatcher(options.host);
94
+ const dispatch = createHostSyncDispatcher(options.host);
94
95
  bridge = createJsonHostcallBridge({
95
96
  dispatch,
96
97
  getMemory: () => instance.exports.memory,
@@ -238,6 +239,10 @@ export async function createBrowserModuleHarness(options = {}) {
238
239
  return decodePluginInvokeResponse(responseBytes);
239
240
  }
240
241
 
242
+ async function callHost(operation, params = {}) {
243
+ return dispatchHostOperation(host, operation, params);
244
+ }
245
+
241
246
  function readManifest() {
242
247
  const getBytesExport =
243
248
  instance.exports[DefaultManifestExports.pluginBytesSymbol];
@@ -267,6 +272,7 @@ export async function createBrowserModuleHarness(options = {}) {
267
272
  host,
268
273
  bridge,
269
274
  wasi,
275
+ callHost,
270
276
  invoke,
271
277
  invokeRaw,
272
278
  readManifest,
@@ -120,7 +120,8 @@ const CapabilitySurfaceMatrix = Object.freeze({
120
120
  syncHostcall: false,
121
121
  nodeHostApi: true,
122
122
  notes: [
123
- "The coarse network capability maps to host-side HTTP/TCP/UDP/TLS/WebSocket services today.",
123
+ "The coarse network capability maps to async host-side HTTP/TCP/UDP/TLS/WebSocket services.",
124
+ "Browser harnesses can await the same capability surface through BrowserHost.invoke().",
124
125
  "Pure WASI guests cannot reach that surface through the current sync hostcall ABI.",
125
126
  "WasmEdge provides non-blocking socket-oriented extensions that can serve as the standard server-side max-WASI target.",
126
127
  ],
@@ -198,6 +199,42 @@ const CapabilitySurfaceMatrix = Object.freeze({
198
199
  "WasmEdge TLS support provides a practical no-wrapper server-side target for guest HTTPS/TLS logic.",
199
200
  ],
200
201
  }),
202
+ ipfs: Object.freeze({
203
+ capability: "ipfs",
204
+ wasi: false,
205
+ standaloneWasi: false,
206
+ wasmedge: false,
207
+ syncHostcall: false,
208
+ nodeHostApi: true,
209
+ notes: [
210
+ "IPFS access is available through async host adapters and browser/module harness dispatch.",
211
+ "Pure WASM guests cannot reach that surface through the current sync hostcall ABI.",
212
+ ],
213
+ }),
214
+ protocol_handle: Object.freeze({
215
+ capability: "protocol_handle",
216
+ wasi: false,
217
+ standaloneWasi: false,
218
+ wasmedge: false,
219
+ syncHostcall: false,
220
+ nodeHostApi: true,
221
+ notes: [
222
+ "Protocol registration is available through async host adapters and browser/module harness dispatch.",
223
+ "Pure WASM guests cannot reach that surface through the current sync hostcall ABI.",
224
+ ],
225
+ }),
226
+ protocol_dial: Object.freeze({
227
+ capability: "protocol_dial",
228
+ wasi: false,
229
+ standaloneWasi: false,
230
+ wasmedge: false,
231
+ syncHostcall: false,
232
+ nodeHostApi: true,
233
+ notes: [
234
+ "Protocol dialing is available through async host adapters and browser/module harness dispatch.",
235
+ "Pure WASM guests cannot reach that surface through the current sync hostcall ABI.",
236
+ ],
237
+ }),
201
238
  context_read: Object.freeze({
202
239
  capability: "context_read",
203
240
  wasi: false,