Appearance
Actor declarations
Declaring an actor is a two-part exercise: a manifest declaration in matrix.json:components[] and a static contract on the actor class in source. The manifest tells the runner which class to mount and where; the static contract tells callers (humans and LLMs) what the actor accepts, emits, and streams.
Part one: the manifest entry
A components[] entry minimally looks like this:
json
{
"type": "MyServiceActor",
"export": "MyServiceActor",
"mount": "my-service"
}Validator-required only type. Everything else is optional but strongly recommended in production packages.
Full shape, with all the fields the live tree uses:
json
{
"type": "ChatConversationProxy",
"export": "ChatConversationProxy",
"mount": "conversation",
"surface": "headless",
"autoStart": true,
"description": "Conversation session management and prompt routing",
"accepts": ["conversation.start", "conversation.send", "conversation.list"],
"emits": ["conversation.message"],
"fsPolicy": "none",
"props": { "maxSessions": 1000 }
}Field reference:
| Field | Required | Purpose |
|---|---|---|
type | yes | The class name (used as the binding identifier) |
export | no | Named export from runtime.entry; default is type |
mount | no | Relative bus mount inside the package namespace; final wire address adds the runtime root |
surface | no | "headless" (process-side) or "browser" (custom element) |
autoStart | no | If true, the runtime mounts this actor at startup without waiting for a request |
description | no | One-line description; surfaces in introspection and the discovery index |
accepts | no | Op names the actor handles |
emits | no | Event names the actor publishes |
fsPolicy | no | Per-component override of the package-level permissions.fsPolicy |
props | no | Object passed into the actor constructor |
Note:
accepts/emitsinmatrix.jsonare advisory; the ground truth lives on the class itself. The discovery extractor (packages/mx-cli/src/utils/discovery-extractor.ts) reads both, with the static contract winning when they conflict.
Part two: the static contract on the class
CLAUDE.md Rule 3 (Actor Communication Contract) requires every actor to declare:
static accepts— ops the actor handlesstatic emits— events the actor publishesstatic streams— long-running streams the actor exposesstatic state— schema of the actor's persisted statestatic subscribes— events the actor subscribes tostatic blackboard— bus topics the actor reads/writes
Every op and field MUST have a description. The mx actor scaffold creates a minimal contract:
ts
import { MatrixActor } from "@open-matrix/core";
export class UserStoreActor extends MatrixActor {
static override accepts = {
getStatus: {},
};
static override emits = {
statusChanged: { status: "string" },
};
private _status = "ready";
protected onGetStatus(): { status: string } {
return { status: this._status };
}
}A production actor declaration looks more like this (paraphrased from system-runtimes):
ts
export class RuntimeManagerActor extends MatrixActor {
static override accepts = {
"runtimes.list": {
description: "List runtime records visible from this Host",
cursor: { type: "string", description: "opaque pagination cursor" },
limit: { type: "number", description: "max items to return", default: 100 },
},
"runtimes.start": {
description: "Start a runtime described by a startSpec",
startSpec: { type: "object", required: true,
description: "shape defined in IHostRuntimeStartSpec" },
idempotencyKey: { type: "string",
description: "optional dedup key for the start request" },
},
};
static override emits = {
"runtimes.changed": {
description: "Emitted whenever the runtime registry mutates",
runtimeId: { type: "string", description: "Runtime that changed" },
},
};
static override state = {
runtimes: { type: "array",
description: "Live runtime records keyed by runtimeId" },
};
}Two-half consistency
The runner uses both halves. The manifest tells the runner which class to instantiate and where to mount it. The static contract tells the runner what to validate, what to log, and what to surface in introspection.
A discrepancy between the two is allowed (the validator does not cross-check), but it always indicates a bug. Specifically:
- If
matrix.json:components[].acceptslists an op the class doesn't implement → the actor will throwunknown-opat runtime. - If the class implements an op but the manifest doesn't list it → it works at runtime but is invisible to discovery and audit tools.
Identity from transport metadata
CLAUDE.md Rule 5: actors derive caller identity from transport metadata, never from payload fields. Do not write op handlers that trust payload.principalId. The runner's transport plumbing fills in authenticated identity on every envelope.
See also
- Overview → Package vs actor
- Authoring → Package manifest
- Authoring → Capability declarations
WORKSTREAMS/core-and-packaging/ACTOR-COMMUNICATION-CONTRACT.md
Source:
projects/matrix-3/packages/mx-cli/src/utils/manifestValidator.tsvalidates the manifest half;WORKSTREAMS/core-and-packaging/ACTOR-COMMUNICATION-CONTRACT.mdis the authoritative spec for the static contract.