Appearance
Actor permissions
The Matrix actor framework gates op invocations via the actor's static metadata. This is the first authorization layer every op encounters, before any capability token or identity check.
The contract
Per WORKSTREAMS/core-and-packaging/ACTOR-COMMUNICATION-CONTRACT.md, every actor declares:
ts
class SomeActor extends MatrixActor {
static accepts = {
'op.name': { description: '<what it does>', /* schema fields */ },
// ...
};
static emits = {
'event.name': { description: '...', schema: { ... } },
// ...
};
static streams = { /* stream subjects */ };
static state = { /* state shapes */ };
static subscribes = { /* events listened to */ };
static blackboard = { /* shared blackboard subjects */ };
}accepts enumerates every op the actor handles. The framework rejects op envelopes whose op name is not in accepts. Every entry MUST have a non-empty description.
What gating actually happens
| Layer | What it checks |
|---|---|
| Transport (NATS subject permissions) | "Can the caller publish to this subject?" |
| Mailbox routing | "Does the subject map to a mounted actor?" |
| Framework dispatch | "Is the op name in static accepts?" |
| Actor implementation | "Are the payload fields valid? Does the requested action make sense in current state?" |
| Returned response | "Did the op succeed?" |
The framework gate is op-name-only today. Two actors with overlapping op names but different actor classes both reject unknown ops, but neither rejects based on caller identity or capability. Any caller that can reach the mailbox can attempt any declared op.
Identity passthrough
Op envelopes carry transport metadata that includes the caller's identity. The actor implementation can consult this metadata to decide whether to honor the op:
ts
async onSomeOp(payload: SomeShape): Promise<SomeReturn> {
const callerPrincipalId = this.context?.callerIdentity?.principalId;
// gate based on callerPrincipalId
}Per CLAUDE.md Rule 5, callerIdentity.principalId is derived from the transport (NATS auth user, HTTP session) — never from payload.principalId.
Singleton authority enforcement
System singletons (system.devices, system.runtimes, system.registry, system.auth, host.control) further enforce that only one live owner per authority root claims their mount. The framework does not enforce this automatically; package authors and Host operators must avoid double-mounting.
Schema annotations as documentation
accepts schema fields carry description strings that are surfaced through $introspect ops. This is how the actor surfaces "what does this op expect?" to callers (UI tooling, CLI introspection, documentation generators).
Today: description is informational. Target state may include field-level type validation in the framework dispatcher.
What actors should NOT do
- Re-implement identity validation. Call
system.auth.auth.identity.resolveorsystem.security.auth.whoamiinstead. - Trust
payload.principalId,payload.email, or any other "who am I" field from the body. - Accept undeclared ops. Use the
static acceptsdeclaration; do not add ad-hoc handler functions outside that declaration. - Hold credentials directly. Use
system.factotum.credential.leasesystem.factotum.credential.materialize.
- Read OS-level credential files (
~/.codex/auth.json,~/.claude/.credentials.json) directly. Use Factotum'scredential.import-detected+credential.importif importing.
Blackboard
static blackboard declares shared blackboard subjects an actor publishes or consumes. Blackboard subjects are intentionally broadcast; authorization is at the NATS subject permission level, not at a per-actor gate. If you want a private signal, use a private subject under the actor's mount, not the blackboard.
See also
- Topic claims — the NATS layer below this.
- Capability model — what a future per-op gate would look like.
WORKSTREAMS/core-and-packaging/ACTOR-COMMUNICATION-CONTRACT.md— the full contract.
Source: the contract itself in
WORKSTREAMS/core-and-packaging/ACTOR-COMMUNICATION-CONTRACT.md; dispatch implementation inprojects/matrix-3/packages/core/(MatrixActorframework).