-
Tags
+
+ Tags
+
- {parsed.process.tags.map(tag => (
-
+ {parsed.process.tags.map((tag) => (
+
{tag}
))}
@@ -247,18 +329,41 @@ function App() {
{trace.length > 0 && (
-
+
Trace Events ({trace.length})
-
+
{trace.map((event, index) => (
- -
-
{event.event_type} · {event.timestamp}
- {event.data !== undefined && (
-
- {JSON.stringify(event.data, null, 2)}
-
- )}
+ -
+
+ {event.event_type} · {event.timestamp}
+
))}
@@ -267,13 +372,21 @@ function App() {
)}
- {state === 'results' && !parsed && output != null && (
-
- {JSON.stringify(output, null, 2)}
+ {!parsed && result?.rawOutput != null && !displayError && (
+
+ {JSON.stringify(result.rawOutput, null, 2)}
)}
-
)
@@ -282,7 +395,17 @@ function App() {
function OutputField({ label, value }: { label: string; value: string }) {
return (
-
{label}
+
+ {label}
+
{value}
)
diff --git a/apps/traverse-starter/web-react/src/host/embeddedHost.test.ts b/apps/traverse-starter/web-react/src/host/embeddedHost.test.ts
new file mode 100644
index 0000000..c47a630
--- /dev/null
+++ b/apps/traverse-starter/web-react/src/host/embeddedHost.test.ts
@@ -0,0 +1,50 @@
+import { describe, it, expect } from 'vitest'
+import { EmbedderTestDouble } from 'traverse-embedder-web'
+import {
+ createTestEmbedder,
+ submitNote,
+ DEFAULT_WORKFLOW_ID,
+ RUNTIME_MODE_EMBEDDED,
+} from './embeddedHost'
+
+describe('embeddedHost', () => {
+ it('exposes Embedded runtime constants', () => {
+ expect(RUNTIME_MODE_EMBEDDED).toBe('Embedded')
+ expect(DEFAULT_WORKFLOW_ID).toBe('traverse-starter.pipeline')
+ })
+
+ it('submitNote returns scripted pipeline output from test double', () => {
+ const output = {
+ validate: { valid: true, issues: [] as string[] },
+ process: {
+ title: 'Hello',
+ tags: ['a'],
+ noteType: 'note',
+ suggestedNextAction: 'next',
+ status: 'ok',
+ },
+ summarize: { summary: 'Sum', wordCount: 1 },
+ }
+ const embedder = createTestEmbedder(output)
+ const result = submitNote(embedder, 'note text')
+ expect(result.error).toBeNull()
+ expect(result.output?.process.title).toBe('Hello')
+ expect(result.events.length).toBeGreaterThan(0)
+ })
+
+ it('submitNote surfaces scripted execution errors', () => {
+ const embedder = new EmbedderTestDouble({
+ appId: 'traverse-starter',
+ platform: 'web',
+ }).withTargetError(DEFAULT_WORKFLOW_ID, 'execution_failed', 'boom')
+ const result = submitNote(embedder, 'x')
+ expect(result.error).toContain('boom')
+ expect(result.output).toBeNull()
+ })
+
+ it('submitNote surfaces rejected unknown targets', () => {
+ const embedder = new EmbedderTestDouble({ appId: 'traverse-starter', platform: 'web' })
+ const result = submitNote(embedder, 'x')
+ expect(result.error).toMatch(/target_not_found|rejected/)
+ })
+})
diff --git a/apps/traverse-starter/web-react/src/host/embeddedHost.ts b/apps/traverse-starter/web-react/src/host/embeddedHost.ts
new file mode 100644
index 0000000..59b7822
--- /dev/null
+++ b/apps/traverse-starter/web-react/src/host/embeddedHost.ts
@@ -0,0 +1,132 @@
+import type {
+ EmbedderEvent,
+ JsonValue,
+ TraverseEmbedderApi,
+} from 'traverse-embedder-web'
+import { BundleEmbedder, EmbedderTestDouble, FetchBundleLoader } from 'traverse-embedder-web'
+import type { TraverseStarterOutput } from '../client/traverseOutput'
+import { parseOutput } from '../client/traverseOutput'
+
+export const RUNTIME_MODE_EMBEDDED = 'Embedded'
+export const DEFAULT_WORKFLOW_ID = 'traverse-starter.pipeline'
+export const DEFAULT_WORKSPACE = 'local-default'
+export const DEFAULT_APP_ID = 'traverse-starter'
+export const DEFAULT_MANIFEST_PATH = '/bundles/traverse-starter/app.manifest.json'
+
+export type RuntimeStatus = 'starting' | 'ready' | 'unavailable'
+
+export interface TraceEvent {
+ event_type: string
+ timestamp: string
+ data?: unknown
+}
+
+export interface HostRunResult {
+ sessionId: string
+ output: TraverseStarterOutput | null
+ rawOutput: unknown
+ events: TraceEvent[]
+ error: string | null
+}
+
+export type { TraverseEmbedderApi, EmbedderEvent }
+
+/** Builds a deterministic test double for Vitest (spec 068 FR-006). */
+export function createTestEmbedder(output: TraverseStarterOutput): TraverseEmbedderApi {
+ return new EmbedderTestDouble({
+ workspaceId: DEFAULT_WORKSPACE,
+ appId: DEFAULT_APP_ID,
+ appVersion: '1.1.0',
+ platform: 'web',
+ }).withTargetOutput(DEFAULT_WORKFLOW_ID, output as unknown as JsonValue)
+}
+
+/** Production init via FetchBundleLoader. Returns null when the bundle is unavailable. */
+export async function initProductionEmbedder(
+ manifestPath = import.meta.env.VITE_TRAVERSE_STARTER_MANIFEST ?? DEFAULT_MANIFEST_PATH,
+): Promise
{
+ try {
+ return await BundleEmbedder.init({
+ manifestPath,
+ loader: new FetchBundleLoader(),
+ workspaceId: DEFAULT_WORKSPACE,
+ platform: 'web',
+ })
+ } catch {
+ return null
+ }
+}
+
+function errorMessageFromData(data: JsonValue): string | null {
+ if (!data || typeof data !== 'object' || Array.isArray(data)) return null
+ const err = (data as Record).error
+ if (typeof err === 'string') return err
+ if (err && typeof err === 'object' && !Array.isArray(err)) {
+ const message = (err as Record).message
+ if (typeof message === 'string') return message
+ }
+ return null
+}
+
+/** Submit `{ note }` to `traverse-starter.pipeline` and collect terminal output. */
+export function submitNote(embedder: TraverseEmbedderApi, note: string): HostRunResult {
+ const collected: EmbedderEvent[] = []
+ embedder.subscribe((event) => {
+ collected.push(event)
+ })
+
+ const outcome = embedder.submit(DEFAULT_WORKFLOW_ID, { note })
+ if (outcome.status === 'rejected') {
+ return {
+ sessionId: outcome.sessionId ?? 'sess-unknown',
+ output: null,
+ rawOutput: null,
+ events: [],
+ error: outcome.error
+ ? `${outcome.error.code}: ${outcome.error.message}`
+ : 'submit rejected',
+ }
+ }
+
+ const sessionId = outcome.sessionId ?? 'sess-unknown'
+ const events: TraceEvent[] = collected.map((event) => ({
+ event_type: event.event_type,
+ timestamp: String(event.sequence),
+ data: event.data,
+ }))
+
+ for (const event of collected) {
+ if (event.session_id && event.session_id !== sessionId) continue
+ if (event.event_type === 'error') {
+ return {
+ sessionId,
+ output: null,
+ rawOutput: null,
+ events,
+ error: errorMessageFromData(event.data) ?? 'execution failed',
+ }
+ }
+ if (event.event_type === 'capability_result') {
+ const data =
+ event.data && typeof event.data === 'object' && !Array.isArray(event.data)
+ ? (event.data as Record)
+ : null
+ const rawOutput = data?.output ?? null
+ return {
+ sessionId,
+ output: parseOutput(rawOutput),
+ rawOutput,
+ events,
+ error: null,
+ }
+ }
+ }
+
+ return {
+ sessionId,
+ output: null,
+ rawOutput: null,
+ events,
+ error: 'embedder emitted no capability_result',
+ }
+}
diff --git a/ci/coverage-targets.txt b/ci/coverage-targets.txt
index 3647fc9..9e56a80 100644
--- a/ci/coverage-targets.txt
+++ b/ci/coverage-targets.txt
@@ -1,5 +1,4 @@
-# Non-trivial traverse-starter web-react UI logic (hooks + runtime event parsing)
+# Non-trivial traverse-starter web-react UI logic (embedded host + output parsing)
# Paths are matched as substrings against istanbul coverage-summary.json keys.
-hooks/useAppState.ts
+host/embeddedHost.ts
client/traverseOutput.ts
-client/traverseClient.ts
diff --git a/docs/design-language.md b/docs/design-language.md
index 86d722f..4b28865 100644
--- a/docs/design-language.md
+++ b/docs/design-language.md
@@ -62,7 +62,7 @@ The UI renders these fields exactly as the runtime provides them — never compu
| Platform | Path | Status |
|---|---|---|
-| Web (React) | `apps/traverse-starter/web-react/` | Shipped |
+| Web (React) | `apps/traverse-starter/web-react/` | Shipped (embedded) |
| trace-explorer | `apps/trace-explorer/web-react/` | Developer tool (trace-only UI) |
| iOS (SwiftUI) | `apps/traverse-starter/ios-swift/` | Shipped |
| macOS (SwiftUI + AppKit) | `apps/traverse-starter/macos-swift/` | Shipped |
@@ -75,7 +75,7 @@ The UI renders these fields exactly as the runtime provides them — never compu
| Platform | Path | Status |
|---|---|---|
-| Web (React) | `apps/doc-approval/web-react/` | Shipped |
+| Web (React) | `apps/doc-approval/web-react/` | Shipped (embedded; bundle waits on #112) |
| iOS (SwiftUI) | `apps/doc-approval/ios-swift/` | Shipped |
| macOS (SwiftUI + AppKit) | `apps/doc-approval/macos-swift/` | Shipped |
| Android (Jetpack Compose) | `apps/doc-approval/android-compose/` | Shipped |
diff --git a/docs/embedded-runtime-plan.md b/docs/embedded-runtime-plan.md
index c8ca4d2..ac0c540 100644
--- a/docs/embedded-runtime-plan.md
+++ b/docs/embedded-runtime-plan.md
@@ -130,7 +130,7 @@ Until (1–2) exist, platform migration tickets stay **Blocked**.
| [110](https://github.com/traverse-framework/reference-apps/issues/110) | traverse-starter.pipeline multi-capability workflow | Blocked |
| [111](https://github.com/traverse-framework/reference-apps/issues/111) | doc-approval.pipeline multi-capability workflow | Blocked |
| [112](https://github.com/traverse-framework/reference-apps/issues/112) | doc-approval manifests | Blocked |
-| [113](https://github.com/traverse-framework/reference-apps/issues/113) | Embed runtime — web-react | Blocked |
+| [113](https://github.com/traverse-framework/reference-apps/issues/113) | Embed runtime — web-react | Done (this PR) |
| [114](https://github.com/traverse-framework/reference-apps/issues/114) | Embed runtime — Swift (iOS + macOS) | Blocked |
| [115](https://github.com/traverse-framework/reference-apps/issues/115) | Embed runtime — Android | Blocked |
| [116](https://github.com/traverse-framework/reference-apps/issues/116) | Embed runtime — Windows | Blocked |
diff --git a/package-lock.json b/package-lock.json
index 4ca349a..ce423a6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,7 +17,8 @@
"version": "0.0.0",
"dependencies": {
"react": "^19.2.7",
- "react-dom": "^19.2.7"
+ "react-dom": "^19.2.7",
+ "traverse-embedder-web": "file:../../../vendor/traverse-embedder-web"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
@@ -202,7 +203,8 @@
"version": "0.0.0",
"dependencies": {
"react": "^19.2.7",
- "react-dom": "^19.2.7"
+ "react-dom": "^19.2.7",
+ "traverse-embedder-web": "file:../../../vendor/traverse-embedder-web"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
@@ -4209,6 +4211,10 @@
"resolved": "apps/trace-explorer/web-react",
"link": true
},
+ "node_modules/traverse-embedder-web": {
+ "resolved": "vendor/traverse-embedder-web",
+ "link": true
+ },
"node_modules/ts-api-utils": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
@@ -4521,6 +4527,24 @@
"node": ">=18"
}
},
+ "node_modules/wabt": {
+ "version": "1.0.39",
+ "resolved": "https://registry.npmjs.org/wabt/-/wabt-1.0.39.tgz",
+ "integrity": "sha512-ba+dRL/75VQQY7RkU/CgriGbkoWAfS8TDyUlJfJhJ8KhtXgMl5dhNvoPNUcQ9IWRhW8u41glMSuZeTvsYq2rRg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "wasm-decompile": "bin/wasm-decompile",
+ "wasm-interp": "bin/wasm-interp",
+ "wasm-objdump": "bin/wasm-objdump",
+ "wasm-stats": "bin/wasm-stats",
+ "wasm-strip": "bin/wasm-strip",
+ "wasm-validate": "bin/wasm-validate",
+ "wasm2c": "bin/wasm2c",
+ "wasm2wat": "bin/wasm2wat",
+ "wat2wasm": "bin/wat2wasm"
+ }
+ },
"node_modules/web-react": {
"resolved": "apps/traverse-starter/web-react",
"link": true
@@ -4662,6 +4686,46 @@
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
}
+ },
+ "vendor/traverse-embedder-web": {
+ "version": "0.7.0",
+ "license": "Apache-2.0",
+ "devDependencies": {
+ "@types/node": "^22.20.1",
+ "typescript": "^5.6.0",
+ "wabt": "^1.0.39"
+ }
+ },
+ "vendor/traverse-embedder-web/node_modules/@types/node": {
+ "version": "22.20.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz",
+ "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "vendor/traverse-embedder-web/node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "vendor/traverse-embedder-web/node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
}
}
}
diff --git a/scripts/ci/sync_web_starter_bundle.sh b/scripts/ci/sync_web_starter_bundle.sh
new file mode 100755
index 0000000..62ae4a0
--- /dev/null
+++ b/scripts/ci/sync_web_starter_bundle.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+# Sync traverse-starter application bundle into web-react/public for FetchBundleLoader.
+# Requires TRAVERSE_REPO (or sibling ../Traverse) with example WASM + workflows.
+set -euo pipefail
+
+REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+TRAVERSE_REPO="${TRAVERSE_REPO:-$REPO_ROOT/../Traverse}"
+DEST="$REPO_ROOT/apps/traverse-starter/web-react/public/bundles/traverse-starter"
+MANIFEST_SRC="$REPO_ROOT/manifests/traverse-starter"
+
+if [ ! -d "$TRAVERSE_REPO/examples/traverse-starter" ]; then
+ echo "FAIL: TRAVERSE_REPO examples missing: $TRAVERSE_REPO"
+ exit 1
+fi
+
+rm -rf "$DEST"
+mkdir -p "$DEST/components/validate" "$DEST/components/process" "$DEST/components/summarize"
+mkdir -p "$DEST/_traverse/examples/traverse-starter"
+mkdir -p "$DEST/_traverse/workflows/examples/traverse-starter"
+mkdir -p "$DEST/_traverse/contracts/examples/traverse-starter"
+
+cp "$MANIFEST_SRC/app.manifest.json" "$DEST/"
+cp "$MANIFEST_SRC/components/validate/component.manifest.json" "$DEST/components/validate/"
+cp "$MANIFEST_SRC/components/process/component.manifest.json" "$DEST/components/process/"
+cp "$MANIFEST_SRC/components/summarize/component.manifest.json" "$DEST/components/summarize/"
+
+rsync -a "$TRAVERSE_REPO/examples/traverse-starter/" "$DEST/_traverse/examples/traverse-starter/"
+rsync -a "$TRAVERSE_REPO/workflows/examples/traverse-starter/" "$DEST/_traverse/workflows/examples/traverse-starter/"
+rsync -a "$TRAVERSE_REPO/contracts/examples/traverse-starter/" "$DEST/_traverse/contracts/examples/traverse-starter/"
+
+echo "OK: synced bundle → $DEST"
diff --git a/vendor/traverse-embedder-web/ORIGIN.md b/vendor/traverse-embedder-web/ORIGIN.md
new file mode 100644
index 0000000..a54b071
--- /dev/null
+++ b/vendor/traverse-embedder-web/ORIGIN.md
@@ -0,0 +1,2 @@
+# Vendored from traverse-framework/Traverse packages/web/TraverseEmbedder @ 0.7.0 (spec 068).
+# Refresh: cd $TRAVERSE_REPO/packages/web/TraverseEmbedder && npm i && npm run build && rsync -a --delete package.json README.md dist/ /vendor/traverse-embedder-web/
diff --git a/vendor/traverse-embedder-web/README.md b/vendor/traverse-embedder-web/README.md
new file mode 100644
index 0000000..f829a99
--- /dev/null
+++ b/vendor/traverse-embedder-web/README.md
@@ -0,0 +1,128 @@
+# traverse-embedder-web
+
+Public Traverse platform embedder SDK for Web/TypeScript clients — the Web
+row of spec `068-public-platform-embedder-packages`, exposing the
+[`embedder-api/1.0.0`](../../../specs/057-embeddable-runtime-host/embedder-api-1.0.0.json)
+operation surface. Production execution requires no `traverse-cli serve`
+sidecar and no `.traverse/server.json` discovery.
+
+## Runtime-WASM execution
+
+Unlike the native platform embedders, which embed Wasmtime to execute
+bundled capability artifacts, the browser *is already* a WebAssembly host.
+`BundleEmbedder` compiles each bundled capability module with
+`WebAssembly.compile` once at `init`, validates its imports against the
+Traverse Host ABI whitelist (only `wasi_snapshot_preview1.fd_read` /
+`fd_write` / `proc_exit` and the reserved `traverse_host.*` imports — no
+filesystem, no network, no environment, deny-by-default, matching the native
+`WasmExecutor`), and instantiates + invokes it synchronously per `submit`
+through a minimal WASI `preview1` shim that pipes JSON stdin/stdout exactly
+like the native executor. Workflow execution supports linear,
+`direct`-triggered pipelines (the shape used by every bundled example
+workflow today); event-driven/conditional edges are rejected deterministically
+at `init` rather than silently mis-executed.
+
+```ts
+import { BundleEmbedder, FetchBundleLoader } from "traverse-embedder-web";
+
+const embedder = await BundleEmbedder.init({
+ manifestPath: "/bundles/my-app/app.manifest.json",
+ loader: new FetchBundleLoader(),
+ platform: "web",
+});
+embedder.subscribe((event) => console.log(event));
+embedder.submit("my-app.process", { note: "hello" });
+```
+
+See `examples/react-integration/` for a working React page that loads the
+checked-in `traverse-starter` bundle straight from the repository and
+executes it with no `traverse-cli serve` process running.
+
+## Operations
+
+| `embedder-api/1.0.0` | TypeScript surface |
+| --- | --- |
+| `runtime.submit` | `TraverseEmbedderApi.submit(targetId, input)` |
+| `runtime.subscribe` | `TraverseEmbedderApi.subscribe(callback)` (ordered, replayed) |
+| `runtime.shutdown` | `TraverseEmbedderApi.shutdown()` |
+| `compatible.start` | `TraverseEmbedderApi.startCompatible(capabilityId, input)` |
+| `compatible.stop` | `TraverseEmbedderApi.stopCompatible(capabilityId, instanceId?)` |
+| `compatible.kill` | `TraverseEmbedderApi.killCompatible(capabilityId, instanceId?)` |
+
+Events are JSON values with the same envelope as every Traverse embedder
+package (`kind: "embedder_event"`, `event_id`, `sequence`, `event_type`,
+`workspace_id`, `app_id`, `session_id`, `data`) and the same deterministic
+identifier scheme (`sess-*`, `req-*`, `evt-*`, `inst-*`), so the same
+operations produce identical event JSON on every platform.
+
+## Bundle compatibility
+
+`validateBundleCompatibility(appManifest)` parses and deterministically
+validates an application bundle manifest (spec
+`044-application-bundle-manifest`): supported `schema_version` values
+(`1.0.0`), component identity fields, and `sha256:` digest metadata.
+`verifyArtifactDigest(bytes, declaredDigest, label)` verifies bundled
+artifact bytes with WebCrypto. Incompatible bundles are rejected with stable
+error codes (`unsupported_bundle_schema`, `bundle_load_failed`) and never
+fall back to a sidecar (spec 068 NFR-001).
+
+## Test double
+
+`EmbedderTestDouble` implements `TraverseEmbedderApi` with scripted results,
+the shared event envelope, deterministic identifiers, the full
+compatible-capability lifecycle (including the `platforms[]` allowlist
+guard), and idempotent shutdown — for host tests without WASM or network
+(spec 068 FR-006):
+
+```ts
+import { EmbedderTestDouble } from "traverse-embedder-web";
+
+const embedder = new EmbedderTestDouble({ appId: "my-app", platform: "web" })
+ .withTargetOutput("my-app.process", { status: "processed" })
+ .withCompatibleTarget("my-app.render", ["web"]);
+
+embedder.subscribe((event) => console.log(event));
+embedder.submit("my-app.process", { note: "hello" });
+embedder.shutdown();
+```
+
+## Error mapping
+
+Boundary failures use stable `EmbedderErrorCode` values —
+`bundle_load_failed`, `unsupported_bundle_schema`, `runtime_stopped`,
+`target_not_found`, `compatible_lifecycle_required`,
+`capability_not_compatible`, `platform_not_supported`, `instance_not_found`,
+`instance_not_running` — identical to the Rust `traverse-embedder` crate.
+Runtime execution failures surface inside `error` events with the runtime's
+stable snake_case codes. Secrets never appear in events, errors, or evidence
+(spec 068 NFR-004).
+
+## Compatibility and upgrade policy
+
+- Embedder API `1.0.0`; a new IDL version requires a new conformance suite
+ revision and a release stating the new version in its evidence.
+- Supported bundle schema versions: `1.0.0`.
+- Semantic versioning; the package versions in lockstep with the Traverse
+ workspace.
+
+## Release evidence
+
+`releaseEvidence()` returns JSON recording the package name/version, the
+runtime implementation (`browser-webassembly` for `BundleEmbedder`,
+`test-double` for `EmbedderTestDouble`), embedder API + conformance
+versions, supported bundle schemas, bundle identity, and the sha-256 digest
+of every bundled WASM component (spec 068 FR-008, NFR-002).
+
+## Development
+
+```bash
+npm install
+npm test # builds with tsc, then runs the node:test suite
+```
+
+`npm test` compiles a set of real WASI capability modules from WebAssembly
+Text format via `wabt` (a devDependency, mirroring the Rust crate's `wat`
+crate test fixtures) and runs `BundleEmbedder` against them — including one
+test that loads and executes the real, checked-in `apps/traverse-starter`
+bundle end to end — so the browser execution engine is exercised for real,
+not mocked.
diff --git a/vendor/traverse-embedder-web/dist/bundleEmbedder.d.ts b/vendor/traverse-embedder-web/dist/bundleEmbedder.d.ts
new file mode 100644
index 0000000..6eff31b
--- /dev/null
+++ b/vendor/traverse-embedder-web/dist/bundleEmbedder.d.ts
@@ -0,0 +1,35 @@
+import type { BundleLoader } from "./bundleLoader.js";
+import type { CompatibleLifecycleOutcome, CompatibleStartOutcome, EventCallback, JsonValue, ShutdownOutcome, SubmitOutcome, TraverseEmbedderApi } from "./types.js";
+/** Configuration for `BundleEmbedder.init` (`runtime.init` input). */
+export interface BundleEmbedderConfig {
+ /** Path or URL to the application bundle's `app.manifest.json`. */
+ readonly manifestPath: string;
+ /** Loader used to fetch bundle files (browser: `FetchBundleLoader`). */
+ readonly loader: BundleLoader;
+ /** Workspace identity recorded on events. Defaults to `local-default`. */
+ readonly workspaceId?: string;
+ /** Platform identity checked against compatible-capability allowlists. */
+ readonly platform?: string;
+}
+export declare class BundleEmbedder implements TraverseEmbedderApi {
+ private readonly core;
+ private readonly wasmTargets;
+ private readonly workflowTargets;
+ private readonly wasmComponentEvidence;
+ private constructor();
+ /**
+ * `runtime.init`: load, digest-verify, host-ABI-validate, and compile the
+ * application bundle. Rejects deterministically with a `BundleRejectedError`
+ * and never falls back to a sidecar (spec 068 NFR-001).
+ */
+ static init(config: BundleEmbedderConfig): Promise;
+ submit(targetId: string, input: JsonValue): SubmitOutcome;
+ private submitCapability;
+ private submitWorkflow;
+ subscribe(callback: EventCallback): void;
+ startCompatible(capabilityId: string, input: JsonValue): CompatibleStartOutcome;
+ stopCompatible(capabilityId: string, instanceId?: string | null): CompatibleLifecycleOutcome;
+ killCompatible(capabilityId: string, instanceId?: string | null): CompatibleLifecycleOutcome;
+ shutdown(): ShutdownOutcome;
+ releaseEvidence(): JsonValue;
+}
diff --git a/vendor/traverse-embedder-web/dist/bundleEmbedder.js b/vendor/traverse-embedder-web/dist/bundleEmbedder.js
new file mode 100644
index 0000000..05798b1
--- /dev/null
+++ b/vendor/traverse-embedder-web/dist/bundleEmbedder.js
@@ -0,0 +1,499 @@
+/**
+ * Production embedder: loads an application-owned bundle and executes
+ * bundled WASM capabilities directly in the browser's native WebAssembly
+ * host — no `traverse-cli serve`, no server round trip, no nested WASM
+ * engine (spec 068 FR-002).
+ *
+ * Where the native Rust `traverse-embedder` crate embeds Wasmtime to run
+ * bundled capability artifacts, the browser *is already* a WebAssembly
+ * host: `BundleEmbedder` compiles each bundled capability module with
+ * `WebAssembly.compile` once at `init`, validates its imports against the
+ * Traverse Host ABI whitelist (`hostAbi.ts`), and instantiates + invokes it
+ * synchronously per `submit` through a minimal WASI `preview1` shim
+ * (`wasi.ts`) that pipes JSON stdin/stdout exactly like the native
+ * `WasmExecutor`.
+ *
+ * Workflow execution supports linear, `direct`-triggered pipelines only
+ * (the shape used by every bundled example workflow today: `analyze` ->
+ * `recommend`, single-node `process`). Event-driven / conditional workflow
+ * edges (specs 052/053) are out of scope for this package slice and are
+ * rejected deterministically at `init` rather than silently mis-executed.
+ */
+import { EmbedderCore } from "./core.js";
+import { BundleRejectedError, asRecord, optionalString, requiredString, validateBundleCompatibility, verifyArtifactDigest, SHA256_DIGEST_PATTERN, } from "./bundleValidation.js";
+import { findUnauthorizedImport } from "./hostAbi.js";
+import { WasiExit, WasiPipes, createWasiPreview1Imports } from "./wasi.js";
+import { embedderError, runtimeStoppedError } from "./types.js";
+function loadFailure(message) {
+ return new BundleRejectedError(embedderError("bundle_load_failed", message));
+}
+/** Copies `bytes` into a fresh, non-shared `ArrayBuffer` for WebAssembly APIs. */
+function toArrayBuffer(bytes) {
+ const copy = new Uint8Array(bytes.byteLength);
+ copy.set(bytes);
+ return copy.buffer;
+}
+function stringArray(value, context) {
+ if (value === undefined) {
+ return [];
+ }
+ if (!Array.isArray(value)) {
+ throw loadFailure(`${context} must be an array`);
+ }
+ return value.map((entry, index) => {
+ if (typeof entry !== "string") {
+ throw loadFailure(`${context}[${index}] must be a string`);
+ }
+ return entry;
+ });
+}
+function initialWorkflowState(input) {
+ const record = asRecord(input);
+ return record !== null ? { ...record } : { input };
+}
+function buildNodeInput(state, keys) {
+ const input = {};
+ for (const key of keys) {
+ const value = state[key];
+ if (value !== undefined) {
+ input[key] = value;
+ }
+ }
+ return input;
+}
+function applyNodeOutput(state, node, output) {
+ const record = asRecord(output);
+ if (record === null) {
+ return;
+ }
+ for (const key of node.toWorkflowState) {
+ const value = record[key];
+ if (value !== undefined) {
+ state[key] = value;
+ }
+ }
+ if (node.publishToStateAs !== null) {
+ state[node.publishToStateAs] = output;
+ }
+}
+function projectWorkflowOutput(state, projection) {
+ if (projection.length === 0) {
+ return { ...state };
+ }
+ const projected = {};
+ for (const key of projection) {
+ const value = state[key];
+ if (value !== undefined) {
+ projected[key] = value;
+ }
+ }
+ return projected;
+}
+export class BundleEmbedder {
+ core;
+ wasmTargets;
+ workflowTargets;
+ wasmComponentEvidence;
+ constructor(core, wasmTargets, workflowTargets, wasmComponentEvidence) {
+ this.core = core;
+ this.wasmTargets = wasmTargets;
+ this.workflowTargets = workflowTargets;
+ this.wasmComponentEvidence = wasmComponentEvidence;
+ }
+ /**
+ * `runtime.init`: load, digest-verify, host-ABI-validate, and compile the
+ * application bundle. Rejects deterministically with a `BundleRejectedError`
+ * and never falls back to a sidecar (spec 068 NFR-001).
+ */
+ static async init(config) {
+ const { manifestPath, loader } = config;
+ let manifestText;
+ try {
+ manifestText = await loader.loadText(manifestPath);
+ }
+ catch (error) {
+ throw loadFailure(`failed to load application bundle manifest: ${String(error)}`);
+ }
+ const summary = validateBundleCompatibility(manifestText);
+ const wasmTargets = new Map();
+ const compatibleTargets = new Map();
+ const wasmComponentEvidence = [];
+ for (const component of summary.components) {
+ const componentManifestPath = loader.resolve(manifestPath, component.manifestPath);
+ let componentManifestText;
+ try {
+ componentManifestText = await loader.loadText(componentManifestPath);
+ }
+ catch (error) {
+ throw loadFailure(`failed to load component manifest '${componentManifestPath}': ${String(error)}`);
+ }
+ let parsed;
+ try {
+ parsed = JSON.parse(componentManifestText);
+ }
+ catch (error) {
+ throw loadFailure(`component manifest '${componentManifestPath}' is not valid JSON: ${String(error)}`);
+ }
+ const record = asRecord(parsed);
+ if (record === null) {
+ throw loadFailure(`component manifest '${componentManifestPath}' must be a JSON object`);
+ }
+ const context = `component manifest '${componentManifestPath}'`;
+ const capabilityId = requiredString(record, "capability_id", context);
+ const capabilityVersion = requiredString(record, "capability_version", context);
+ const executionMode = optionalString(record, "execution_mode") ?? "wasm";
+ if (executionMode === "compatible") {
+ const platforms = stringArray(record["platforms"], `${context} platforms`);
+ if (platforms.length === 0) {
+ throw loadFailure(`${context} declares execution_mode 'compatible' but no platforms`);
+ }
+ compatibleTargets.set(capabilityId, platforms);
+ continue;
+ }
+ if (executionMode !== "wasm") {
+ throw loadFailure(`${context} declares unsupported execution_mode '${executionMode}'`);
+ }
+ const wasmDigest = requiredString(record, "wasm_digest", context);
+ if (!SHA256_DIGEST_PATTERN.test(wasmDigest)) {
+ throw loadFailure(`${context} declares invalid wasm_digest metadata '${wasmDigest}'`);
+ }
+ if (wasmDigest.toLowerCase() !== component.digest.toLowerCase()) {
+ throw loadFailure(`${context} wasm_digest does not match the app manifest's declared component digest`);
+ }
+ const wasmBinaryPath = requiredString(record, "wasm_binary_path", context);
+ const wasmPath = loader.resolve(componentManifestPath, wasmBinaryPath);
+ let wasmBytes;
+ try {
+ wasmBytes = await loader.loadBytes(wasmPath);
+ }
+ catch (error) {
+ throw loadFailure(`failed to load WASM artifact '${wasmPath}': ${String(error)}`);
+ }
+ await verifyArtifactDigest(wasmBytes, wasmDigest, `component '${capabilityId}' artifact`);
+ let module;
+ try {
+ module = await WebAssembly.compile(toArrayBuffer(wasmBytes));
+ }
+ catch (error) {
+ throw loadFailure(`component '${capabilityId}' WASM artifact failed to compile: ${String(error)}`);
+ }
+ const unauthorized = findUnauthorizedImport(module);
+ if (unauthorized !== null) {
+ throw loadFailure(`component '${capabilityId}' imports unauthorized host function ` +
+ `'${unauthorized.module}.${unauthorized.name}'; Traverse Host ABI 1.0.0 permits ` +
+ "only the whitelisted stdio and traverse_host imports");
+ }
+ wasmTargets.set(capabilityId, { capabilityVersion, digest: wasmDigest, module });
+ wasmComponentEvidence.push({
+ component_id: component.componentId,
+ capability_id: capabilityId,
+ wasm_digest: wasmDigest,
+ });
+ }
+ const workflowTargets = new Map();
+ for (const workflowRef of summary.workflows) {
+ const workflowPath = loader.resolve(manifestPath, workflowRef.path);
+ let workflowText;
+ try {
+ workflowText = await loader.loadText(workflowPath);
+ }
+ catch (error) {
+ throw loadFailure(`failed to load workflow definition '${workflowPath}': ${String(error)}`);
+ }
+ let parsed;
+ try {
+ parsed = JSON.parse(workflowText);
+ }
+ catch (error) {
+ throw loadFailure(`workflow definition '${workflowPath}' is not valid JSON: ${String(error)}`);
+ }
+ const record = asRecord(parsed);
+ if (record === null) {
+ throw loadFailure(`workflow definition '${workflowPath}' must be a JSON object`);
+ }
+ const context = `workflow definition '${workflowPath}'`;
+ const startNode = requiredString(record, "start_node", context);
+ const outputProjection = stringArray(record["output_projection"], `${context} output_projection`);
+ const nodesValue = record["nodes"];
+ if (!Array.isArray(nodesValue)) {
+ throw loadFailure(`${context} requires a 'nodes' array`);
+ }
+ const nodes = new Map();
+ for (const [index, entry] of nodesValue.entries()) {
+ const node = asRecord(entry);
+ if (node === null) {
+ throw loadFailure(`${context} nodes[${index}] must be a JSON object`);
+ }
+ const nodeContext = `${context} nodes[${index}]`;
+ const nodeId = requiredString(node, "node_id", nodeContext);
+ const input = asRecord(node["input"]);
+ const output = asRecord(node["output"]);
+ nodes.set(nodeId, {
+ nodeId,
+ capabilityId: requiredString(node, "capability_id", nodeContext),
+ capabilityVersion: requiredString(node, "capability_version", nodeContext),
+ fromWorkflowInput: stringArray(input?.["from_workflow_input"], `${nodeContext}.input.from_workflow_input`),
+ toWorkflowState: stringArray(output?.["to_workflow_state"], `${nodeContext}.output.to_workflow_state`),
+ publishToStateAs: output !== null ? optionalString(output, "publish_to_state_as") : null,
+ });
+ }
+ const edgesValue = record["edges"];
+ const nextByFrom = new Map();
+ if (Array.isArray(edgesValue)) {
+ for (const [index, entry] of edgesValue.entries()) {
+ const edge = asRecord(entry);
+ if (edge === null) {
+ throw loadFailure(`${context} edges[${index}] must be a JSON object`);
+ }
+ const edgeContext = `${context} edges[${index}]`;
+ const trigger = optionalString(edge, "trigger") ?? "direct";
+ if (trigger !== "direct") {
+ throw loadFailure(`${edgeContext} uses trigger '${trigger}'; this package version supports only ` +
+ "'direct'-triggered linear pipelines");
+ }
+ const from = requiredString(edge, "from", edgeContext);
+ const to = requiredString(edge, "to", edgeContext);
+ if (nextByFrom.has(from)) {
+ throw loadFailure(`${edgeContext}: node '${from}' already has an outgoing direct edge; ` +
+ "branching pipelines are not supported by this package version");
+ }
+ nextByFrom.set(from, to);
+ }
+ }
+ workflowTargets.set(workflowRef.workflowId, {
+ version: workflowRef.workflowVersion,
+ nodes,
+ nextByFrom,
+ startNode,
+ outputProjection,
+ });
+ }
+ const core = new EmbedderCore(config.workspaceId ?? "local-default", summary.appId, summary.appVersion, config.platform ?? "web", compatibleTargets);
+ return new BundleEmbedder(core, wasmTargets, workflowTargets, wasmComponentEvidence);
+ }
+ submit(targetId, input) {
+ if (this.core.stopped) {
+ return this.core.rejectedSubmit(targetId, runtimeStoppedError());
+ }
+ if (this.workflowTargets.has(targetId)) {
+ return this.submitWorkflow(targetId, input);
+ }
+ if (this.wasmTargets.has(targetId)) {
+ return this.submitCapability(targetId, input);
+ }
+ if (this.core.compatibleTargets.has(targetId)) {
+ return this.core.rejectedSubmit(targetId, embedderError("compatible_lifecycle_required", `capability '${targetId}' is a compatible-mode capability; use compatible.start/stop/kill`));
+ }
+ return this.core.rejectedSubmit(targetId, embedderError("target_not_found", `'${targetId}' is neither a bundled workflow nor a bundled capability`));
+ }
+ submitCapability(targetId, input) {
+ const target = this.wasmTargets.get(targetId);
+ if (target === undefined) {
+ return this.core.rejectedSubmit(targetId, embedderError("target_not_found", `'${targetId}' is not a bundled capability`));
+ }
+ const sessionId = this.core.nextSessionId();
+ const requestId = this.core.nextRequestId();
+ const executionId = `exec_${requestId}`;
+ this.core.emit("capability_invoked", sessionId, {
+ execution_id: executionId,
+ capability_id: targetId,
+ capability_version: target.capabilityVersion,
+ });
+ const result = executeWasmModule(target, input);
+ if (result.ok) {
+ this.core.emit("capability_result", sessionId, {
+ execution_id: executionId,
+ capability_id: targetId,
+ status: "completed",
+ output: result.output,
+ });
+ }
+ else {
+ this.core.emit("error", sessionId, {
+ execution_id: executionId,
+ capability_id: targetId,
+ status: "error",
+ error: { code: result.code, message: result.message, details: {} },
+ });
+ }
+ return { sessionId, status: "accepted", error: null };
+ }
+ submitWorkflow(targetId, input) {
+ const workflow = this.workflowTargets.get(targetId);
+ if (workflow === undefined) {
+ return this.core.rejectedSubmit(targetId, embedderError("target_not_found", `'${targetId}' is not a bundled workflow`));
+ }
+ const sessionId = this.core.nextSessionId();
+ const requestId = this.core.nextRequestId();
+ const state = initialWorkflowState(input);
+ const steps = [];
+ let failure = null;
+ let stepIndex = 0;
+ let currentNodeId = workflow.startNode;
+ while (currentNodeId !== undefined) {
+ const node = workflow.nodes.get(currentNodeId);
+ if (node === undefined) {
+ failure = {
+ code: "execution_failed",
+ message: `workflow node '${currentNodeId}' could not be resolved during traversal`,
+ };
+ break;
+ }
+ const target = this.wasmTargets.get(node.capabilityId);
+ if (target === undefined) {
+ steps.push({
+ stepIndex,
+ nodeId: node.nodeId,
+ capabilityId: node.capabilityId,
+ capabilityVersion: node.capabilityVersion,
+ status: "failed",
+ });
+ failure = {
+ code: "capability_not_found",
+ message: `capability '${node.capabilityId}' is not a bundled WASM capability`,
+ };
+ break;
+ }
+ const nodeInput = buildNodeInput(state, node.fromWorkflowInput);
+ const result = executeWasmModule(target, nodeInput);
+ if (!result.ok) {
+ steps.push({
+ stepIndex,
+ nodeId: node.nodeId,
+ capabilityId: node.capabilityId,
+ capabilityVersion: node.capabilityVersion,
+ status: "failed",
+ });
+ failure = { code: result.code, message: result.message };
+ break;
+ }
+ steps.push({
+ stepIndex,
+ nodeId: node.nodeId,
+ capabilityId: node.capabilityId,
+ capabilityVersion: node.capabilityVersion,
+ status: "completed",
+ });
+ applyNodeOutput(state, node, result.output);
+ stepIndex += 1;
+ currentNodeId = workflow.nextByFrom.get(node.nodeId);
+ }
+ for (const step of steps) {
+ this.core.emit("capability_invoked", sessionId, {
+ request_id: requestId,
+ workflow_id: targetId,
+ workflow_version: workflow.version,
+ step_index: step.stepIndex,
+ node_id: step.nodeId,
+ capability_id: step.capabilityId,
+ capability_version: step.capabilityVersion,
+ status: step.status,
+ });
+ }
+ if (failure === null) {
+ this.core.emit("capability_result", sessionId, {
+ request_id: requestId,
+ workflow_id: targetId,
+ workflow_version: workflow.version,
+ status: "completed",
+ output: projectWorkflowOutput(state, workflow.outputProjection),
+ });
+ }
+ else {
+ this.core.emit("error", sessionId, {
+ request_id: requestId,
+ workflow_id: targetId,
+ workflow_version: workflow.version,
+ status: "error",
+ error: { code: failure.code, message: failure.message, details: {} },
+ });
+ }
+ return { sessionId, status: "accepted", error: null };
+ }
+ subscribe(callback) {
+ this.core.subscribe(callback);
+ }
+ startCompatible(capabilityId, input) {
+ return this.core.startCompatible(capabilityId, input);
+ }
+ stopCompatible(capabilityId, instanceId = null) {
+ return this.core.transitionCompatible(capabilityId, instanceId, "stopped");
+ }
+ killCompatible(capabilityId, instanceId = null) {
+ return this.core.transitionCompatible(capabilityId, instanceId, "killed");
+ }
+ shutdown() {
+ return this.core.shutdown();
+ }
+ releaseEvidence() {
+ return this.core.evidence("browser-webassembly", [...this.wasmComponentEvidence]);
+ }
+}
+/**
+ * Instantiates and invokes one bundled WASM capability module synchronously
+ * against an already-compiled, host-ABI-validated `WebAssembly.Module`,
+ * piping `input` as WASI stdin JSON and parsing WASI stdout as the output
+ * JSON — the same contract as the native `WasmExecutor` (spec 057).
+ */
+function executeWasmModule(target, input) {
+ const inputBytes = new TextEncoder().encode(JSON.stringify(input));
+ const pipes = new WasiPipes(inputBytes);
+ const memoryRef = { memory: null };
+ const importObject = {
+ wasi_snapshot_preview1: createWasiPreview1Imports(pipes, memoryRef),
+ };
+ let instance;
+ try {
+ instance = new WebAssembly.Instance(target.module, importObject);
+ }
+ catch (error) {
+ return {
+ ok: false,
+ output: null,
+ code: "constraint_violated",
+ message: `module instantiation failed: ${String(error)}`,
+ };
+ }
+ const exportedMemory = instance.exports["memory"];
+ memoryRef.memory = exportedMemory instanceof WebAssembly.Memory ? exportedMemory : null;
+ const entry = instance.exports["_start"] ?? instance.exports[""];
+ if (typeof entry !== "function") {
+ return {
+ ok: false,
+ output: null,
+ code: "constraint_violated",
+ message: "module has no WASI command entry point ('_start')",
+ };
+ }
+ let trapped = null;
+ try {
+ entry();
+ }
+ catch (error) {
+ if (error instanceof WasiExit) {
+ if (error.code !== 0) {
+ trapped = { code: "execution_failed", message: `module exited with code ${error.code}` };
+ }
+ }
+ else {
+ trapped = { code: "execution_failed", message: `module trapped: ${String(error)}` };
+ }
+ }
+ if (trapped !== null) {
+ return { ok: false, output: null, ...trapped };
+ }
+ const rawOutput = pipes.stdoutBytes();
+ const rawText = new TextDecoder().decode(rawOutput);
+ try {
+ const output = JSON.parse(rawText);
+ return { ok: true, output, code: "", message: "" };
+ }
+ catch (error) {
+ return {
+ ok: false,
+ output: null,
+ code: "output_deserialization_failed",
+ message: `stdout is not valid JSON: ${String(error)} — raw: ${rawText}`,
+ };
+ }
+}
diff --git a/vendor/traverse-embedder-web/dist/bundleLoader.d.ts b/vendor/traverse-embedder-web/dist/bundleLoader.d.ts
new file mode 100644
index 0000000..014b912
--- /dev/null
+++ b/vendor/traverse-embedder-web/dist/bundleLoader.d.ts
@@ -0,0 +1,36 @@
+/**
+ * Loads application bundle files (manifests, contracts, WASM artifacts) by
+ * relative path. `BundleEmbedder` depends only on this interface, so a
+ * browser host supplies `FetchBundleLoader` (production) and a Node host
+ * (tests, Electron main-process embedding) supplies `NodeFsBundleLoader` —
+ * neither is baked into the public API surface (spec 068: no `traverse-cli
+ * serve` dependency in the production path).
+ */
+export interface BundleLoader {
+ /** Resolves `relativePath` against the directory containing `basePath`. */
+ resolve(basePath: string, relativePath: string): string;
+ /** Loads a UTF-8 text file (manifests, contracts, workflow definitions). */
+ loadText(path: string): Promise;
+ /** Loads raw bytes (WASM artifacts). */
+ loadBytes(path: string): Promise;
+}
+/**
+ * Browser bundle loader: resolves paths as URLs relative to the manifest
+ * URL and loads them via `fetch`. This is the production loader — no
+ * `traverse-cli serve` sidecar is involved.
+ */
+export declare class FetchBundleLoader implements BundleLoader {
+ resolve(basePath: string, relativePath: string): string;
+ loadText(path: string): Promise;
+ loadBytes(path: string): Promise;
+}
+/**
+ * Node.js filesystem bundle loader, for tests and non-browser hosts
+ * (Electron main process, server-side prerendering). Uses dynamic `import`
+ * so `node:fs` is never a static dependency of the browser entry point.
+ */
+export declare class NodeFsBundleLoader implements BundleLoader {
+ resolve(basePath: string, relativePath: string): string;
+ loadText(path: string): Promise;
+ loadBytes(path: string): Promise;
+}
diff --git a/vendor/traverse-embedder-web/dist/bundleLoader.js b/vendor/traverse-embedder-web/dist/bundleLoader.js
new file mode 100644
index 0000000..dcc604b
--- /dev/null
+++ b/vendor/traverse-embedder-web/dist/bundleLoader.js
@@ -0,0 +1,74 @@
+/**
+ * Loads application bundle files (manifests, contracts, WASM artifacts) by
+ * relative path. `BundleEmbedder` depends only on this interface, so a
+ * browser host supplies `FetchBundleLoader` (production) and a Node host
+ * (tests, Electron main-process embedding) supplies `NodeFsBundleLoader` —
+ * neither is baked into the public API surface (spec 068: no `traverse-cli
+ * serve` dependency in the production path).
+ */
+function dirname(path) {
+ const index = path.lastIndexOf("/");
+ return index === -1 ? "" : path.slice(0, index);
+}
+function joinPosix(base, relative) {
+ if (relative.startsWith("/") || /^[a-zA-Z]+:/.test(relative)) {
+ return relative;
+ }
+ const segments = `${base}/${relative}`.split("/");
+ const resolved = [];
+ for (const segment of segments) {
+ if (segment === "" || segment === ".") {
+ continue;
+ }
+ if (segment === "..") {
+ resolved.pop();
+ continue;
+ }
+ resolved.push(segment);
+ }
+ return (base.startsWith("/") ? "/" : "") + resolved.join("/");
+}
+/**
+ * Browser bundle loader: resolves paths as URLs relative to the manifest
+ * URL and loads them via `fetch`. This is the production loader — no
+ * `traverse-cli serve` sidecar is involved.
+ */
+export class FetchBundleLoader {
+ resolve(basePath, relativePath) {
+ const documentBase = typeof document !== "undefined" ? document.baseURI : globalThis.location.href;
+ return new URL(relativePath, new URL(basePath, documentBase)).toString();
+ }
+ async loadText(path) {
+ const response = await fetch(path);
+ if (!response.ok) {
+ throw new Error(`failed to fetch '${path}': HTTP ${response.status}`);
+ }
+ return response.text();
+ }
+ async loadBytes(path) {
+ const response = await fetch(path);
+ if (!response.ok) {
+ throw new Error(`failed to fetch '${path}': HTTP ${response.status}`);
+ }
+ return new Uint8Array(await response.arrayBuffer());
+ }
+}
+/**
+ * Node.js filesystem bundle loader, for tests and non-browser hosts
+ * (Electron main process, server-side prerendering). Uses dynamic `import`
+ * so `node:fs` is never a static dependency of the browser entry point.
+ */
+export class NodeFsBundleLoader {
+ resolve(basePath, relativePath) {
+ return joinPosix(dirname(basePath), relativePath);
+ }
+ async loadText(path) {
+ const { readFile } = await import("node:fs/promises");
+ return readFile(path, "utf8");
+ }
+ async loadBytes(path) {
+ const { readFile } = await import("node:fs/promises");
+ const buffer = await readFile(path);
+ return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
+ }
+}
diff --git a/vendor/traverse-embedder-web/dist/bundleValidation.d.ts b/vendor/traverse-embedder-web/dist/bundleValidation.d.ts
new file mode 100644
index 0000000..25d5d27
--- /dev/null
+++ b/vendor/traverse-embedder-web/dist/bundleValidation.d.ts
@@ -0,0 +1,54 @@
+import type { EmbedderError, JsonValue } from "./types.js";
+/** Thrown when a bundle is rejected at the embedder boundary. */
+export declare class BundleRejectedError extends Error {
+ readonly embedderError: EmbedderError;
+ constructor(error: EmbedderError);
+}
+/** One bundled component reference parsed from the app manifest. */
+export interface BundleComponentSummary {
+ readonly componentId: string;
+ readonly version: string;
+ readonly digest: string;
+ readonly manifestPath: string;
+}
+/** One bundled workflow reference parsed from the app manifest. */
+export interface BundleWorkflowSummary {
+ readonly workflowId: string;
+ readonly workflowVersion: string;
+ readonly path: string;
+}
+/** Deterministic bundle compatibility summary (spec 068 NFR-001). */
+export interface BundleCompatibility {
+ readonly appId: string;
+ readonly appVersion: string;
+ readonly schemaVersion: string;
+ readonly components: readonly BundleComponentSummary[];
+ readonly workflowIds: readonly string[];
+ readonly workflows: readonly BundleWorkflowSummary[];
+}
+export declare function asRecord(value: JsonValue | undefined): {
+ [key: string]: JsonValue;
+} | null;
+export declare function requiredString(record: {
+ [key: string]: JsonValue;
+}, key: string, context: string): string;
+export declare function optionalString(record: {
+ [key: string]: JsonValue;
+}, key: string): string | null;
+export declare const SHA256_DIGEST_PATTERN: RegExp;
+/**
+ * Parses and deterministically validates an application bundle manifest
+ * (spec `044-application-bundle-manifest`) for embedder compatibility:
+ * schema version support, component identity, and sha-256 digest metadata.
+ * Rejection never falls back to a sidecar (spec 068 NFR-001).
+ *
+ * @throws {BundleRejectedError} with a stable `EmbedderErrorCode`.
+ */
+export declare function validateBundleCompatibility(appManifest: string | JsonValue): BundleCompatibility;
+/**
+ * Verifies bundled artifact bytes against declared sha-256 digest metadata
+ * using WebCrypto (browser) or the Node.js webcrypto implementation.
+ *
+ * @throws {BundleRejectedError} with `bundle_load_failed` on mismatch.
+ */
+export declare function verifyArtifactDigest(bytes: Uint8Array, declaredDigest: string, artifactLabel: string): Promise;
diff --git a/vendor/traverse-embedder-web/dist/bundleValidation.js b/vendor/traverse-embedder-web/dist/bundleValidation.js
new file mode 100644
index 0000000..d1ca229
--- /dev/null
+++ b/vendor/traverse-embedder-web/dist/bundleValidation.js
@@ -0,0 +1,129 @@
+/**
+ * Deterministic application bundle manifest validation (spec
+ * `044-application-bundle-manifest`) and artifact digest verification.
+ * Rejection never falls back to a sidecar (spec 068 NFR-001).
+ */
+import { SUPPORTED_BUNDLE_SCHEMA_VERSIONS, embedderError } from "./types.js";
+/** Thrown when a bundle is rejected at the embedder boundary. */
+export class BundleRejectedError extends Error {
+ embedderError;
+ constructor(error) {
+ super(`${error.code}: ${error.message}`);
+ this.name = "BundleRejectedError";
+ this.embedderError = error;
+ }
+}
+export function asRecord(value) {
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
+ return value;
+ }
+ return null;
+}
+export function requiredString(record, key, context) {
+ const value = record[key];
+ if (typeof value !== "string" || value.trim() === "") {
+ throw new BundleRejectedError(embedderError("bundle_load_failed", `${context} requires a non-empty string '${key}'`));
+ }
+ return value;
+}
+export function optionalString(record, key) {
+ const value = record[key];
+ return typeof value === "string" ? value : null;
+}
+export const SHA256_DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
+/**
+ * Parses and deterministically validates an application bundle manifest
+ * (spec `044-application-bundle-manifest`) for embedder compatibility:
+ * schema version support, component identity, and sha-256 digest metadata.
+ * Rejection never falls back to a sidecar (spec 068 NFR-001).
+ *
+ * @throws {BundleRejectedError} with a stable `EmbedderErrorCode`.
+ */
+export function validateBundleCompatibility(appManifest) {
+ let parsed;
+ if (typeof appManifest === "string") {
+ try {
+ parsed = JSON.parse(appManifest);
+ }
+ catch (error) {
+ throw new BundleRejectedError(embedderError("bundle_load_failed", `application bundle manifest is not valid JSON: ${String(error)}`));
+ }
+ }
+ else {
+ parsed = appManifest;
+ }
+ const manifest = asRecord(parsed);
+ if (manifest === null) {
+ throw new BundleRejectedError(embedderError("bundle_load_failed", "application bundle manifest must be a JSON object"));
+ }
+ const appId = requiredString(manifest, "app_id", "application bundle manifest");
+ const appVersion = requiredString(manifest, "version", "application bundle manifest");
+ const schemaVersion = requiredString(manifest, "schema_version", "application bundle manifest");
+ if (!SUPPORTED_BUNDLE_SCHEMA_VERSIONS.includes(schemaVersion)) {
+ throw new BundleRejectedError(embedderError("unsupported_bundle_schema", `bundle declares schema_version '${schemaVersion}' but this package supports ` +
+ `[${SUPPORTED_BUNDLE_SCHEMA_VERSIONS.join(", ")}]; no sidecar fallback is attempted`));
+ }
+ const componentsValue = manifest["components"];
+ if (!Array.isArray(componentsValue)) {
+ throw new BundleRejectedError(embedderError("bundle_load_failed", "application bundle manifest requires a 'components' array"));
+ }
+ const components = componentsValue.map((entry, index) => {
+ const component = asRecord(entry);
+ if (component === null) {
+ throw new BundleRejectedError(embedderError("bundle_load_failed", `components[${index}] must be a JSON object`));
+ }
+ const context = `components[${index}]`;
+ const digest = requiredString(component, "digest", context);
+ if (!SHA256_DIGEST_PATTERN.test(digest)) {
+ throw new BundleRejectedError(embedderError("bundle_load_failed", `${context} declares invalid digest metadata '${digest}'; ` +
+ "expected sha256:<64 hex characters>"));
+ }
+ return {
+ componentId: requiredString(component, "component_id", context),
+ version: requiredString(component, "version", context),
+ digest,
+ manifestPath: requiredString(component, "manifest_path", context),
+ };
+ });
+ const workflowsValue = manifest["workflows"];
+ const workflowIds = [];
+ const workflows = [];
+ if (Array.isArray(workflowsValue)) {
+ for (const [index, entry] of workflowsValue.entries()) {
+ const workflow = asRecord(entry);
+ if (workflow === null) {
+ throw new BundleRejectedError(embedderError("bundle_load_failed", `workflows[${index}] must be a JSON object`));
+ }
+ const context = `workflows[${index}]`;
+ const workflowId = requiredString(workflow, "workflow_id", context);
+ workflowIds.push(workflowId);
+ workflows.push({
+ workflowId,
+ workflowVersion: requiredString(workflow, "workflow_version", context),
+ path: requiredString(workflow, "path", context),
+ });
+ }
+ }
+ return { appId, appVersion, schemaVersion, components, workflowIds, workflows };
+}
+/**
+ * Verifies bundled artifact bytes against declared sha-256 digest metadata
+ * using WebCrypto (browser) or the Node.js webcrypto implementation.
+ *
+ * @throws {BundleRejectedError} with `bundle_load_failed` on mismatch.
+ */
+export async function verifyArtifactDigest(bytes, declaredDigest, artifactLabel) {
+ if (!SHA256_DIGEST_PATTERN.test(declaredDigest)) {
+ throw new BundleRejectedError(embedderError("bundle_load_failed", `${artifactLabel} declares invalid digest metadata '${declaredDigest}'`));
+ }
+ const digestBytes = await crypto.subtle.digest("SHA-256", bytes.slice().buffer);
+ const actual = [...new Uint8Array(digestBytes)]
+ .map((byte) => byte.toString(16).padStart(2, "0"))
+ .join("");
+ const expected = declaredDigest.slice("sha256:".length);
+ if (actual !== expected) {
+ throw new BundleRejectedError(embedderError("bundle_load_failed", `${artifactLabel} digest mismatch: manifest declares sha256:${expected} ` +
+ `but the bundled artifact hashes to sha256:${actual}; ` +
+ "no sidecar fallback is attempted"));
+ }
+}
diff --git a/vendor/traverse-embedder-web/dist/core.d.ts b/vendor/traverse-embedder-web/dist/core.d.ts
new file mode 100644
index 0000000..aa8dbc6
--- /dev/null
+++ b/vendor/traverse-embedder-web/dist/core.d.ts
@@ -0,0 +1,33 @@
+import type { CompatibleLifecycleOutcome, CompatibleStartOutcome, EmbedderError, EmbedderEvent, EventCallback, JsonValue, SubmitOutcome } from "./types.js";
+export declare class EmbedderCore {
+ readonly workspaceId: string;
+ readonly appId: string;
+ readonly appVersion: string;
+ readonly platform: string;
+ readonly compatibleTargets: Map;
+ private readonly instances;
+ private readonly subscribers;
+ private readonly history;
+ private nextEvent;
+ private nextSession;
+ private nextRequest;
+ private nextInstance;
+ stopped: boolean;
+ constructor(workspaceId: string, appId: string, appVersion: string, platform: string, compatibleTargets: Map);
+ nextSessionId(): string;
+ nextRequestId(): string;
+ private nextInstanceId;
+ emit(eventType: EmbedderEvent["event_type"], sessionId: string | null, data: JsonValue): void;
+ subscribe(callback: EventCallback): void;
+ emitErrorEvent(sessionId: string | null, error: EmbedderError, data: {
+ [key: string]: JsonValue;
+ }): void;
+ rejectedSubmit(targetId: string, error: EmbedderError): SubmitOutcome;
+ startCompatible(capabilityId: string, input: JsonValue): CompatibleStartOutcome;
+ transitionCompatible(capabilityId: string, instanceId: string | null, targetState: "stopped" | "killed"): CompatibleLifecycleOutcome;
+ private setInstanceState;
+ shutdown(): {
+ killedInstances: number;
+ };
+ evidence(runtimeImplementation: string, wasmComponents: JsonValue): JsonValue;
+}
diff --git a/vendor/traverse-embedder-web/dist/core.js b/vendor/traverse-embedder-web/dist/core.js
new file mode 100644
index 0000000..d79b016
--- /dev/null
+++ b/vendor/traverse-embedder-web/dist/core.js
@@ -0,0 +1,191 @@
+/**
+ * Shared deterministic embedder state: identity, counters, subscribers,
+ * event history, and the compatible-capability lifecycle table. Both
+ * `EmbedderTestDouble` and `BundleEmbedder` delegate here so their public
+ * boundary behavior — event envelope, deterministic identifiers, and
+ * compatible lifecycle — is identical (mirrors the Rust SDK's `EmbedderCore`).
+ */
+import { EMBEDDER_API_VERSION, EMBEDDER_CONFORMANCE_VERSION, EVENT_SCHEMA_VERSION, PACKAGE_NAME, PACKAGE_VERSION, SUPPORTED_BUNDLE_SCHEMA_VERSIONS, embedderError, errorValue, paddedId, runtimeStoppedError, } from "./types.js";
+export class EmbedderCore {
+ workspaceId;
+ appId;
+ appVersion;
+ platform;
+ compatibleTargets;
+ instances = new Map();
+ subscribers = [];
+ history = [];
+ nextEvent = 0;
+ nextSession = 0;
+ nextRequest = 0;
+ nextInstance = 0;
+ stopped = false;
+ constructor(workspaceId, appId, appVersion, platform, compatibleTargets) {
+ this.workspaceId = workspaceId;
+ this.appId = appId;
+ this.appVersion = appVersion;
+ this.platform = platform;
+ this.compatibleTargets = compatibleTargets;
+ }
+ nextSessionId() {
+ this.nextSession += 1;
+ return paddedId("sess", this.nextSession);
+ }
+ nextRequestId() {
+ this.nextRequest += 1;
+ return paddedId("req", this.nextRequest);
+ }
+ nextInstanceId() {
+ this.nextInstance += 1;
+ return paddedId("inst", this.nextInstance);
+ }
+ emit(eventType, sessionId, data) {
+ this.nextEvent += 1;
+ const event = {
+ kind: "embedder_event",
+ schema_version: EVENT_SCHEMA_VERSION,
+ embedder_api_version: EMBEDDER_API_VERSION,
+ event_id: paddedId("evt", this.nextEvent),
+ sequence: this.nextEvent,
+ event_type: eventType,
+ workspace_id: this.workspaceId,
+ app_id: this.appId,
+ session_id: sessionId,
+ data,
+ };
+ for (const subscriber of this.subscribers) {
+ subscriber(event);
+ }
+ this.history.push(event);
+ }
+ subscribe(callback) {
+ for (const event of this.history) {
+ callback(event);
+ }
+ this.subscribers.push(callback);
+ }
+ emitErrorEvent(sessionId, error, data) {
+ this.emit("error", sessionId, { ...data, error: errorValue(error) });
+ }
+ rejectedSubmit(targetId, error) {
+ this.emitErrorEvent(null, error, { target_id: targetId });
+ return { sessionId: null, status: "rejected", error };
+ }
+ startCompatible(capabilityId, input) {
+ let error = null;
+ if (this.stopped) {
+ error = runtimeStoppedError();
+ }
+ else {
+ const platforms = this.compatibleTargets.get(capabilityId);
+ if (platforms === undefined) {
+ error = embedderError("capability_not_compatible", `capability '${capabilityId}' is not a compatible-mode capability in this bundle`);
+ }
+ else if (!platforms.includes(this.platform)) {
+ error = embedderError("platform_not_supported", `capability '${capabilityId}' permits platforms [${platforms.join(", ")}] ` +
+ `but this embedder runs on '${this.platform}'`);
+ }
+ }
+ if (error !== null) {
+ this.emitErrorEvent(null, error, { capability_id: capabilityId });
+ return { instanceId: null, status: "error", error };
+ }
+ const instanceId = this.nextInstanceId();
+ this.instances.set(instanceId, { capabilityId, state: "started" });
+ this.emit("state_changed", null, {
+ capability_id: capabilityId,
+ instance_id: instanceId,
+ state: "started",
+ previous_state: null,
+ input,
+ });
+ return { instanceId, status: "started", error: null };
+ }
+ transitionCompatible(capabilityId, instanceId, targetState) {
+ if (this.stopped) {
+ const error = runtimeStoppedError();
+ this.emitErrorEvent(null, error, { capability_id: capabilityId });
+ return { status: "error", error };
+ }
+ let selected;
+ if (instanceId !== null) {
+ const instance = this.instances.get(instanceId);
+ if (instance === undefined || instance.capabilityId !== capabilityId) {
+ const error = embedderError("instance_not_found", `no instance '${instanceId}' exists for capability '${capabilityId}'`);
+ this.emitErrorEvent(null, error, {
+ capability_id: capabilityId,
+ instance_id: instanceId,
+ });
+ return { status: "error", error };
+ }
+ if (instance.state !== "started") {
+ const error = embedderError("instance_not_running", `instance '${instanceId}' of capability '${capabilityId}' is not running`);
+ this.emitErrorEvent(null, error, {
+ capability_id: capabilityId,
+ instance_id: instanceId,
+ });
+ return { status: "error", error };
+ }
+ selected = [instanceId];
+ }
+ else {
+ selected = [...this.instances.entries()]
+ .filter(([, instance]) => instance.capabilityId === capabilityId && instance.state === "started")
+ .map(([id]) => id);
+ }
+ if (selected.length === 0) {
+ const error = embedderError("instance_not_running", `capability '${capabilityId}' has no running instances`);
+ this.emitErrorEvent(null, error, { capability_id: capabilityId });
+ return { status: "error", error };
+ }
+ for (const id of selected) {
+ this.setInstanceState(id, targetState);
+ }
+ return { status: targetState, error: null };
+ }
+ setInstanceState(instanceId, targetState) {
+ const instance = this.instances.get(instanceId);
+ if (instance === undefined) {
+ return;
+ }
+ const previous = instance.state;
+ instance.state = targetState;
+ this.emit("state_changed", null, {
+ capability_id: instance.capabilityId,
+ instance_id: instanceId,
+ state: targetState,
+ previous_state: previous,
+ });
+ }
+ shutdown() {
+ if (this.stopped) {
+ return { killedInstances: 0 };
+ }
+ const running = [...this.instances.entries()]
+ .filter(([, instance]) => instance.state === "started")
+ .map(([id]) => id);
+ for (const id of running) {
+ this.setInstanceState(id, "killed");
+ }
+ this.stopped = true;
+ return { killedInstances: running.length };
+ }
+ evidence(runtimeImplementation, wasmComponents) {
+ return {
+ kind: "embedder_release_evidence",
+ schema_version: EVENT_SCHEMA_VERSION,
+ package: { name: PACKAGE_NAME, version: PACKAGE_VERSION },
+ embedder_api_version: EMBEDDER_API_VERSION,
+ conformance_version: EMBEDDER_CONFORMANCE_VERSION,
+ runtime: { implementation: runtimeImplementation },
+ supported_bundle_schema_versions: [...SUPPORTED_BUNDLE_SCHEMA_VERSIONS],
+ bundle: {
+ app_id: this.appId,
+ app_version: this.appVersion,
+ wasm_components: wasmComponents,
+ },
+ workspace_id: this.workspaceId,
+ platform: this.platform,
+ };
+ }
+}
diff --git a/vendor/traverse-embedder-web/dist/hostAbi.d.ts b/vendor/traverse-embedder-web/dist/hostAbi.d.ts
new file mode 100644
index 0000000..d8d147a
--- /dev/null
+++ b/vendor/traverse-embedder-web/dist/hostAbi.d.ts
@@ -0,0 +1,21 @@
+/**
+ * Traverse Host ABI import whitelist (mirrors
+ * `crates/traverse-runtime/src/executor/host_abi_v1.json`). A bundled WASM
+ * capability module may declare only these imports; every other import is
+ * rejected deterministically before the module is instantiated — the
+ * browser executor never links an unauthorized host function (deny-by-
+ * default, matching the native `WasmExecutor`'s no-filesystem,
+ * no-network, no-env-vars posture).
+ */
+export interface HostAbiImport {
+ readonly module: string;
+ readonly name: string;
+}
+/** Traverse Host ABI version this package validates modules against. */
+export declare const SUPPORTED_HOST_ABI_VERSION = "1.0.0";
+export declare const HOST_ABI_V1_WHITELIST: readonly HostAbiImport[];
+/**
+ * Returns the first function import outside the host ABI whitelist, or
+ * `null` when every function import is authorized.
+ */
+export declare function findUnauthorizedImport(module: WebAssembly.Module): HostAbiImport | null;
diff --git a/vendor/traverse-embedder-web/dist/hostAbi.js b/vendor/traverse-embedder-web/dist/hostAbi.js
new file mode 100644
index 0000000..ddd0290
--- /dev/null
+++ b/vendor/traverse-embedder-web/dist/hostAbi.js
@@ -0,0 +1,37 @@
+/**
+ * Traverse Host ABI import whitelist (mirrors
+ * `crates/traverse-runtime/src/executor/host_abi_v1.json`). A bundled WASM
+ * capability module may declare only these imports; every other import is
+ * rejected deterministically before the module is instantiated — the
+ * browser executor never links an unauthorized host function (deny-by-
+ * default, matching the native `WasmExecutor`'s no-filesystem,
+ * no-network, no-env-vars posture).
+ */
+/** Traverse Host ABI version this package validates modules against. */
+export const SUPPORTED_HOST_ABI_VERSION = "1.0.0";
+export const HOST_ABI_V1_WHITELIST = [
+ { module: "wasi_snapshot_preview1", name: "fd_read" },
+ { module: "wasi_snapshot_preview1", name: "fd_write" },
+ { module: "wasi_snapshot_preview1", name: "proc_exit" },
+ { module: "traverse_host", name: "capability_id" },
+ { module: "traverse_host", name: "capability_version" },
+ { module: "traverse_host", name: "runtime_config" },
+ { module: "traverse_host", name: "trace_context" },
+ { module: "traverse_host", name: "execution_id" },
+];
+/**
+ * Returns the first function import outside the host ABI whitelist, or
+ * `null` when every function import is authorized.
+ */
+export function findUnauthorizedImport(module) {
+ for (const imported of WebAssembly.Module.imports(module)) {
+ if (imported.kind !== "function") {
+ continue;
+ }
+ const allowed = HOST_ABI_V1_WHITELIST.some((entry) => entry.module === imported.module && entry.name === imported.name);
+ if (!allowed) {
+ return { module: imported.module, name: imported.name };
+ }
+ }
+ return null;
+}
diff --git a/vendor/traverse-embedder-web/dist/index.d.ts b/vendor/traverse-embedder-web/dist/index.d.ts
new file mode 100644
index 0000000..3f2bf32
--- /dev/null
+++ b/vendor/traverse-embedder-web/dist/index.d.ts
@@ -0,0 +1,32 @@
+/**
+ * Public Traverse platform embedder SDK for Web/TypeScript clients.
+ *
+ * This package is the Web row of spec `068-public-platform-embedder-packages`:
+ * a versioned public package implementing the `embedder-api/1.0.0` operations
+ * (spec `057-embeddable-runtime-host`) against an application-owned bundle,
+ * with no production dependency on `traverse-cli serve`.
+ *
+ * The event envelope, deterministic identifier scheme (`sess-*`, `req-*`,
+ * `evt-*`, `inst-*`), stable error codes, compatible-capability lifecycle,
+ * and shutdown semantics are identical to the Rust `traverse-embedder`
+ * package so every platform observes the same boundary (spec 057 FR-003).
+ *
+ * `BundleEmbedder` is the production implementation: it loads an
+ * application-owned bundle, digest-verifies and host-ABI-validates every
+ * bundled WASM capability, and executes them directly in the browser's
+ * native WebAssembly host via a minimal WASI shim — no nested WASM engine,
+ * no sidecar (spec 068 FR-002, NFR-001). `EmbedderTestDouble` is the
+ * deterministic in-memory implementation required by spec 068 FR-006.
+ */
+export { EMBEDDER_API_VERSION, EMBEDDER_CONFORMANCE_VERSION, SUPPORTED_BUNDLE_SCHEMA_VERSIONS, } from "./types.js";
+export type { CompatibleLifecycleOutcome, CompatibleStartOutcome, EmbedderError, EmbedderErrorCode, EmbedderEvent, EventCallback, JsonValue, ShutdownOutcome, SubmitOutcome, TraverseEmbedderApi, } from "./types.js";
+export { EmbedderTestDouble } from "./testDouble.js";
+export type { EmbedderTestDoubleConfig } from "./testDouble.js";
+export { BundleRejectedError, validateBundleCompatibility, verifyArtifactDigest, } from "./bundleValidation.js";
+export type { BundleCompatibility, BundleComponentSummary, BundleWorkflowSummary } from "./bundleValidation.js";
+export { BundleEmbedder } from "./bundleEmbedder.js";
+export type { BundleEmbedderConfig } from "./bundleEmbedder.js";
+export { FetchBundleLoader, NodeFsBundleLoader } from "./bundleLoader.js";
+export type { BundleLoader } from "./bundleLoader.js";
+export { HOST_ABI_V1_WHITELIST, SUPPORTED_HOST_ABI_VERSION, findUnauthorizedImport, } from "./hostAbi.js";
+export type { HostAbiImport } from "./hostAbi.js";
diff --git a/vendor/traverse-embedder-web/dist/index.js b/vendor/traverse-embedder-web/dist/index.js
new file mode 100644
index 0000000..a80e30e
--- /dev/null
+++ b/vendor/traverse-embedder-web/dist/index.js
@@ -0,0 +1,26 @@
+/**
+ * Public Traverse platform embedder SDK for Web/TypeScript clients.
+ *
+ * This package is the Web row of spec `068-public-platform-embedder-packages`:
+ * a versioned public package implementing the `embedder-api/1.0.0` operations
+ * (spec `057-embeddable-runtime-host`) against an application-owned bundle,
+ * with no production dependency on `traverse-cli serve`.
+ *
+ * The event envelope, deterministic identifier scheme (`sess-*`, `req-*`,
+ * `evt-*`, `inst-*`), stable error codes, compatible-capability lifecycle,
+ * and shutdown semantics are identical to the Rust `traverse-embedder`
+ * package so every platform observes the same boundary (spec 057 FR-003).
+ *
+ * `BundleEmbedder` is the production implementation: it loads an
+ * application-owned bundle, digest-verifies and host-ABI-validates every
+ * bundled WASM capability, and executes them directly in the browser's
+ * native WebAssembly host via a minimal WASI shim — no nested WASM engine,
+ * no sidecar (spec 068 FR-002, NFR-001). `EmbedderTestDouble` is the
+ * deterministic in-memory implementation required by spec 068 FR-006.
+ */
+export { EMBEDDER_API_VERSION, EMBEDDER_CONFORMANCE_VERSION, SUPPORTED_BUNDLE_SCHEMA_VERSIONS, } from "./types.js";
+export { EmbedderTestDouble } from "./testDouble.js";
+export { BundleRejectedError, validateBundleCompatibility, verifyArtifactDigest, } from "./bundleValidation.js";
+export { BundleEmbedder } from "./bundleEmbedder.js";
+export { FetchBundleLoader, NodeFsBundleLoader } from "./bundleLoader.js";
+export { HOST_ABI_V1_WHITELIST, SUPPORTED_HOST_ABI_VERSION, findUnauthorizedImport, } from "./hostAbi.js";
diff --git a/vendor/traverse-embedder-web/dist/testDouble.d.ts b/vendor/traverse-embedder-web/dist/testDouble.d.ts
new file mode 100644
index 0000000..2824927
--- /dev/null
+++ b/vendor/traverse-embedder-web/dist/testDouble.d.ts
@@ -0,0 +1,26 @@
+import type { CompatibleLifecycleOutcome, CompatibleStartOutcome, EventCallback, JsonValue, ShutdownOutcome, SubmitOutcome, TraverseEmbedderApi } from "./types.js";
+/** Configuration for the deterministic test double. */
+export interface EmbedderTestDoubleConfig {
+ readonly workspaceId?: string;
+ readonly appId?: string;
+ readonly appVersion?: string;
+ readonly platform?: string;
+}
+export declare class EmbedderTestDouble implements TraverseEmbedderApi {
+ private readonly core;
+ private readonly scripted;
+ constructor(config?: EmbedderTestDoubleConfig);
+ /** Scripts `submit(targetId, _)` to succeed with `output`. */
+ withTargetOutput(targetId: string, output: JsonValue): this;
+ /** Scripts `submit(targetId, _)` to fail with a runtime-shaped error. */
+ withTargetError(targetId: string, code: string, message: string): this;
+ /** Declares a compatible-mode capability with a platform allowlist. */
+ withCompatibleTarget(capabilityId: string, platforms: readonly string[]): this;
+ submit(targetId: string, input: JsonValue): SubmitOutcome;
+ subscribe(callback: EventCallback): void;
+ startCompatible(capabilityId: string, input: JsonValue): CompatibleStartOutcome;
+ stopCompatible(capabilityId: string, instanceId?: string | null): CompatibleLifecycleOutcome;
+ killCompatible(capabilityId: string, instanceId?: string | null): CompatibleLifecycleOutcome;
+ shutdown(): ShutdownOutcome;
+ releaseEvidence(): JsonValue;
+}
diff --git a/vendor/traverse-embedder-web/dist/testDouble.js b/vendor/traverse-embedder-web/dist/testDouble.js
new file mode 100644
index 0000000..561b8fa
--- /dev/null
+++ b/vendor/traverse-embedder-web/dist/testDouble.js
@@ -0,0 +1,84 @@
+/**
+ * Deterministic in-memory test double implementing the same public boundary
+ * as the production embedder (spec 068 FR-006). It shares the event
+ * envelope, identifier scheme, compatible lifecycle, and shutdown semantics;
+ * only capability execution is replaced with scripted results. It contains
+ * no business logic.
+ */
+import { EmbedderCore } from "./core.js";
+import { embedderError, runtimeStoppedError } from "./types.js";
+export class EmbedderTestDouble {
+ core;
+ scripted = new Map();
+ constructor(config = {}) {
+ this.core = new EmbedderCore(config.workspaceId ?? "local-default", config.appId ?? "test-app", config.appVersion ?? "1.0.0", config.platform ?? "web", new Map());
+ }
+ /** Scripts `submit(targetId, _)` to succeed with `output`. */
+ withTargetOutput(targetId, output) {
+ this.scripted.set(targetId, { kind: "output", output });
+ return this;
+ }
+ /** Scripts `submit(targetId, _)` to fail with a runtime-shaped error. */
+ withTargetError(targetId, code, message) {
+ this.scripted.set(targetId, { kind: "error", code, message });
+ return this;
+ }
+ /** Declares a compatible-mode capability with a platform allowlist. */
+ withCompatibleTarget(capabilityId, platforms) {
+ this.core.compatibleTargets.set(capabilityId, platforms);
+ return this;
+ }
+ submit(targetId, input) {
+ void input;
+ if (this.core.stopped) {
+ return this.core.rejectedSubmit(targetId, runtimeStoppedError());
+ }
+ const result = this.scripted.get(targetId);
+ if (result === undefined) {
+ return this.core.rejectedSubmit(targetId, embedderError("target_not_found", `'${targetId}' is neither a bundled workflow nor a bundled capability`));
+ }
+ const sessionId = this.core.nextSessionId();
+ const requestId = this.core.nextRequestId();
+ const executionId = `exec_${requestId}`;
+ this.core.emit("capability_invoked", sessionId, {
+ execution_id: executionId,
+ capability_id: targetId,
+ capability_version: "1.0.0",
+ });
+ if (result.kind === "output") {
+ this.core.emit("capability_result", sessionId, {
+ execution_id: executionId,
+ capability_id: targetId,
+ status: "completed",
+ output: result.output,
+ });
+ }
+ else {
+ this.core.emit("error", sessionId, {
+ execution_id: executionId,
+ capability_id: targetId,
+ status: "error",
+ error: { code: result.code, message: result.message, details: {} },
+ });
+ }
+ return { sessionId, status: "accepted", error: null };
+ }
+ subscribe(callback) {
+ this.core.subscribe(callback);
+ }
+ startCompatible(capabilityId, input) {
+ return this.core.startCompatible(capabilityId, input);
+ }
+ stopCompatible(capabilityId, instanceId = null) {
+ return this.core.transitionCompatible(capabilityId, instanceId, "stopped");
+ }
+ killCompatible(capabilityId, instanceId = null) {
+ return this.core.transitionCompatible(capabilityId, instanceId, "killed");
+ }
+ shutdown() {
+ return this.core.shutdown();
+ }
+ releaseEvidence() {
+ return this.core.evidence("test-double", []);
+ }
+}
diff --git a/vendor/traverse-embedder-web/dist/types.d.ts b/vendor/traverse-embedder-web/dist/types.d.ts
new file mode 100644
index 0000000..86188ef
--- /dev/null
+++ b/vendor/traverse-embedder-web/dist/types.d.ts
@@ -0,0 +1,109 @@
+/**
+ * Shared wire types for the `embedder-api/1.0.0` boundary (spec 057). These
+ * are wire-identical in field naming to the Rust `traverse-embedder` crate
+ * so every platform observes the same operations, event envelope, and error
+ * codes (spec 057 FR-003).
+ */
+/** Implemented embedder API version (spec 057 IDL `$id` suffix). */
+export declare const EMBEDDER_API_VERSION = "1.0.0";
+/** Conformance suite revision this package certifies against (spec 057). */
+export declare const EMBEDDER_CONFORMANCE_VERSION = "1.0.0";
+/** Application bundle manifest `schema_version` values this package accepts. */
+export declare const SUPPORTED_BUNDLE_SCHEMA_VERSIONS: readonly string[];
+export declare const EVENT_SCHEMA_VERSION = "1.0.0";
+export declare const PACKAGE_NAME = "traverse-embedder-web";
+export declare const PACKAGE_VERSION = "0.7.0";
+/** Stable embedder-boundary error codes (wire-identical to the Rust SDK). */
+export type EmbedderErrorCode = "bundle_load_failed" | "unsupported_bundle_schema" | "runtime_stopped" | "target_not_found" | "compatible_lifecycle_required" | "capability_not_compatible" | "platform_not_supported" | "instance_not_found" | "instance_not_running";
+/** A structured embedder-boundary error. */
+export interface EmbedderError {
+ readonly code: EmbedderErrorCode;
+ readonly message: string;
+}
+/** `runtime.submit` output. */
+export interface SubmitOutcome {
+ readonly sessionId: string | null;
+ readonly status: "accepted" | "rejected";
+ readonly error: EmbedderError | null;
+}
+/** `compatible.start` output. */
+export interface CompatibleStartOutcome {
+ readonly instanceId: string | null;
+ readonly status: "started" | "error";
+ readonly error: EmbedderError | null;
+}
+/** `compatible.stop` / `compatible.kill` output. */
+export interface CompatibleLifecycleOutcome {
+ readonly status: "stopped" | "killed" | "error";
+ readonly error: EmbedderError | null;
+}
+/** `runtime.shutdown` output (always stopped; idempotent). */
+export interface ShutdownOutcome {
+ readonly killedInstances: number;
+}
+/** JSON value type for wire payloads. */
+export type JsonValue = string | number | boolean | null | JsonValue[] | {
+ [key: string]: JsonValue;
+};
+/**
+ * Ordered embedder event (JSON wire format, spec 057). The envelope is
+ * byte-identical in field naming to the Rust SDK's `embedder_event`.
+ */
+export interface EmbedderEvent {
+ readonly kind: "embedder_event";
+ readonly schema_version: string;
+ readonly embedder_api_version: string;
+ readonly event_id: string;
+ readonly sequence: number;
+ readonly event_type: "state_changed" | "capability_invoked" | "capability_result" | "error";
+ readonly workspace_id: string;
+ readonly app_id: string;
+ readonly session_id: string | null;
+ readonly data: JsonValue;
+}
+/** Ordered, synchronous event subscriber. */
+export type EventCallback = (event: EmbedderEvent) => void;
+/**
+ * The uniform `embedder-api/1.0.0` operation surface (spec 057 FR-003).
+ *
+ * `EmbedderTestDouble` is the deterministic in-memory implementation
+ * required by spec 068 FR-006; `BundleEmbedder` is the production
+ * runtime-WASM implementation. Both implement this identical boundary.
+ */
+export interface TraverseEmbedderApi {
+ /** `runtime.submit`: execute a bundled workflow or WASM capability. */
+ submit(targetId: string, input: JsonValue): SubmitOutcome;
+ /**
+ * `runtime.subscribe`: register an ordered event callback. Previously
+ * emitted events are replayed to the new subscriber first, so late
+ * subscribers observe the identical ordered stream.
+ */
+ subscribe(callback: EventCallback): void;
+ /** `compatible.start`: start a compatible-mode capability instance. */
+ startCompatible(capabilityId: string, input: JsonValue): CompatibleStartOutcome;
+ /**
+ * `compatible.stop`: gracefully stop one instance (`instanceId`) or every
+ * running instance of the capability (`null`).
+ */
+ stopCompatible(capabilityId: string, instanceId?: string | null): CompatibleLifecycleOutcome;
+ /**
+ * `compatible.kill`: force-terminate one instance (`instanceId`) or every
+ * running instance of the capability (`null`).
+ */
+ killCompatible(capabilityId: string, instanceId?: string | null): CompatibleLifecycleOutcome;
+ /**
+ * `runtime.shutdown`: kill running compatible instances and stop accepting
+ * operations. Idempotent.
+ */
+ shutdown(): ShutdownOutcome;
+ /**
+ * Release evidence connecting this embedder to its package version,
+ * runtime, conformance version, and bundle digests (spec 068 FR-008,
+ * NFR-002).
+ */
+ releaseEvidence(): JsonValue;
+}
+export declare function embedderError(code: EmbedderErrorCode, message: string): EmbedderError;
+export declare function errorValue(error: EmbedderError): JsonValue;
+export declare function paddedId(prefix: string, counter: number): string;
+export declare function runtimeStoppedError(): EmbedderError;
diff --git a/vendor/traverse-embedder-web/dist/types.js b/vendor/traverse-embedder-web/dist/types.js
new file mode 100644
index 0000000..6405a8b
--- /dev/null
+++ b/vendor/traverse-embedder-web/dist/types.js
@@ -0,0 +1,27 @@
+/**
+ * Shared wire types for the `embedder-api/1.0.0` boundary (spec 057). These
+ * are wire-identical in field naming to the Rust `traverse-embedder` crate
+ * so every platform observes the same operations, event envelope, and error
+ * codes (spec 057 FR-003).
+ */
+/** Implemented embedder API version (spec 057 IDL `$id` suffix). */
+export const EMBEDDER_API_VERSION = "1.0.0";
+/** Conformance suite revision this package certifies against (spec 057). */
+export const EMBEDDER_CONFORMANCE_VERSION = "1.0.0";
+/** Application bundle manifest `schema_version` values this package accepts. */
+export const SUPPORTED_BUNDLE_SCHEMA_VERSIONS = ["1.0.0"];
+export const EVENT_SCHEMA_VERSION = "1.0.0";
+export const PACKAGE_NAME = "traverse-embedder-web";
+export const PACKAGE_VERSION = "0.7.0";
+export function embedderError(code, message) {
+ return { code, message };
+}
+export function errorValue(error) {
+ return { code: error.code, message: error.message };
+}
+export function paddedId(prefix, counter) {
+ return `${prefix}-${String(counter).padStart(8, "0")}`;
+}
+export function runtimeStoppedError() {
+ return embedderError("runtime_stopped", "the embedded runtime was shut down and accepts no further operations");
+}
diff --git a/vendor/traverse-embedder-web/dist/wasi.d.ts b/vendor/traverse-embedder-web/dist/wasi.d.ts
new file mode 100644
index 0000000..71643f6
--- /dev/null
+++ b/vendor/traverse-embedder-web/dist/wasi.d.ts
@@ -0,0 +1,38 @@
+/**
+ * Minimal deterministic WASI `preview1` shim covering exactly the
+ * `wasi_snapshot_preview1` surface in the Traverse Host ABI whitelist
+ * (`fd_read`, `fd_write`, `proc_exit`; see `hostAbi.ts`). No filesystem, no
+ * network, no environment, no clocks, no randomness — deny-by-default,
+ * mirroring the native `WasmExecutor`'s `WasiCtxBuilder` configuration
+ * (stdin = input JSON bytes, stdout = captured buffer).
+ *
+ * Any capability module that requires a broader WASI surface (args,
+ * environ, clocks, filesystem) is already outside the Traverse Host ABI
+ * whitelist and would be rejected by the native runtime's own import
+ * validation before this shim would ever see it (spec `064`).
+ */
+/** Thrown by `proc_exit`; unwinds the synchronous WASM call. */
+export declare class WasiExit extends Error {
+ readonly code: number;
+ constructor(code: number);
+}
+/** Mutable memory handle, populated once the WASM instance is created. */
+export interface WasiMemoryRef {
+ memory: WebAssembly.Memory | null;
+}
+/** Captured stdio pipes for one execution. */
+export declare class WasiPipes {
+ private readonly stdin;
+ private stdinOffset;
+ private readonly stdoutChunks;
+ constructor(stdinBytes: Uint8Array);
+ readStdin(maxLength: number): Uint8Array;
+ writeStdout(bytes: Uint8Array): void;
+ stdoutBytes(): Uint8Array;
+}
+/**
+ * Builds the `wasi_snapshot_preview1` import object for one execution.
+ * `memoryRef.memory` must be set to the instantiated module's exported
+ * memory before any of these functions are invoked.
+ */
+export declare function createWasiPreview1Imports(pipes: WasiPipes, memoryRef: WasiMemoryRef): WebAssembly.ModuleImports;
diff --git a/vendor/traverse-embedder-web/dist/wasi.js b/vendor/traverse-embedder-web/dist/wasi.js
new file mode 100644
index 0000000..3cf97de
--- /dev/null
+++ b/vendor/traverse-embedder-web/dist/wasi.js
@@ -0,0 +1,115 @@
+/**
+ * Minimal deterministic WASI `preview1` shim covering exactly the
+ * `wasi_snapshot_preview1` surface in the Traverse Host ABI whitelist
+ * (`fd_read`, `fd_write`, `proc_exit`; see `hostAbi.ts`). No filesystem, no
+ * network, no environment, no clocks, no randomness — deny-by-default,
+ * mirroring the native `WasmExecutor`'s `WasiCtxBuilder` configuration
+ * (stdin = input JSON bytes, stdout = captured buffer).
+ *
+ * Any capability module that requires a broader WASI surface (args,
+ * environ, clocks, filesystem) is already outside the Traverse Host ABI
+ * whitelist and would be rejected by the native runtime's own import
+ * validation before this shim would ever see it (spec `064`).
+ */
+const STDIN_FD = 0;
+const STDOUT_FD = 1;
+const STDERR_FD = 2;
+const WASI_ERRNO_SUCCESS = 0;
+const WASI_ERRNO_BADF = 8;
+/** Thrown by `proc_exit`; unwinds the synchronous WASM call. */
+export class WasiExit extends Error {
+ code;
+ constructor(code) {
+ super(`proc_exit(${code})`);
+ this.name = "WasiExit";
+ this.code = code;
+ }
+}
+/** Captured stdio pipes for one execution. */
+export class WasiPipes {
+ stdin;
+ stdinOffset = 0;
+ stdoutChunks = [];
+ constructor(stdinBytes) {
+ this.stdin = stdinBytes;
+ }
+ readStdin(maxLength) {
+ const remaining = this.stdin.length - this.stdinOffset;
+ const length = Math.min(maxLength, Math.max(remaining, 0));
+ const chunk = this.stdin.subarray(this.stdinOffset, this.stdinOffset + length);
+ this.stdinOffset += length;
+ return chunk;
+ }
+ writeStdout(bytes) {
+ for (const byte of bytes) {
+ this.stdoutChunks.push(byte);
+ }
+ }
+ stdoutBytes() {
+ return Uint8Array.from(this.stdoutChunks);
+ }
+}
+function memoryView(memoryRef) {
+ if (memoryRef.memory === null) {
+ throw new Error("wasi: module does not export linear memory");
+ }
+ return new DataView(memoryRef.memory.buffer);
+}
+function memoryBytes(memoryRef, ptr, length) {
+ if (memoryRef.memory === null) {
+ throw new Error("wasi: module does not export linear memory");
+ }
+ return new Uint8Array(memoryRef.memory.buffer, ptr, length);
+}
+/**
+ * Builds the `wasi_snapshot_preview1` import object for one execution.
+ * `memoryRef.memory` must be set to the instantiated module's exported
+ * memory before any of these functions are invoked.
+ */
+export function createWasiPreview1Imports(pipes, memoryRef) {
+ return {
+ fd_read(fd, iovsPtr, iovsLen, nreadPtr) {
+ if (fd !== STDIN_FD) {
+ return WASI_ERRNO_BADF;
+ }
+ const view = memoryView(memoryRef);
+ let totalRead = 0;
+ for (let index = 0; index < iovsLen; index += 1) {
+ const base = iovsPtr + index * 8;
+ const bufPtr = view.getUint32(base, true);
+ const bufLen = view.getUint32(base + 4, true);
+ const chunk = pipes.readStdin(bufLen);
+ if (chunk.length > 0) {
+ memoryBytes(memoryRef, bufPtr, chunk.length).set(chunk);
+ }
+ totalRead += chunk.length;
+ if (chunk.length < bufLen) {
+ break;
+ }
+ }
+ view.setUint32(nreadPtr, totalRead, true);
+ return WASI_ERRNO_SUCCESS;
+ },
+ fd_write(fd, iovsPtr, iovsLen, nwrittenPtr) {
+ if (fd !== STDOUT_FD && fd !== STDERR_FD) {
+ return WASI_ERRNO_BADF;
+ }
+ const view = memoryView(memoryRef);
+ let totalWritten = 0;
+ for (let index = 0; index < iovsLen; index += 1) {
+ const base = iovsPtr + index * 8;
+ const bufPtr = view.getUint32(base, true);
+ const bufLen = view.getUint32(base + 4, true);
+ if (fd === STDOUT_FD) {
+ pipes.writeStdout(memoryBytes(memoryRef, bufPtr, bufLen));
+ }
+ totalWritten += bufLen;
+ }
+ view.setUint32(nwrittenPtr, totalWritten, true);
+ return WASI_ERRNO_SUCCESS;
+ },
+ proc_exit(code) {
+ throw new WasiExit(code);
+ },
+ };
+}
diff --git a/vendor/traverse-embedder-web/package.json b/vendor/traverse-embedder-web/package.json
new file mode 100644
index 0000000..b75ed88
--- /dev/null
+++ b/vendor/traverse-embedder-web/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "traverse-embedder-web",
+ "version": "0.7.0",
+ "description": "Public Traverse platform embedder SDK for Web/TypeScript clients, implementing embedder-api/1.0.0 (spec 068)",
+ "license": "Apache-2.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/traverse-framework/traverse",
+ "directory": "packages/web/TraverseEmbedder"
+ },
+ "type": "module",
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ }
+ },
+ "files": [
+ "dist",
+ "README.md"
+ ],
+ "scripts": {
+ "build": "tsc",
+ "test": "npm run build && node --test tests/embedder.test.mjs tests/bundleEmbedder.test.mjs"
+ },
+ "devDependencies": {
+ "@types/node": "^22.20.1",
+ "typescript": "^5.6.0",
+ "wabt": "^1.0.39"
+ }
+}