@endo/exoCreate defensive remotable objects by combining Far objects with InterfaceGuards.
An Exo is a remotable object (created with Far from
@endo/pass-style) protected by an
InterfaceGuard (from @endo/patterns).
The guard automatically validates all method arguments and return values,
providing the first layer of defense against malformed input.
This package provides three patterns for creating exos:
Far objects are remotable but don't validate inputs. Exos add automatic validation:
import { Far } from '@endo/pass-style';
import { makeExo } from '@endo/exo';
import { M } from '@endo/patterns';
// Far object - no validation
let count = 0;
const counter1 = Far('Counter', {
increment(n) {
count += n; // What if n is not a number? undefined? a string?
return count;
}
});
// Exo - automatic validation
const CounterI = M.interface('Counter', {
increment: M.call(M.number()).returns(M.number())
});
const counter2 = makeExo('Counter', CounterI, {
increment(n) {
count += n; // n is guaranteed to be a number by the guard
return count;
}
});
counter2.increment(5); // OK
counter2.increment('5'); // throws: Must be a number
The InterfaceGuard validates arguments before the method executes, catching errors at the boundary rather than deep in your logic.
Use when you need one exo instance with no complex state management:
import { makeExo } from '@endo/exo';
import { M } from '@endo/patterns';
const GreeterI = M.interface('Greeter', {
greet: M.call(M.string()).returns(M.string())
});
const greeter = makeExo('Greeter', GreeterI, {
greet(name) {
return `Hello, ${name}!`;
}
});
greeter.greet('World'); // 'Hello, World!'
When to use:
Use when you need multiple exo instances, each with their own state:
import { defineExoClass } from '@endo/exo';
import { M } from '@endo/patterns';
const CounterI = M.interface('Counter', {
increment: M.call().optional(M.number()).returns(M.number()),
getValue: M.call().returns(M.number())
});
const makeCounter = defineExoClass(
'Counter',
CounterI,
// init function: creates initial state for each instance
(initialValue = 0) => ({ count: initialValue }),
// methods: have access to this.state and this.self
{
increment(delta = 1) {
this.state.count += delta;
return this.state.count;
},
getValue() {
return this.state.count;
}
}
);
const counter1 = makeCounter(0);
const counter2 = makeCounter(100);
counter1.increment(); // 1
counter2.increment(); // 101 (separate state)
When to use:
State access:
this.state - The instance's state objectthis.self - Reference to the exo itself (for return values or callbacks)Use when you need multiple related objects (facets) that share the same state. This is the key pattern for least authority: give each client only the facet they need.
import { defineExoClassKit } from '@endo/exo';
import { M } from '@endo/patterns';
const CounterKitI = {
up: M.interface('UpCounter', {
increment: M.call(M.number()).returns(M.number())
}),
down: M.interface('DownCounter', {
decrement: M.call(M.number()).returns(M.number())
}),
reader: M.interface('CounterReader', {
getValue: M.call().returns(M.number())
})
};
const makeCounterKit = defineExoClassKit(
'Counter',
CounterKitI,
// init: shared state across all facets
(initialValue = 0) => ({ count: initialValue }),
// methods: one object per facet
{
up: {
increment(delta) {
this.state.count += delta;
return this.state.count;
}
},
down: {
decrement(delta) {
this.state.count -= delta;
return this.state.count;
}
},
reader: {
getValue() {
return this.state.count;
}
}
}
);
const { up, down, reader } = makeCounterKit(50);
// Give different facets to different clients
// incrementer only gets `up`, decrementer only gets `down`
// Everyone can have `reader`, it's read-only
up.increment(10); // 60
down.decrement(5); // 55
reader.getValue(); // 55
When to use:
Context access:
this.state - Shared state across all facetsthis.facets - Object containing all facets (for inter-facet communication)For methods that await promises, use M.callWhen() instead of M.call():
import { makeExo } from '@endo/exo';
import { M } from '@endo/patterns';
import { E } from '@endo/eventual-send';
const FetcherI = M.interface('Fetcher', {
// Async method: validates and awaits arguments before calling method
fetch: M.callWhen(M.string()).returns(M.string())
});
const fetcher = makeExo('Fetcher', FetcherI, {
async fetch(url) {
// url is validated, then awaited if it's a promise
const response = await E(httpClient).get(url);
return response.text();
}
});
The M.callWhen() guard:
This enables safe eventual-send semantics: remote calls can pass promises, and your method receives validated resolved values.
Each exo pattern handles state differently:
No built-in state management. Use closure variables:
let count = 0;
const counter = makeExo('Counter', CounterI, {
increment() {
count += 1;
return count;
}
});
this.state is per-instance:
const makeCounter = defineExoClass(
'Counter',
CounterI,
(initial) => ({ count: initial }), // state for this instance
{
increment() {
this.state.count += 1; // each instance has separate state
return this.state.count;
}
}
);
this.state is shared across all facets:
const makeKit = defineExoClassKit(
'Kit',
{ facet1: I1, facet2: I2 },
() => ({ sharedData: [] }), // shared by both facets
{
facet1: {
add(item) {
this.state.sharedData.push(item); // modifies shared state
}
},
facet2: {
getAll() {
return this.state.sharedData; // reads shared state
}
}
}
);
GET_INTERFACE_GUARDEvery exo with an InterfaceGuard has a meta-method to retrieve its interface at runtime:
import { GET_INTERFACE_GUARD } from '@endo/exo';
import { getInterfaceMethodKeys } from '@endo/patterns';
const counter = makeCounter();
// Get the interface guard (works with E() too)
const interfaceGuard = await E(counter)[GET_INTERFACE_GUARD]();
// Inspect available methods
const methodNames = getInterfaceMethodKeys(interfaceGuard);
console.log(methodNames); // ['increment', 'getValue']
// Build dynamic clients, generate documentation, etc.
This enables:
Note: The interface can change across vat upgrades, so clients caching it may become stale.
This package provides heap-based exos that don't survive vat termination. For production systems with high cardinality or upgrade requirements, see:
@agoric/vat-data - Provides:
defineVirtualExoClass - Backed by virtual object storage (pageable)defineDurableExoClass - Survives vat upgradesprepareExoClass - Unified API for bothprepareExoClassKit - Durable/virtual kitsExo Taxonomy - Complete reference of all exo creation patterns including virtual and durable variants
The heap exos in this package are ideal for:
Far()E()Complete Tutorial: See Message Passing for a comprehensive guide showing how exos work with pass-style, patterns, and eventual-send to enable safe distributed computing.