Skip to content
Back to skills

Meerkat Wasm

BSecurity

Developer guide for the Meerkat WASM embedded runtime (meerkat-web-runtime) and the `@rkat/web` browser SDK. Covers wasm32 compilation, building/deploying WASM bundles, writing/auditing wasm_bindgen exports (sessions, mobs, comms, JS tool callbacks, external auth resolver, subscription handles), the auth_binding + registerExternalAuthResolver auth model, tokio_with_wasm aliasing, cfg-gating patterns, mobpack bootstrap, and common wasm32 gotchas. Use when touching meerkat-web-runtime, sdks/web...

  • 21 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 2, 2026
developmentjavascripttypescriptrustgojavabashsqlnodeexpressdebugging

Works with

  • terminal
  • cli
  • api
  • mcp

Security analysis

B85/100
  • highPerforms destructive filesystem operations

Pro scans all 2 files and shows the line behind each finding

Scanned September 29, 2026

npx -y skills add lukacf/meerkat --skill meerkat-wasm --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Meerkat Wasm?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Meerkat Wasm
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/lukacf-meerkat-wasm/badge)](https://www.skillsdirectory.com/skills/lukacf-meerkat-wasm)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: meerkat-wasm
description: "Developer guide for the Meerkat WASM embedded runtime (meerkat-web-runtime) and the `@rkat/web` browser SDK. Covers wasm32 compilation, building/deploying WASM bundles, writing/auditing wasm_bindgen exports (sessions, mobs, comms, JS tool callbacks, external auth resolver, subscription handles), the auth_binding + registerExternalAuthResolver auth model, tokio_with_wasm aliasing, cfg-gating patterns, mobpack bootstrap, and common wasm32 gotchas. Use when touching meerkat-web-runtime, sdks/web, browser auth flows, or making a meerkat crate wasm32-compatible."
---

# Meerkat WASM Embedded Runtime

The `meerkat-web-runtime` crate compiles the full meerkat agent stack to `wasm32-unknown-unknown`. It is an embedded deployment target for mobpacks — the JavaScript equivalent of the Rust SDK. Not a protocol server like RPC/REST.

## Crate Compatibility Pattern

To make a meerkat crate compile for wasm32, apply this pattern:

**Cargo.toml** — gate platform-specific deps:
```toml
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { workspace = true }
rusqlite = { workspace = true }

[target.'cfg(target_arch = "wasm32")'.dependencies]
tokio_with_wasm = { workspace = true }
```

**lib.rs** - tokio alias + module gating:
```rust
#[cfg(target_arch = "wasm32")]
pub mod tokio {
    pub use tokio_with_wasm::alias::*;
    pub use meerkat_core::tokio::task;
    pub use meerkat_core::time_compat::wasm as time;
}

#[cfg(not(target_arch = "wasm32"))]
mod filesystem_module;
```

**async_trait** — dual cfg_attr:
```rust
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
```

**Time types** — use `meerkat_core::time_compat::{SystemTime, Instant, Duration}`, never `std::time::*`.

**Tokio imports** in wasm32-visible code — add `#[cfg(target_arch = "wasm32")] use crate::tokio;` then existing `use tokio::...` paths resolve through the alias.

Use the shared core alias for WASM scheduling. Its time adapter owns the exact
JavaScript timer handle and cancels it when the Rust future is dropped. Direct
`tokio_with_wasm::alias::time` calls bypass that cleanup. The shared task alias
also implements `yield_now` through the same cancellable timer. Native code continues
to use Tokio. Lifecycle tests must exit naturally after teardown; forcing a
successful Node exit hides retained timer resources.

## Override-First Resource Injection

Browser bootstrap composes `AgentFactory::minimal().builtins(true)` with
`with_browser_runtime_profile()`. `FactoryAgentBuilder` injects the browser tool
dispatcher and a `StoreAdapter` backed by `MemoryStore` into direct and mob
builds. The shared profile validates requested capabilities before construction.

| Resource | Browser composition |
|----------|---------------------|
| Tool dispatcher | `CompositeDispatcher::new_wasm()` plus runtime-registered JS tools |
| Session store | `StoreAdapter` backed by `meerkat_store::MemoryStore` |
| Hooks | Configured entries and hook-engine overrides are excluded by the profile |
| Skills | Factory-owned embedded sources; custom engines and repository discovery are excluded |

Embedded skill selection uses canonical `SkillKey` values. The profile permits
the builtin source and rejects external sources with `CAPABILITY_UNAVAILABLE`.
Resource injection does not bypass profile enforcement.

## Runtime mode on wasm32

WASM is a runtime-backed surface. Direct sessions and mob members share the
cached `MeerkatMachine` adapter and use canonical `SessionOwned` bindings:

```text
WASM -> MeerkatMachine -> prepared runtime bindings -> SessionOwned -> SessionService -> AgentFactory
```

Use `meerkat::surface::materialize_ephemeral_runtime_session` for direct
creation and the shared runtime turn, interrupt, and peer-wiring helpers for
later operations. Keep exact registration custody through actor attachment and
cancellation cleanup. Browser handle maps and subscriptions are private
mechanics; they never determine admission, lifecycle, or terminality.

`BrowserRuntimeProfile` in `meerkat-capabilities` owns browser limitations.
In-memory storage and page-lifetime execution do not change runtime authority.
Direct `keep_alive: true` sessions with `comms_name` support runtime peer ingress
and comms drain. Never construct `StandaloneEphemeral` on this surface.

Await bootstrap, session creation, state reads, interruption, and destruction.
The SDK's `Session.sessionId` is also a promise. Turn results and session state
project the generated canonical contracts; no browser-only status or identity
fields belong in those results.

## `@rkat/web` npm Package

The `sdks/web/` directory contains `@rkat/web` — a TypeScript wrapper around the wasm_bindgen exports with an idiomatic camelCase API. Ships with the pre-built WASM binary and a Node.js provider proxy.

**Key classes:** `MeerkatRuntime`, `Mob`, `Session`, `EventSubscription`. Auth helpers live in `sdks/web/src/auth.ts` (`registerExternalAuthResolver`, `clearExternalAuthResolver`, `withAuthBinding`, `Auth` types).

Current release-line notes: `runtime_version()` and package compatibility
should match the workspace `Cargo.toml` version (latest published release line
is `0.8.x`; do not hardcode a stale patch pin). The OpenAI and global catalog
default is `gpt-6-astra`. Confirm the proxy's API organization has access;
otherwise select `gpt-5.5` explicitly. Existing configured models are unchanged. Anthropic and
Gemini examples should use the current `claude-opus-5` and
`gemini-3.5-flash` defaults unless a test deliberately pins another model. WASM
mob flows use the same current
identity-first mob runtime, including helper/fork/respawn controls and
runtime-committed session projection behavior.

**Provider proxy** (`sdks/web/proxy/`): Node.js auth-injecting reverse proxy. Sits between browser WASM and LLM providers so API keys stay server-side. Strips `Origin`/`Referer`/`Accept-Encoding` headers from forwarded requests.

```bash
# Standalone proxy
ANTHROPIC_API_KEY=sk-... npx @rkat/web proxy --port 3100
```

For a server with Fetch-style routing, set `ANTHROPIC_API_KEY` in that server's
environment. Pass a `Request` and the path with the provider prefix removed,
then return the resulting `Response`:

```typescript
import { createProxyHandler } from '@rkat/web/proxy';

const anthropicProxy = createProxyHandler('anthropic');

export function handleRequest(request: Request): Promise<Response> {
  const { pathname } = new URL(request.url);
  if (pathname !== '/anthropic' && !pathname.startsWith('/anthropic/')) {
    return Promise.resolve(new Response('Not found', { status: 404 }));
  }
  return anthropicProxy(request, pathname.slice('/anthropic'.length) || '/');
}
```

This is not Express middleware. A Node/Express host must adapt its request to
the Fetch API and stream the returned response back; `startProxy` in
`sdks/web/proxy/index.mjs` demonstrates that adapter.

WASM client uses per-provider base URLs to point at the proxy natively — no fetch override needed:
```typescript
const runtime = await MeerkatRuntime.init(wasm, {
  anthropicApiKey: 'proxy',  // dummy, proxy replaces it
  anthropicBaseUrl: 'http://localhost:3100/anthropic',
});
```

### Browser auth model

Stock `MeerkatRuntime.init()` and `initFromMobpack()` accept provider keys and
base URLs, not realm/binding configuration. They synthesize `InlineSecret`
bindings in the in-memory `global` realm. Use keys or a server-side auth proxy
with that stock bootstrap; registering a callback does not create a realm or
convert an `InlineSecret` binding to external auth. Native realm config files
are not loaded by these browser exports.

The external resolver API is a **custom Rust/WASM composition seam**. To use
it, the custom bootstrap must supply a binding whose credential source is
`CredentialSourceSpec::ExternalResolver { handle: "wasm_host" }`. Only then
does the factory call the registered host callback for a typed
`ExternalAuthLease`. The following is a callback-registration fragment, not
a runnable external-binding setup for the stock npm bundle:

```typescript
import { registerExternalAuthResolver } from '@rkat/web';
import * as wasm from '@rkat/web/wasm/meerkat_web_runtime.js';

await wasm.default(); // Load the wasm-pack web binary before invoking an export.
registerExternalAuthResolver(wasm, async (authBinding) => {
  const token = await myHostFetchToken(authBinding);
  return {
    kind: 'inline_secret',
    secret: token.accessToken,
    metadata: { account_id: token.accountId },
    expires_at: token.expiresAt,
  };
});
```

Register before building the externally bound session. First runtime
installation preserves a registration made after loading the binary, but
replacing an existing runtime clears it; re-register after that bootstrap.
`withAuthBinding(authBinding, config)` only sets the structural selector on
the config. It cannot install the binding required by a custom composition.

Notes:

- WASM synthesizes its api-key binding section under the reserved `global` realm (`GLOBAL_REALM_SLUG`) in `crates/meerkat-web-runtime/src/lib.rs`. A session built without an explicit `authBinding` uses `global` as its explicit head; this is a degenerate single-realm chain with no filesystem doc or parent edges. Do not synthesize under any other slug.
- `authBinding` is structural and supported on `runtime.createSession({...})`, `mob.spawnHelper(...)`, and `mob.forkHelper(...)`. Plain `mob.spawn([...])` specs do not currently carry an auth binding.
- `mob.spawnHelper(...)` and `mob.forkHelper(...)` options require `resultLabel` and `maxTextBytes` (wire `result_label` / `max_text_bytes`); the helper result carries required `output`, `tokens_used`, `agent_identity`, `member_ref`, `bounded_result`, `session_id`, `usage`, `turns`, and `tool_calls`, plus optional `retirement_error`.
- `RuntimeConfig` uses `anthropicApiKey` / `anthropicBaseUrl`, `openaiApiKey` / `openaiBaseUrl`, and `geminiApiKey` / `geminiBaseUrl`. Raw WASM uses `anthropic_api_key` / `anthropic_base_url`, `openai_api_key` / `openai_base_url`, and `gemini_api_key` / `gemini_base_url`. Generic `apiKey` / `baseUrl` compatibility fields are deleted at both runtime and session boundaries; per-session credentials are not accepted.
- Use `clearExternalAuthResolver(wasm)` from `@rkat/web`, or pass `JsValue::NULL` / `undefined` to the raw WASM export, to clear the registration.
- `register_tool_callback` registers promise-returning JS callbacks. `register_js_tool` registration is synchronous, but dispatch reports pending detached host work, not completion. The host observes `ToolCallRequested`, performs the action, and reports any actual completion/result through a later session/mob message. Both registrations require initialized runtime state; prefer the instance methods after `MeerkatRuntime.init(...)`.
- Await `destroy_runtime(expected_handle?)` or `runtime.destroy()` for canonical session and mob teardown. The SDK passes the opaque bootstrap `runtime_handle` so stale wrappers cannot destroy a replacement runtime. Successful teardown clears subscriptions and the resolver, then drops `RuntimeState`; failures preserve cleanup authority for retry. This does not undo already-dispatched external host side effects.

For repository smoke coverage, browser/WASM scenarios are owned by the Rust lane
harness in `tests/integration/src/e2e_lanes.rs`. Prefer
`make e2e-live` or `make e2e-smoke` over manual `npm run smoke` unless you are
debugging the browser surface directly. Cargo is the default backend; with
BuildBuddy access, `MEERKAT_BUILDBUDDY=1 make e2e-live` and
`MEERKAT_BUILDBUDDY=1 make e2e-smoke` route through the matching Bazel/RBE
lanes.

**Version validation:** `runtime_version()` export returns `CARGO_PKG_VERSION`. `MeerkatRuntime.init()` checks it against the expected version to catch stale cached WASM.

## Building

From the repository root, build the SDK's WASM artifact through its checked
build wrapper and the repository Cargo facade:

```bash
CARGO="$PWD/scripts/repo-cargo" RUST_LANE_ID=wasm-sdk npm --prefix sdks/web run build:wasm
```

The wrapper owns the wasm32 flags, build cache, and output lock. For custom
`wasm-pack` work, the crate path is `crates/meerkat-web-runtime`; `--out-dir` is
relative to that crate root, not the workspace root.

**CRITICAL: wasm-pack creates a `.gitignore` with `*` inside the output directory.** Delete it before `npm publish` or add an `.npmignore` to the package — otherwise npm excludes the WASM binary.

Verify targeted wasm32 work with `./scripts/repo-cargo check -p <crate>
--target wasm32-unknown-unknown`. Clippy for the runtime remains
`./scripts/repo-cargo clippy -p meerkat-web-runtime --target
wasm32-unknown-unknown -- -D warnings` when debugging locally. The optional
BuildBuddy path exposes `scripts/buildbuddy-dev wasm-check` and
`scripts/buildbuddy-dev wasm-contract` for the CI-equivalent WASM lanes.

## Common Gotchas

1. **Stale WASM binary**: Browser caches WASM aggressively. Nuke Playwright cache between rebuilds.
2. **cargo clean scope**: `cargo clean -p <crate>` only cleans native. Use `rm -rf target/wasm32-unknown-unknown`.
3. **cfg inside async_trait**: May not propagate. Move cfg-gated logic to standalone functions outside the impl.
4. **Feature unioning**: Workspace `tokio = { features = ["full"] }` pulls mio which fails on wasm32. Each crate needs target-specific deps.
5. **MobBuilder**: Requires `.allow_ephemeral_sessions(true)` for the browser profile's in-memory session service. Members still receive `SessionOwned` bindings from the shared machine.
6. **Flow output format**: Without `output_format` or `expected_schema_ref`, a step defaults to text. With a schema and no explicit format, it defaults to JSON; explicit `output_format` selects the format. Require JSON-compatible output for JSON steps rather than assuming every flow output is JSON.

## Subsystem Availability on wasm32

Available: canonical session admission, keep-alive, cancellation, terminal
publication, agent loop, provider fetch transport, in-process comms, local mob
orchestration, browser tools and callbacks, tool scoping, embedded skills,
in-memory config and storage, compaction, multimodal content, and request-only
transient turn context.

`BrowserRuntimeProfile::require` is the availability authority. Its exclusions
include durable persistence, execution after page teardown, host processes,
configured hooks, runtime skill discovery, MCP clients, TCP/UDS comms, remote
member placement, and services the browser composition does not install.
Requests for excluded capabilities return `CAPABILITY_UNAVAILABLE` with typed
`profile`, `capability`, and `clearing_action` data. Consult the profile rather
than adding surface-local availability tables or refusal text.

## Key Files

For detailed API surface and architecture, load: `references/api_surface.md`.

- `crates/meerkat-web-runtime/src/lib.rs` — wasm_bindgen exports (bootstrap, sessions, mob, subscriptions, comms)
- `sdks/web/src/runtime.ts` — `MeerkatRuntime` class (TypeScript wrapper entry point)
- `sdks/web/proxy/index.mjs` — Node.js provider proxy
- `crates/meerkat/src/factory.rs` — `build_agent()` with overrides, `AgentFactory::minimal()`
- `crates/meerkat/src/service_factory.rs` — `FactoryAgentBuilder` with default injection

Files in this skill

  • SKILL.md11 KB
  • references/api_surface.md14.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.