Chio/Docs
LOGIN · JOIN

Run the Chio Kernel in the Browser

Build the portable kernel for a browser page, hand the page a short-lived capability, and preflight each tool call locally.

Guide chio@5f63e6e

On this page

A web page can check each tool call its agent makes against a capability, with no network round trip. The portable kernel compiles to WebAssembly, verifies the capability’s signature, issuer, lifetime, and scope in the tab, and returns its verdict to the page.

The page holds authority a server issued and issues none of its own. A call that passes in the tab comes back pending_approval, and the service that executes the call admits it under its full kernel, with guards, revocation, budgets, and durable receipts.

Start

The tab checks the capability, and the server keeps durable authority

A browser deployment splits trust work between a short-lived kernel in the page and a long-lived authority on a server, which talk over HTTPS on a cadence that never blocks a per-call decision. Decisions in the tab cost no round trip and keep working offline, while the durable checks stay on the server.

Browser kernelChoose an outcome.

κcapabilityσscopegguards: on hostBbudget: on host

The tab preflights the call, and the service admits it

evaluate verifies the capability’s signature, trusted issuer, lifetime, subject, and scope in the tab, with no network round trip. The guard pipeline is empty, so an allow comes back pending_approval with authorized: false, and the service that executes the call admits it under its full kernel, with guards, revocation, budgets, and durable receipts. Architecture: TCB Map.

One tool call from an in-page agent through evaluate in the tab’s WebAssembly kernel to the service that executes it. Capability issuance, revocation, and durable receipt storage stay on the authority and are left out.Portable Kernel

The kernel has two layers. chio-kernel-core is pure computation, with no async runtime, filesystem, network, or ambient authority: every input is an explicit argument, and the only outputs are a verdict, a signed receipt, or a structured error. chio-kernel wraps that core with tokio, SQLite, HTTP transport, revocation stores, and budget persistence. The core compiles to wasm32-unknown-unknown unchanged, so the verdict path inside the desktop sidecar also runs in a browser tab, a Cloudflare Worker, or a mobile app compiled through UniFFI.

DutyIn the tabOn the authoritative host
Signature and trusted issuerCheckedChecked
Lifetime, subject, scopeCheckedChecked
Guard pipelineEmptyRuns
RevocationExpiry onlyRevocation store
Quotas and spendingDeclared limits, read onlyEnforced at admission
Capability issuanceNoneAuthority signs
ReceiptsSigned, held in memoryDurable store

Read the tab’s verdict as a preflight. A capability that is unexpired by its own time bounds passes in the tab even after an upstream revocation, so keep browser capabilities short-lived and let expiry do that work. Transport is the page’s own: there is no HTTP server, stdio pipe, or MCP adapter in the bundle, and the page calls entry points as JavaScript functions.

Read more in Portable Kernel and Architecture: TCB Map.

1. Build the browser bundle

Install Rust with the wasm32-unknown-unknown target and wasm-pack, the supported build path; Build from source selects the toolchain. In a clone of the Chio repository, build the web target:

browser-kernel · buildtranscript
$ wasm-pack build --target web --release crates/kernel/chio-kernel-browser \
    | grep -v 'newer version of wasm-pack'
[INFO]: 🎯  Checking for the Wasm target...
[INFO]: 🌀  Compiling to Wasm...
    Finished `release` profile [optimized] target(s) in 0.49s
[INFO]: License key is set in Cargo.toml but no LICENSE file(s) were found; Please add the LICENSE file(s) to your project directory
[INFO]: ✨   Done in 0.88s
[INFO]: 📦   Your wasm pkg is ready to publish at crates/kernel/chio-kernel-browser/pkg.
exit 0in Chio source checkout

You should see the bundle land in crates/kernel/chio-kernel-browser/pkg. The filter drops one line, wasm-pack’s notice about its own newer release, and the license line belongs to the crate. Inside the crate directory, list what the build wrote:

browser-kernel · pkgtranscript
$ ls -1 pkg
README.md
chio_kernel_browser.d.ts
chio_kernel_browser.js
chio_kernel_browser_bg.wasm
chio_kernel_browser_bg.wasm.d.ts
package.json
exit 0in Chio source checkout / crates/kernel/chio-kernel-browser

chio_kernel_browser_bg.wasm is the compiled kernel, chio_kernel_browser.js the ES-module glue, the two .d.ts files the TypeScript declarations for the glue and the module’s raw exports, and package.json an npm-publishable manifest for a bundler that understands ES modules. The managed package @chio-protocol/browser wraps this output and exports verify_receipt with a verifyReceiptHex helper; the steps below import pkg/ directly for all seven entry points.

2. Load the module once per page

Import the glue from a bundler or a static file server. It exposes the seven #[wasm_bindgen] entry points the crate declares:

src/chio-kernel.tstypescript
import init, {
  evaluate,
  sign_receipt,
  sign_receipt_relaying_trusted_body,
  verify_capability,
  verify_capability_with_context,
  verify_receipt,
  mint_signing_seed_hex,
} from "./pkg/chio_kernel_browser.js";

// Call once per page load. init() fetches and instantiates the wasm module.
await init();

// The seven entry points are now callable.
export {
  evaluate,
  sign_receipt,
  sign_receipt_relaying_trusted_body,
  verify_capability,
  verify_capability_with_context,
  verify_receipt,
  mint_signing_seed_hex,
};

Await init() once to fetch and instantiate the module. Every entry point after it is synchronous and returns its result by value. The steps below call evaluate, sign_receipt, and mint_signing_seed_hex, and the reference lists the other four.

3. Issue the page a short-lived capability

Every browser entry point takes a signed capability as input, so issuance lives on a server. The authority signs a capability scoped to the page’s session and hands it to the tab over a trusted channel, here for one hour and 100 invocations of one tool:

server/src/session.rsrust
// Server-side: a Chio authority issues a short-lived capability
// and returns it to the browser over a trusted channel.
use chio_core_types::capability::{
    scope::{ChioScope, Operation, ToolGrant},
    token::{CapabilityToken, CapabilityTokenBody},
};
use chio_core_types::crypto::Keypair;

pub fn mint_session_capability(
    authority: &Keypair,
    agent_subject: &Keypair,
    now_unix_secs: u64,
) -> CapabilityToken {
    let scope = ChioScope {
        grants: vec![ToolGrant {
            server_id: "web".to_string(),
            tool_name: "search".to_string(),
            operations: vec![Operation::Invoke],
            constraints: vec![],
            max_invocations: Some(100),
            max_cost_per_invocation: None,
            max_total_cost: None,
            dpop_required: None,
        }],
        resource_grants: vec![],
        prompt_grants: vec![],
    };
    let body = CapabilityTokenBody {
        id: format!("cap-{}", uuid::Uuid::new_v4()),
        issuer: authority.public_key(),
        subject: agent_subject.public_key(),
        scope,
        issued_at: now_unix_secs,
        expires_at: now_unix_secs + 3600, // one hour
        delegation_chain: vec![],
        aggregate_invocation_budget: None,
    };
    CapabilityToken::sign(body, authority).expect("sign capability")
}

The capability submodules are the public API, so the import names the domain: grants and scopes come from capability::scope, the token and its body from capability::token. Give the tab the exact tools, scoped paths or hosts, a tight expiry, and a capped invocation count: a page compromised by XSS, a malicious extension, or a stolen session token inherits exactly the privilege the capability encodes.

Hold the capability in a variable for the life of the tab. localStorage and sessionStorage are readable by any script in the origin and by extensions with broad permissions, and a capability loaded from them outlives the tab that needed it.

4. Evaluate each tool call before it leaves the page

The in-page agent can be a browser-side call loop, an extension background script, or a CopilotKit host. Send every tool call it wants to make through evaluate, with the capability and the trusted issuer keys:

src/agent.tstypescript
import { evaluate } from "./chio-kernel.js";

// The capability you received from the server at session start.
const capability = sessionCapability;

// The authority that signed it (hex-encoded Ed25519 public key).
const TRUSTED_ISSUERS_HEX = [AUTHORITY_PUBLIC_KEY_HEX];

export interface ToolCall {
  request_id: string;
  tool_name: string;
  server_id: string;
  agent_id: string;     // hex-encoded agent public key
  arguments: unknown;
}

export function evaluateCandidate(call: ToolCall) {
  const envelope = {
    request: call,
    capability,
    trusted_issuers_hex: TRUSTED_ISSUERS_HEX,
  };

  const verdict = evaluate(JSON.stringify(envelope));

  // "deny" is the only terminal negative. A capability that passed the
  // signature, time, subject, and scope checks comes back as
  // "pending_approval" (authorized: false), because the in-page kernel
  // cannot itself authorize execution.
  if (verdict.verdict === "deny") {
    throw new Error(
      `denied: ${verdict.reason ?? "no reason given"}`,
    );
  }

  // A pending result still needs authoritative host admission.
  return verdict;
}

You should see one of two verdicts. The browser build runs an empty guard pipeline, so a raw kernel allow is downgraded to pending_approval with authorized: false, and deny is the only terminal negative. Treat deny as the failure signal; the raw decision stays on capability_verdict for diagnostics.

A denial’s reason names the failed check, such as capability has expired or capability issuer is not a trusted CA, from KernelCoreError::deny_reason, stable enough to switch on. verify_capability reports the same failures in its own wording, so match on the path you call.

5. Prove the three verdicts from Node

Build the same crate for the Node target and drive it with a short script against the signed capability vector the repository ships at tests/bindings/vectors/capability/v1.json:

browser-kernel · build-nodetranscript
$ wasm-pack build --target nodejs --release crates/kernel/chio-kernel-browser \
    | grep -v 'newer version of wasm-pack'
[INFO]: 🎯  Checking for the Wasm target...
[INFO]: 🌀  Compiling to Wasm...
    Blocking waiting for file lock on package cache
    Blocking waiting for file lock on package cache
    Blocking waiting for file lock on package cache
    Finished `release` profile [optimized] target(s) in 0.99s
[INFO]: License key is set in Cargo.toml but no LICENSE file(s) were found; Please add the LICENSE file(s) to your project directory
[INFO]: ✨   Done in 1.40s
[INFO]: 📦   Your wasm pkg is ready to publish at crates/kernel/chio-kernel-browser/pkg.
exit 0in Chio source checkout
The diagnostic script, bk-probe.js
bk-probe.jsjavascript
const crypto = require("node:crypto");
const fs = require("node:fs");
const kernel = require(process.env.CHIO_BROWSER_PKG ?? "./pkg/chio_kernel_browser.js");

// serde-wasm-bindgen hands arbitrary JSON back as a JS Map, which
// JSON.stringify renders as {}. Convert before printing or serializing.
const plain = (value) =>
  value instanceof Map
    ? Object.fromEntries([...value].map(([k, v]) => [k, plain(v)]))
    : Array.isArray(value)
      ? value.map(plain)
      : value && typeof value === "object"
        ? Object.fromEntries(Object.entries(value).map(([k, v]) => [k, plain(v)]))
        : value;

const show = (value) => console.log(JSON.stringify(plain(value), null, 2));
const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");

const vectors = JSON.parse(fs.readFileSync(process.env.CHIO_CAPABILITY_VECTORS, "utf8"));
const capability = vectors.cases
  .find((c) => c.id === "valid_no_delegation_chain").capability;
const grant = capability.scope.grants[0];

const request = {
  request_id: "req-1",
  tool_name: grant.tool_name,
  server_id: grant.server_id,
  agent_id: capability.subject,
  arguments: { path: "/workspace/notes.md" },
};

const envelope = (overrides) =>
  JSON.stringify({
    request,
    capability,
    trusted_issuers_hex: [capability.issuer],
    clock_override_unix_secs: 1710000400,
    ...overrides,
  });

// A fixed seed keeps this reproducible. A browser mints one per receipt.
const SEED_HEX = "22".repeat(32);
const CANONICAL_CONTENT = Buffer.from(JSON.stringify({ shown: "to-the-human" }));
const PARAMETERS = { path: "/workspace/notes.md" };

const receiptBody = (drop) => {
  const body = {
    id: "browser-rcpt-1",
    timestamp: 1710000400,
    capability_id: capability.id,
    tool_server: grant.server_id,
    tool_name: grant.tool_name,
    action: { parameters: PARAMETERS, parameter_hash: sha256(JSON.stringify(PARAMETERS)) },
    decision: { verdict: "allow" },
    receipt_kind: "mediated_decision",
    boundary_class: "prevent",
    tool_origin: "caller_executed",
    redaction_mode: "none",
    content_hash: sha256(CANONICAL_CONTENT),
    policy_hash: "00".repeat(32),
    evidence: [],
    trust_level: "mediated",
    kernel_key: "00".repeat(32),
  };
  if (drop) delete body[drop];
  return JSON.stringify({ body, canonical_content: Array.from(CANONICAL_CONTENT) });
};

const attempt = (run) => {
  try {
    show(run());
  } catch (error) {
    show(JSON.parse(JSON.stringify(error, Object.getOwnPropertyNames(error ?? {}))));
  }
};

switch (process.argv[2]) {
  case "allow":
    show(kernel.evaluate(envelope()));
    break;
  case "untrusted":
    show(kernel.evaluate(envelope({ trusted_issuers_hex: ["11".repeat(32)] })));
    break;
  case "out-of-scope":
    show(kernel.evaluate(envelope({ request: { ...request, tool_name: "file_write" } })));
    break;
  case "seed":
    attempt(() => kernel.mint_signing_seed_hex());
    break;
  case "receipt":
    attempt(() => kernel.sign_receipt(receiptBody(), SEED_HEX));
    break;
  case "receipt-incomplete":
    attempt(() => kernel.sign_receipt(receiptBody("receipt_kind"), SEED_HEX));
    break;
}

Point CHIO_BROWSER_PKG at the glue file the build wrote and CHIO_CAPABILITY_VECTORS at the vector file, then run one mode at a time. The request envelope’s optional clock_override_unix_secs pins the clock in place of Date.now(), the way the native test suite exercises expiry; keep it to tests.

The vector’s capability grants file_read on srv-files under a /workspace/ path prefix, and the pinned clock sits inside its validity window. Every check passes, and the verdict is still pending:

browser-kernel · allowtranscript
$ node bk-probe.js allow
{
  "verdict": "pending_approval",
  "capability_verdict": "allow",
  "reason": "capability-only browser evaluation requires a mediated prevent receipt before execution",
  "authorized": false,
  "authorization_basis": "capability_only",
  "guards_evaluated": false,
  "matched_grant_index": 0,
  "subject_hex": "0b513ad9b4924015ca0902ed079044d3ac5dbec2306f06948c10da8eb6e39f2d",
  "issuer_hex": "66be7e332c7a453332bd9d0a7f7db055f5c5ef1a06ada66d98b39fb6810c473a",
  "capability_id": "cap-bindings-direct",
  "evaluated_at": 1710000400
}
exit 0

You should see capability_verdict allow, a top-level pending_approval, authorized false, and authorization_basis capability_only, with the five optional identity fields filled because the capability verified. Swap in an issuer key that did not sign the token, and the identity fields vanish with the verification:

browser-kernel · untrustedtranscript
$ node bk-probe.js untrusted
{
  "verdict": "deny",
  "capability_verdict": "deny",
  "reason": "capability issuer is not a trusted CA",
  "authorized": false,
  "authorization_basis": "denied",
  "guards_evaluated": false
}
exit 0deny

Ask for a tool the grant does not cover, and the capability still verifies, so the identity fields survive while the scope matcher refuses:

browser-kernel · out-of-scopetranscript
$ node bk-probe.js out-of-scope
{
  "verdict": "deny",
  "capability_verdict": "deny",
  "reason": "requested tool file_write on server srv-files is not in capability scope",
  "authorized": false,
  "authorization_basis": "denied",
  "guards_evaluated": false,
  "subject_hex": "0b513ad9b4924015ca0902ed079044d3ac5dbec2306f06948c10da8eb6e39f2d",
  "issuer_hex": "66be7e332c7a453332bd9d0a7f7db055f5c5ef1a06ada66d98b39fb6810c473a",
  "capability_id": "cap-bindings-direct",
  "evaluated_at": 1710000400
}
exit 0deny

6. Sign the evaluation as an advisory record

The page can sign an advisory record of its evaluation, bound to the exact canonical_content bytes your application supplies. The signer checks their hash against body.content_hash, and the record carries the evaluation in metadata with no execution decision:

src/receipts.tstypescript
import { sign_receipt, mint_signing_seed_hex } from "./chio-kernel.js";

// ToolCall and EvaluationVerdict are the application types shown above.
export function recordEvaluation(
  verdict: EvaluationVerdict,
  call: ToolCall,
  policyHash: string,
  parameterHash: string,
  contentHash: string,
  // The exact bytes content_hash was derived from. sign_receipt recomputes
  // sha256_hex(canonicalContent) and refuses if it disagrees with content_hash.
  canonicalContent: Uint8Array,
) {
  // Mint a fresh seed per receipt. The kernel rejects zero-filled seeds.
  const seedHex = mint_signing_seed_hex();

  const body = {
    // Rewritten by the signer to the content-addressed id and kept as the
    // signing nonce, so pass something unique per receipt rather than a
    // constant.
    id: crypto.randomUUID(),
    timestamp: Math.floor(Date.now() / 1000),
    capability_id: verdict.capability_id ?? "",
    tool_server: call.server_id,
    tool_name: call.tool_name,
    action: { parameters: call.arguments, parameter_hash: parameterHash },
    receipt_kind: "advisory_evaluation",
    boundary_class: "advisory_only",
    observation_outcome: "evaluated",
    tool_origin: "host_executed_unmediated",
    redaction_mode: "none",
    content_hash: contentHash,
    policy_hash: policyHash,
    evidence: [],
    trust_level: "advisory",
    metadata: { browser_evaluation: verdict },
    // Rewritten by sign_receipt to the seed's public key, but the field is
    // not optional: send hex, not null.
    kernel_key: "00".repeat(32),
  };

  // canonical_content crosses the wasm-bindgen boundary as a JSON array of u8.
  return sign_receipt(
    JSON.stringify({ body, canonical_content: Array.from(canonicalContent) }),
    seedHex,
  );
}

Advisory records require advisory_evaluation, advisory_only, an observation outcome, and advisory trust, and a pending evaluation stays pending when you sign it. sign_receipt_relaying_trusted_body accepts an already-minted body without its preimage, for a relay you choose deliberately. The diagnostic script signs a synthetic body with a mediated classification as a test input; omitting a required field fails parsing:

browser-kernel · receipt-incompletetranscript
$ node bk-probe.js receipt-incomplete
{
  "code": "invalid_json_input",
  "message": "sign_receipt body: missing field `receipt_kind` at line 1 column 677"
}
exit 0

With a complete body, the signer returns the receipt, having rewritten id to the content address and kernel_key to the seed’s public key, and moved the id you supplied into metadata.chio_receipt_signing_nonce:

browser-kernel · receipttranscript
$ node bk-probe.js receipt
{
  "id": "55af714bcc1a7d1bd4973742a3cb96ab58b1a04067bd0c288e54bb5726eab3fd",
  "timestamp": 1710000400,
  "capability_id": "cap-bindings-direct",
  "tool_server": "srv-files",
  "tool_name": "file_read",
  "action": {
    "parameters": {
      "path": "/workspace/notes.md"
    },
    "parameter_hash": "d358b22b4ff6f0515ebb0f5219842ac08c57d468c8f1d6d6bec0927dafb706a2"
  },
  "decision": {
    "verdict": "allow"
  },
  "receipt_kind": "mediated_decision",
  "boundary_class": "prevent",
  "tool_origin": "caller_executed",
  "redaction_mode": "none",
  "content_hash": "96fb4bce475dfb30cc2543e50f7ed86f0c1ac601beebea2c9e87841e54009c02",
  "policy_hash": "0000000000000000000000000000000000000000000000000000000000000000",
  "metadata": {
    "chio_receipt_signing_nonce": "browser-rcpt-1"
  },
  "trust_level": "mediated",
  "kernel_key": "a09aa5f47a6759802ff955f8dc2d2a14a5c99d23be97f864127ff9383455a4f0",
  "signature": "0dccc87c83c86642825fc09603ed94c9483dc32ddd311bfaba9c781dda04efbe8accd72d4de2f838942c6424bc8c36133a77561225b3c532f6f8fb8297c88102"
}
exit 0allow

Convert the returned value before sending it anywhere. serde-wasm-bindgen returns arbitrary JSON as JavaScript Maps, which JSON.stringify renders as {}, so a receipt posted as-is arrives with empty action.parameters and fails parameter-hash verification although it was signed correctly. The reference lists three patterns for where page receipts go.

A compromised page holds exactly the capability it was given

The browser is a hostile place for key material. Extensions with the right permissions read page memory, XSS exfiltrates anything in localStorage or a closure, and a malicious bundler injects code at build time. Two rules bound what a compromised tab can do.

  • The page’s signer is ephemeral. The example mints a fresh seed for each evaluation record and keeps none. Code that controls the page can still read or replace the evaluation and signing inputs, so the receiving service applies its own admission and trust rules.
  • The authority’s signer stays on the server. The root of trust for capability issuance never enters the browser. The page sees only capabilities the server signed, and the trust control plane can revoke them or refuse to reissue at any time.

verify_receipt sets ok only when the signature verifies, the parameter hash matches, the receipt id is the content address, and the receipt’s kernel_key is in a non-empty trusted-issuer set. A freshly minted key is in no such set, so a page receipt reads ok: false with the first three checks true. Decide up front whether the receiving service pins issuers, which gives the page a delegated signer it can register, or treats browser receipts as evidence.

Read more in Secrets & Signing Keys, which covers key custody for the deployment.

Browser kernel reference

Entry points

chio-kernel-browser exports these entry points. Each deserializes JSON, calls into chio-kernel-core, and serializes the result back, keeping no state across calls.

  • verify_capability checks the signature, walks the trusted issuer set, and confirms the validity interval is live against Date.now(). verify_capability_with_context adds trust roots and parent-budget snapshots.
  • evaluate runs the synchronous capability path: signature, time, subject binding, and a scope match by the kernel-core matcher. With an empty guard pipeline, its top-level verdict is pending_approval or deny.
  • sign_receipt accepts a ChioReceiptBody paired with its exact canonical_content preimage and an Ed25519 seed, and returns a signed ChioReceipt. A body without the preimage is refused.
  • sign_receipt_relaying_trusted_body signs a body an upstream producer already minted, without its preimage.
  • verify_receipt checks the signature, parameter hash, content-addressed id, and signer trust.
  • mint_signing_seed_hex fills a seed through the WebCryptoRng adapter from window.crypto.getRandomValues, with a fail-closed zero-seed guard so receipts are never signed with degraded entropy.

What the tab leaves to the host

  • Receipt persistence. The page decides where each signed receipt goes: IndexedDB, a remote trust control plane, or a best-effort post to an analytics endpoint.
  • Revocation. The evaluator has no revocation store; short capability lifetimes bound the exposure.
  • Budgets. The request path keeps no persistent budget counter or monetary reservation. The page can read declared limits, and the authoritative host enforces quotas and spending when it admits execution.
  • Guards. evaluate passes an empty guard list into the core, so a decision in the tab rests on capability scope and time bounds. Guards run on a sidecar or the executing service.
  • Durable state. Revocation, budgets, and receipt persistence live in a component outside the tab, typically a trust control plane the page reaches on a slower cadence than per request.

Where page receipts go

  • Batch to the trust control plane. Queue signed receipts in memory and flush them to a server endpoint every N seconds, or on tab close with navigator.sendBeacon; the server persists them to the durable receipt store.
  • IndexedDB with a periodic checkpoint. Hold receipts in IndexedDB while offline and upload the batch when a connection returns, for extensions and PWAs that run disconnected.
  • Sign and forward. Attach the signed receipt to a response the agent already sends to a server, which verifies it alongside the result and persists it.

EvaluationVerdict

The verdict envelope is a plain JavaScript object. Once the capability checks pass, it also carries the verified capability identity:

EvaluationVerdicttypescript
export interface EvaluationVerdict {
  // Browser authority decision. Never "allow" from an in-page evaluate:
  // a raw kernel allow is downgraded to "pending_approval".
  verdict: "deny" | "pending_approval";
  // Raw kernel capability + scope verdict before the downgrade.
  // Diagnostic only, not an execution-authorization signal.
  capability_verdict: "allow" | "deny" | "pending_approval";
  reason?: string;
  // Always false for in-page evaluation (no guard pipeline ran).
  authorized: boolean;
  // Machine-readable authorization state, e.g. "capability_only", "denied".
  authorization_basis: string;
  // Whether a guard pipeline participated. Always false here.
  guards_evaluated: boolean;
  matched_grant_index?: number;
  subject_hex?: string;
  issuer_hex?: string;
  capability_id?: string;
  evaluated_at?: number;
}

Error shape

Every entry point fails with a structured error: an object with a code and a message.

BindingErrortypescript
interface BindingError {
  code: string;    // machine-readable
  message: string; // human-readable
}

// Error codes surfaced by chio-kernel-browser:
//   invalid_json_input
//   invalid_issuer_hex
//   invalid_seed_hex
//   invalid_authority_input
//   invalid_budget_snapshot
//   capability_verification_failed
//   canonical_content_required     // sign_receipt without a preimage
//   receipt_signing_failed
//   weak_entropy
//   webcrypto_unavailable
//   encode_result_failed
//   unsupported_authorization_extension   // evaluate, on a request carrying
//                                         // governed approvals or supplemental
//                                         // authorization
//   // verify_receipt-specific:
//   invalid_receipt_envelope
//   invalid_trusted_issuers
//   receipt_id_check_failed
//   parameter_hash_check_failed
//   signature_check_failed

weak_entropy refuses a zero-filled seed. webcrypto_unavailable means the entropy helper cannot reach Window.crypto. Evaluation and signing with a supplied seed run in any WASM host, so a Node or worker host supplies its own securely generated seed to sign_receipt; the diagnostic script’s fixed seed is a reproducible test input.

browser-kernel · seedtranscript
$ node bk-probe.js seed
{
  "code": "webcrypto_unavailable",
  "message": "Web Crypto API unavailable: no Window global is available"
}
exit 0

Failures

  • Seed minting throws webcrypto_unavailable. The helper needs Window.crypto. In another WASM host, supply a seed from that host’s secure generator and its key-custody policy, and keep deterministic seeds in tests.
  • sign_receipt throws invalid_json_input naming a field. Add the field and match the receipt classification: a mediated receipt requires a decision, and an advisory record requires an observation outcome and omits the decision.
  • sign_receipt throws canonical_content_required. The payload carried a body and no preimage. Send the exact bytes content_hash was derived from, or use the relay entry point for a body an upstream producer minted.
  • A receipt passes three checks and reports ok: false. The signer is outside the verifier’s trusted-issuer set, as an ephemeral browser seed always is. Pin a signer the page can reuse, or read the receipt as evidence.
  • A posted receipt fails parameter-hash verification. The returned action.parameters is a Map and serialized as an empty object. Convert Maps to plain objects before sending.
  • evaluate throws unsupported_authorization_extension. The request carried a governed intent, an approval token, a threshold proposal, or supplemental authorization, which the browser build cannot authenticate. Send that request to a full kernel.
  • A revoked capability passes in the tab. The page has no revocation store. Keep capabilities short-lived and let expiry do the work.

Node and edge targets

The build script drives three wasm-pack targets: web, bundler, and nodejs. The Cloudflare Workers SDK, @chio-protocol/workers, is built from the bundler package, and the Vercel Edge and Deno SDKs, @chio-protocol/edge and @chio-protocol/deno, reuse the web package. The JavaScript API is identical across them.

Next: see where the portable core runs