This document explains the parts of the ocap kernel that are relevant to building services and vats in a host application. It is intended for an agent or developer who needs to register kernel services, write vat code, and wire up system subclusters — without needing to understand the kernel's internals.
- Core Concepts
- The Kernel API
- Writing Vat Code
- Vat Endowments
- Kernel Services
- System Subclusters
- Eventual Send with E()
- Exos (Remotable Objects)
- Baggage (Persistent State)
- Revocation
- Glossary and Key Types
The kernel is a centralized manager of vats and distributed objects. It routes messages between vats, manages object references, handles persistence, and performs garbage collection.
A vat is an isolated unit of computation — think of it as a worker process. User code runs inside vats. Vats communicate with each other and with the kernel through asynchronous message passing. You never call methods on objects in other vats directly; you use E() (eventual send) to queue messages.
A subcluster is a logically related group of vats that are launched together. When you launch a subcluster, all its vats start, and then the bootstrap vat receives references to the other vats and to any kernel services the subcluster requested.
A system subcluster is a subcluster declared at kernel startup time. System subclusters can access privileged services (marked systemOnly) that regular subclusters cannot.
A kernel service is an object registered with the kernel that vats can call via E(). Services run outside of vats — they execute in the kernel's own context. Examples include the kernel facet (privileged kernel operations) and IO services.
A kref (kernel reference) is a string like ko42 that uniquely identifies an object within the kernel. Krefs are the kernel's internal addressing system. When a vat exports an object or receives a reference to one, the kernel assigns and tracks krefs.
An exo is a remotable object created with makeDefaultExo() (or the lower-level @endo/exo APIs). Exos are the standard way to create objects that can be passed between vats, stored in baggage, and invoked via E().
The kernel is instantiated by the host application via Kernel.make():
import { Kernel } from '@metamask/ocap-kernel';
const kernel = await Kernel.make(platformServices, kernelDatabase, {
logger,
systemSubclusters: [{ name: 'my-system', config: clusterConfig }],
});| Method | Description |
|---|---|
registerKernelServiceObject(name, object, options?) |
Register a kernel service that vats can call via E(). |
launchSubcluster(config) |
Launch a new subcluster of vats. Returns { subclusterId, rootKref, bootstrapResult }. |
terminateSubcluster(subclusterId) |
Terminate a subcluster and all its vats. |
queueMessage(target, method, args) |
Send a message to a kernel object (identified by kref). |
getStatus() |
Get current kernel status (vats, subclusters, remote comms state). |
revoke(kref) |
Revoke an object. Any future E() calls to it will fail. |
isRevoked(kref) |
Check if an object has been revoked. |
getPresence(kref, iface?) |
Convert a kref string to a slot value (presence) for use in messages. |
stop() |
Gracefully stop the kernel. |
reset() |
Stop all vats and reset kernel state (debugging only). |
Every vat exports a buildRootObject function. This is the entry point for the vat's code.
import { makeDefaultExo } from '@metamask/kernel-utils/exo';
import type { Baggage } from '@metamask/ocap-kernel';
export function buildRootObject(
vatPowers: unknown,
parameters: Record<string, unknown>,
baggage: Baggage,
) {
return makeDefaultExo('root', {
async bootstrap(
vats: Record<string, unknown>,
services: Record<string, unknown>,
): Promise<void> {
// Called once when the subcluster is first launched.
// `vats` contains references to other vats in the subcluster.
// `services` contains references to kernel services requested in the cluster config.
},
async myMethod(arg: string): Promise<string> {
return `hello ${arg}`;
},
});
}vatPowers— Special powers provided to the vat (e.g., a logger). Contents vary by vat configuration.parameters— Static parameters from the vat's config (VatConfig.parameters). Useful for passing configuration like names or settings.baggage— Persistent key-value storage that survives vat restarts. See Baggage.
The bootstrap method is called exactly once when the subcluster is first launched. It receives:
vats— A record mapping vat names to their root object references. UseE()to call methods on them.services— A record mapping service names to kernel service references. UseE()to call methods on them.
async bootstrap(
vats: { alice: unknown; bob: unknown },
services: { kernelFacet: unknown; myService: unknown },
): Promise<void> {
// Store service references in baggage for use after restart
baggage.init('kernelFacet', services.kernelFacet);
// Communicate with other vats
const greeting = await E(vats.alice).hello('world');
}After a vat restart (resuscitation), bootstrap is not called again. The vat must restore its state from baggage.
SES Compartments do not expose host or Web APIs by default — setTimeout, Date.now(), crypto, Math.random(), URL, and friends are either absent or deliberately tamed (e.g., Date.now() and Math.random() throw in secure mode). Vats that need them must request them explicitly through the globals field on their VatConfig.
Name the host/Web APIs the vat needs in its cluster config:
await kernel.launchSubcluster({
bootstrap: 'worker',
vats: {
worker: {
bundleSpec: '...',
parameters: {},
globals: ['setTimeout', 'clearTimeout', 'Date', 'crypto', 'URL'],
},
},
});Only names in the vat's globals array are installed in the vat's compartment. Names not in the kernel's allowed set cause initVat to fail with Vat "<id>" requested unknown global "<name>".
The kernel ships with the following set, sourced from @metamask/snaps-execution-environments:
| Name | Category | Notes |
|---|---|---|
setTimeout |
Timer (attenuated) | Isolated per vat. Cancelled automatically on vat termination. |
clearTimeout |
Timer (attenuated) | Only clears timers created by the same vat. |
setInterval |
Timer (attenuated) | Isolated per vat. Cancelled automatically on vat termination. |
clearInterval |
Timer (attenuated) | Only clears intervals created by the same vat. |
Date |
Attenuated | Each Date.now() read adds up to 1 ms of random jitter, clamped monotonic non-decreasing; precise sub-millisecond timing cannot leak through. |
Math |
Attenuated | Math.random() is sourced from crypto.getRandomValues. Not a CSPRNG per the upstream NOTE — defends against stock-RNG timing side channels only. |
crypto |
Web Crypto | Hardened Web Crypto API. |
SubtleCrypto |
Web Crypto | Hardened Web Crypto API. |
TextEncoder |
Text codec | Plain hardened. |
TextDecoder |
Text codec | Plain hardened. |
URL |
URL | Plain hardened. |
URLSearchParams |
URL | Plain hardened. |
atob |
Base64 | Plain hardened. |
btoa |
Base64 | Plain hardened. |
AbortController |
Abort | Plain hardened. |
AbortSignal |
Abort | Plain hardened. |
"Plain hardened" means the value is the host's implementation wrapped with harden() — it behaves identically to the browser/Node version. "Attenuated" means the value is a deliberate reimplementation with different semantics; the Notes column flags the relevant differences. The canonical list lives in endowments.ts.
Two levers, applied at different layers:
1. Kernel-level allowlist via Kernel.make({ allowedGlobalNames }). Restrict the set of globals any vat is permitted to request. Names outside this list cause initVat to fail even if the vat asks for them.
const kernel = await Kernel.make(platformServices, db, {
allowedGlobalNames: ['TextEncoder', 'TextDecoder', 'URL'],
});Omit the option to allow the full default set.
2. Supervisor-level factory via VatSupervisor({ makeAllowedGlobals }). Replace the endowment factory entirely — useful for tests or host applications that need custom attenuations. The factory is invoked once per supervisor and must return { globals, teardown }, where teardown releases any resources the custom endowments hold.
import type { VatEndowments } from '@metamask/ocap-kernel';
const makeAllowedGlobals = (): VatEndowments =>
harden({
globals: harden({ Date: globalThis.Date }),
teardown: async () => undefined,
});
new VatSupervisor({
id,
kernelStream,
logger,
makeAllowedGlobals,
});Most host applications should stick with the default createDefaultEndowments() factory; override only when you need bespoke attenuations.
A kernel service is a JavaScript object registered with the kernel. Vats interact with it via E() just like any other remote object, but it runs in the kernel's own context (not in a vat).
import { makeDefaultExo } from '@metamask/kernel-utils/exo';
const myService = makeDefaultExo('myService', {
async doSomething(arg: string): Promise<string> {
// This runs in the kernel's context, not in a vat.
// You have full access to the host application's APIs here.
return `result: ${arg}`;
},
});
// Register before launching subclusters that need it
kernel.registerKernelServiceObject('myService', myService);kernel.registerKernelServiceObject('privilegedService', serviceObj, {
systemOnly: true,
});System-only services can only be accessed by system subclusters. Regular subclusters that request them will get an error at launch time.
Services are requested by name in the services array of the cluster config:
const config: ClusterConfig = {
bootstrap: 'myVat',
services: ['myService', 'kernelFacet'],
vats: {
myVat: { sourceSpec: './my-vat.ts' },
},
};The kernel validates that all requested services exist (and are accessible) before launching the subcluster. If validation fails, the launch throws.
import { E } from '@endo/eventual-send';
export function buildRootObject(
_vatPowers: unknown,
_params: unknown,
baggage: Baggage,
) {
let myService: unknown;
return makeDefaultExo('root', {
async bootstrap(_vats: unknown, services: { myService: unknown }) {
myService = services.myService;
baggage.init('myService', myService); // persist for restarts
},
async doWork() {
const result = await E(myService).doSomething('hello');
console.log(result); // "result: hello"
},
});
}When a vat calls E(service).method(args):
- The vat issues a syscall sending a message to the service's kref.
- The kernel's router recognizes the target as a kernel service (not a vat).
- The KernelServiceManager deserializes the message and calls the method on the service object directly.
- The result (or error) is resolved as a kernel promise, which is delivered back to the vat as a notification.
This is transparent to the vat — it just looks like a normal E() call.
System subclusters are declared at kernel startup and have special properties:
- They can access
systemOnlyservices (like the kernel facet). - They persist across kernel restarts — the kernel restores them automatically.
- They are identified by a unique name (not just a subcluster ID).
- The host application can retrieve the bootstrap root kref via
kernel.getSystemSubclusterRoot(name).
const kernel = await Kernel.make(platformServices, kernelDatabase, {
systemSubclusters: [
{
name: 'my-system-subcluster',
config: {
bootstrap: 'controllerVat',
services: ['kernelFacet', 'myHostService'],
vats: {
controllerVat: {
sourceSpec: './controller-vat.ts',
parameters: { name: 'controller' },
},
},
},
},
],
});The kernel facet is a built-in systemOnly service that gives system vats access to privileged kernel operations:
| Method | Description |
|---|---|
getStatus() |
Get kernel status |
getSubclusters() |
List all subclusters |
getSubcluster(id) |
Get a specific subcluster |
launchSubcluster(config) |
Launch a new subcluster |
terminateSubcluster(id) |
Terminate a subcluster |
getSystemSubclusterRoot(name) |
Get a system subcluster's root kref |
queueMessage(target, method, args) |
Send a message to any kernel object |
getPresence(kref, iface?) |
Convert a kref to a presence |
pingVat(vatId) |
Ping a vat |
reset() |
Reset the kernel (debugging) |
ping() |
Returns 'pong' |
Usage from a system vat:
import { E } from '@endo/eventual-send';
// In bootstrap:
const kernelFacet = services.kernelFacet;
// Later:
const status = await E(kernelFacet).getStatus();
const { subclusterId } = await E(kernelFacet).launchSubcluster(config);
await E(kernelFacet).terminateSubcluster(subclusterId);E() from @endo/eventual-send is the standard way to send messages to remote objects (objects in other vats or kernel services). It returns a promise for the result.
import { E } from '@endo/eventual-send';
// Basic call — always returns a promise
const result = await E(remoteObject).methodName(arg1, arg2);
// Fire-and-forget (don't await)
E(remoteObject).notifyOfSomething(data);
// Error handling
try {
await E(remoteObject).riskyMethod();
} catch (error) {
console.error('Remote call failed:', error);
}Rules:
- Always use
E()when calling methods on objects from other vats or kernel services. E()can also be used on local objects — it just queues the call for the next microtask.E()can be used on promises that resolve to objects:E(promise).method()will wait for the promise to resolve, then send the message.- Never call methods directly on remote references (e.g.,
remoteObject.method()won't work).
An exo is a remotable object — one that can be passed between vats, stored in baggage, and invoked via E(). All objects that participate in the kernel's object capability system must be exos.
import { makeDefaultExo } from '@metamask/kernel-utils/exo';
const myObject = makeDefaultExo('MyObject', {
greet(name: string): string {
return `hello ${name}`;
},
async fetchData(id: string): Promise<unknown> {
// async methods work too
return someAsyncOperation(id);
},
});The first argument is a name (used for debugging and interface identification). The second is an object of methods. makeDefaultExo wraps @endo/exo's makeExo with permissive default guards (accepting any "passable" arguments).
- Remotable: Can be sent to other vats as arguments or return values.
- Durable: Can be stored in baggage and survive vat restarts.
- Hardened: Exos are automatically frozen/hardened — their methods cannot be modified after creation.
- Interface-guarded: Method arguments are validated against an interface guard (default: "passable", which accepts most serializable values).
This codebase uses makeDefaultExo instead of Far() from @endo/far. Do not use Far().
Baggage is a durable key-value store provided to each vat. Data stored in baggage survives vat restarts (resuscitation). Baggage is the primary mechanism for vat state persistence.
// Check if a key exists
baggage.has('myKey'); // boolean
// Initialize a new key (throws if key already exists)
baggage.init('myKey', value);
// Get a value (throws if key doesn't exist)
baggage.get('myKey'); // unknown — cast to expected type
// Update an existing key (throws if key doesn't exist)
baggage.set('myKey', newValue);
// Delete a key
baggage.delete('myKey');export function buildRootObject(_vp: unknown, _p: unknown, baggage: Baggage) {
// Restore state from baggage, or initialize if first run
let counter: number;
if (baggage.has('counter')) {
counter = baggage.get('counter') as number;
} else {
counter = 0;
baggage.init('counter', counter);
}
// Restore service references from baggage (for resuscitation)
let myService: unknown = baggage.has('myService')
? baggage.get('myService')
: undefined;
return makeDefaultExo('root', {
async bootstrap(_vats: unknown, services: { myService: unknown }) {
// Only called on first launch, not on restart
myService = services.myService;
baggage.init('myService', myService);
},
increment(): number {
counter += 1;
baggage.set('counter', counter);
return counter;
},
});
}- Primitive values (strings, numbers, booleans)
- Hardened plain objects and arrays
- Exos and other remotable objects (including references to objects in other vats)
- Not arbitrary class instances, functions, or unhardened objects
The kernel supports revoking object references. Once revoked, any E() call targeting that object will fail.
// Host application side
kernel.revoke(kref);
// Check revocation status
kernel.isRevoked(kref); // trueRevocation is a kernel-level operation. Vats holding a reference to a revoked object will get errors when they try to use it. Revocation is permanent and cannot be undone.
For definitions of terms used in this guide (kernel, vat, subcluster, exo, baggage, kref, etc.), see the canonical glossary.
// Configuration for launching a group of vats
type ClusterConfig = {
bootstrap: string; // Name of the bootstrap vat
forceReset?: boolean; // Force reset of persisted state
services?: string[]; // Names of kernel services to inject
io?: Record<string, IOConfig>; // IO channel configurations
vats: Record<string, VatConfig>; // Vat configurations by name
bundles?: Record<string, VatConfig>; // Named bundles
};
// Configuration for a single vat (one of these source specs is required)
type VatConfig = {
sourceSpec?: string; // Path to source file
bundleSpec?: string; // Path to bundle file
bundleName?: string; // Name of a pre-registered bundle
creationOptions?: Record<string, Json>; // Options for vat creation
parameters?: Record<string, Json>; // Static parameters passed to buildRootObject
platformConfig?: Partial<PlatformConfig>; // Platform-specific configuration
globals?: string[]; // Host/Web API globals the vat requests — see [Vat Endowments](#vat-endowments)
};
// Configuration for a system subcluster
type SystemSubclusterConfig = {
name: string; // Unique name (used for retrieval)
config: ClusterConfig; // The cluster configuration
};
// Result of launching a subcluster
type SubclusterLaunchResult = {
subclusterId: string; // The assigned subcluster ID
rootKref: KRef; // Kref of the bootstrap vat's root object
bootstrapResult: CapData<KRef> | undefined; // Return value of bootstrap()
};
// Kernel status
type KernelStatus = {
vats: { id: VatId; config: VatConfig; subclusterId: string }[];
subclusters: Subcluster[];
remoteComms?:
| { state: 'disconnected' }
| { state: 'identity-only'; peerId: string }
| { state: 'connected'; peerId: string; listenAddresses: string[] };
};import { Kernel } from '@metamask/ocap-kernel';
import { makeDefaultExo } from '@metamask/kernel-utils/exo';
// 1. Create a kernel service
const weatherService = makeDefaultExo('weatherService', {
async getTemperature(city: string): Promise<number> {
// Call your host application's internal APIs here
return hostApp.weather.getTemp(city);
},
async getForecast(city: string, days: number): Promise<string[]> {
return hostApp.weather.forecast(city, days);
},
});
// 2. Create the kernel
const kernel = await Kernel.make(platformServices, db, {
systemSubclusters: [
{
name: 'weather-system',
config: {
bootstrap: 'weatherVat',
services: ['weatherService', 'kernelFacet'],
vats: {
weatherVat: {
sourceSpec: './weather-vat.ts',
parameters: { defaultCity: 'London' },
},
},
},
},
],
});
// 3. Register the service (must happen before kernel.make resolves,
// or before any subcluster that uses it is launched)
kernel.registerKernelServiceObject('weatherService', weatherService);
// 4. Interact with the system subcluster's root object
const rootKref = kernel.getSystemSubclusterRoot('weather-system');
const result = await kernel.queueMessage(rootKref, 'getWeatherReport', [
'Paris',
]);import { E } from '@endo/eventual-send';
import { makeDefaultExo } from '@metamask/kernel-utils/exo';
import type { Baggage } from '@metamask/ocap-kernel';
type WeatherService = {
getTemperature(city: string): Promise<number>;
getForecast(city: string, days: number): Promise<string[]>;
};
export function buildRootObject(
_vatPowers: unknown,
parameters: { defaultCity?: string },
baggage: Baggage,
) {
const defaultCity = parameters.defaultCity ?? 'London';
let weatherService: WeatherService | undefined = baggage.has('weatherService')
? (baggage.get('weatherService') as WeatherService)
: undefined;
return makeDefaultExo('root', {
async bootstrap(
_vats: unknown,
services: { weatherService: WeatherService },
): Promise<void> {
weatherService = services.weatherService;
baggage.init('weatherService', weatherService);
},
async getWeatherReport(city?: string): Promise<string> {
const target = city ?? defaultCity;
const temp = await E(weatherService!).getTemperature(target);
const forecast = await E(weatherService!).getForecast(target, 3);
return `${target}: ${temp}C. Next 3 days: ${forecast.join(', ')}`;
},
});
}