Maia normative contracts
Status: planned target contract. Each section names what exists in source today
(verified in packages/protocol/src, packages/agent/src) and the roadmap slice
that delivers the rest. Nothing here makes an operation callable; only the
registry and a complete vertical slice do.
Authority
packages/protocolowns the executable Effect schemas and the operation registry. Today the registry is the method list inpackages/protocol/src/local-node/node-api/methods.tsplus feature descriptors (maia.feature-registry/v1,packages/protocol/src/feature/) that carry risk, idempotency, confirmation, receipt and projection metadata.- This document owns cross-domain invariants, schema requirements, versioning and generation rules.
cli.mdowns human command grammar and example output.- Domain architecture documents own algorithms and lifecycle policy. They may narrow these contracts but never redefine them.
- Generated help, JSON Schema, SDK types and OpenAPI are projections and never override the registry.
Only this repository is authority. Historical Maia OS repositories, Pi Config, Codex and Pi CLI documents are migration evidence.
Version and compatibility policy
Today: maia.protocol/v1alpha1 (MAIA_PROTOCOL_VERSION), plus per-domain
schema versions such as maia.execution/v1alpha1,
maia.terminal-session/v1alpha1 and command receipt schemaVersion: 1.
During alpha a breaking change is allowed only when all of these change in one change: the registry digest; the affected Effect schemas and fixtures; the CLI, SDK, Node, worker and documentation projections; a migration or explicit refusal for persisted data; the compatibility matrix and release notes.
- Every connection starts with the
node.hellohandshake (RL2). Release owns its fields, the skew policy andprotocol_incompatiblerefusal (decision). - An operation descriptor has
since,stabilityand an optionalreplacementOperationId. A renamed or removed operation, command, flag or config key keeps an alias with machine-readable deprecation diagnostics for at least 90 days and at least one minor release during alpha (two after v1 stable), recorded with aremoveAfterdate that CI enforces. - CI compares the operation registry with the last released snapshot. An addition needs a minor bump. During alpha a breaking change follows the rule above and raises the minor; after 1.0, removals and incompatible changes are refused within a major.
- Unknown fields fail in security-, identity-, authority- and mutation-bearing objects. A read projection may ignore additive fields only when its schema marks them forward-compatible.
storage.integrityofnode.statusandstorageIntegrityof the voice status now acceptunavailablenext took. An older client cannot decodeunavailable, so client and Node upgrade together.okmeans the storage connection is open and, when the Node reports retention, its journal counters read; it does not mean an integrity check passed.storage.eventsofnode.statusis optional (excessBytes,oldestAgeMs,checkpointBlocked; see events). A client built with it accepts an older Node. A client built before it rejects a newer Node's response, because unknown fields fail: during alpha, client and Node upgrade together.- Source additions to
v1alpha1that a reader built before them rejects, because unknown fields fail: thesession.attachstream records (maia.session-stream/v1,SESSION_STREAM_SCHEMA_VERSION); thehelderror code (HOLD_ADMISSION_ERROR_CODE) and theholdIdofsession.stopped; the Session credential scheme (NODE_API_SESSION_CREDENTIAL_SCHEME) withSESSION_ROLE_OPERATIONSandSESSION_GUARD_ERROR_CODES, where an older Node refuses the Session-authenticatedsession.list; the optionalcwdofsession.create(a start directory that an older Node rejects as an unknown field), the optionalhostAuthorityandpermissionofsession.createwith thecontainmentof a Session and thehostAuthorityofnode.status(D-198; an older Node rejects the fields, so a client never falls back to the sandbox) andHOST_AUTHORITY_DISABLED_ERROR_CODE, the optionallabelsofsession.createand a Session (also rejected by an older Node;MAX_SESSION_LABELS), the optionallabels,settledandidleForSecondsfilters ofsession.list(withsettledReasonon its items), the optionalsummaryandtranscriptPathofsession.archiveand its result (MAX_SESSION_ARCHIVE_SUMMARY_BYTES,MAX_SESSION_TRANSCRIPT_PATH_BYTES,DEFAULT_SESSION_SETTLED_IDLE_SECONDS,MAX_SESSION_SETTLED_IDLE_SECONDS), the optionalrole,labelsandtitleofsession.adopt.preview(echoed in its result and bound into its digest) andMANAGER_EXISTS_ERROR_CODE(D-201), and Session titles (MAX_SESSION_TITLE_LENGTH) withsession.rename,session.renamedandreference.resolve(MIN_REFERENCE_PREFIX_LENGTH,MAX_REFERENCE_CANDIDATES); and themachinekind of thecontext.showprincipal with itsmachineblock (MachinePrincipal,maia.invocation-context/v1unchanged because no Node emits it before MN1 enrollment: an older reader refuses a machine projection, which no deployed Node sends). Thenode.statusprocess-resource valuescgroup_delegation_unavailable(pressure) andrun_node_in_delegated_cgroup(nextStep), andlandlock_scoped_unavailable(pressure) withupgrade_kernel(nextStep), are such additions: a Linux Node outside a delegated cgroup, or on a kernel below Landlock ABI 6 (or whose probe failed), sends them, and an older reader refuses that status instead of showing hard containment that every launch would refuse. TheexecutorHostfield of a Linux Node'snode.statusand thesession.createerror codes inEXECUTOR_HOST_ERROR_CODES(host_bubblewrap_missing,host_socat_missing,host_user_namespaces_restricted) are additions too: an older reader refuses a Linux Node's status, and still reads the refusals, because an error code is any well-formed string. An error that carries a key its reader does not know is rejected as invalid, and a bridge failure frame then keeps only the bridge's own code. The bridgerequestframe'sprincipal(coordinator) is another: an older bridge closes on a marked frame as invalid. A newer bridge forwards the mark asx-maia-principal, which every published Node enforces; a Node built before that header ignores it, so the bridge and its Node run from the same install. The bridgereadyframe gains no field, so an older CLI still reads a newer bridge's greeting. A bridge built before streams refuses asubscribeframe as invalid and exits 65; the SDK maps that exit, after a subscribe with noitemorend, tocommand_not_implemented. A newer bridge ends a subscribe for any other operationfailure stream_unsupported, and a subscribe markedprincipal: "coordinator"failure role_denied, the code the Node's gate uses. Prerelease versions ship the CLI and Node together. - Mixed-version order: deploy a backward-readable Node; migrate durable schemas; deploy worker and SDK/CLI projections; enable new operations; remove the old reader only after restore and downgrade-refusal tests.
Identifier and reference grammar
Today the wire accepts protocol identifiers of 1–128 characters matching
^[A-Za-z0-9][A-Za-z0-9._:-]*$, and the Node generates <prefix>_<32 hex> IDs
(container_, project_, receipt_, ...).
Target: identifiers are opaque ASCII with a type prefix and 128 bits of random
or deterministic idempotent entropy, matching
^[a-z][a-z0-9-]{1,31}_[A-Za-z0-9_-]{16,128}$. Prefixes include project_,
session_, activation_, attempt_, message_, routine_, tick_,
node_, attachment_, connection_, effect_, event_, receipt_,
artifact_ and evaluation_.
On the CLI, a resource argument may also be a unique ID prefix (at least four
hexadecimal digits) or a unique name or Session title; the Node's
reference.resolve turns it into the exact ID before any operation, and an
ambiguous one fails listing the candidates
(CLI). Operations themselves take exact
IDs only.
Human definition references are never bare IDs:
builtin:<id>
user:<id>
project:<project-id>/<id>
Agent, Skill, Routine, evaluator, result-contract, Connection and executor
references use this grammar. A Project-local CLI command may accept a short ID
only after an exact --project resolves the namespace; JSON always uses the full
reference.
- Revisions are positive safe integers. A mutation names every aggregate revision it depends on.
- Timestamps are UTC RFC 3339 with millisecond precision. Durations are integer milliseconds.
- Digests are lowercase SHA-256 with a
sha256:prefix, over canonical UTF-8 JSON or exact resource bytes.
Workspace and Execution Environment naming
Approved target vocabulary, not yet API names (decision). Roadmap R1b schedules it; PF-25/DF-25 accept it. Current commands, SDK bindings, schema types, fields, owner discriminants and IDs keep their meaning until then.
- Workspace means only the organizational parent of Projects. A Project owns focused work, knowledge and workers.
- Execution Environment is a checkout, Git worktree or separately admitted private working directory; Execution Node is the host computer or server; Execution Profile is the permission and containment policy. These are execution resources, not organizational levels. A working directory or worktree is not a sandbox.
| Meaning | Current compatibility contract | Future versioned contract |
|---|---|---|
| Organizational Workspace | container.*; containerId; existing container_* IDs | workspace.*; organizational workspaceId |
| Execution Environment | workspace.*; execution workspaceId; existing workspace_* IDs | environment.*; environmentId |
New product vocabulary uses Workspace and Execution Environment, never Container, Execution Workspace or a second ProjectEnvironment concept. Source references may keep the old names when labelled.
Migration requirements, under the version policy:
- Freeze the exact old/new operation, field, owner and identity mappings in the
registry. Bind dispatch to the negotiated version and descriptor digest; never
infer organizational or execution meaning from a display name, prefix, UI
focus or alias. An unversioned
workspace.*request never silently changes target kind; unsupported clients fail before effects. - Preserve organizational and execution IDs, Project memberships, repositories, filesystem identities, Session and assignment references, grants, history, events and receipts. Never rewrite ID prefixes or reinterpret execution rows as organizations. Retained IDs get version-aware validation; new-ID allocation and any storage migration or refusal are explicit decisions. One Node-owned writer.
- Stored confirmations and receipts keep their exact operation, input and revision binding. Retries reconcile under their original contract and idempotency key; a pending proof never becomes authority in the new namespace. If an old contract cannot resume safely, return an explicit refusal with a receipt-reconciliation path.
- Deliver protocol → SDK → Node → CLI → applicable agent, voice and client projections with mechanical parity and mixed-version tests. Expose new names only after their registry entries and handlers qualify; keep old help labels marked as compatibility names.
- Test empty and populated disposable databases, identity and history preservation, concurrent old and new readers and commands, stale or expired proofs, duplicate and lost-response retries, wrong-kind IDs, unsupported-version refusal, rollback, restore and downgrade refusal. Freeze backup and recovery before any separately authorized live migration.
Ordinary Workspace Session ownership
Required for R3 (PF-03/PF-22, DF-22); not current runtime support. A Workspace can own several ordinary Sessions, even with zero Projects. Creating, listing, inspecting and reopening them needs no Project, repository, checkout or Execution Environment and provisions no coordinator. A Workspace coordinator (home) Session is optional and separate; disabling it never disables ordinary Sessions.
- One owner. Every Session has exactly one closed global, Workspace or
Project owner, independent of its coordinator or ordinary role. A
Workspace-owned Session binds the exact organizational Workspace identity, not
a Project ID or a legacy execution
workspaceId. Ownership is never inferred from navigation, a role name, a parent Session or a path; conflicting or forged owner combinations are rejected. - Distinct Sessions. Several ordinary Sessions with the same owner and Agent stay distinct; home-Session uniqueness applies only to the designated home Session. Separate create commands make separate Sessions; an exact retry reconciles its original command and receipt.
- Scoped access. Grants, history and activation context bind the authenticated principal, the exact owner and the Session. Workspace membership does not authorize reading another conversation or reusing its provider history, private context, tool grants or caches. Shared Workspace knowledge comes through explicit grants and the admitted learning policy.
- Durable identity. Identity, owner, messages, revisions, receipts and history survive disconnect and restart. Reopening history is a read, not model activation or archive restore. Navigation, adding a Project or delegating work never reparents a conversation; Project execution uses a separately authorized Project-owned Session through parent-mediated delegation.
- No implicit execution. Creating Session metadata starts no worker and grants no filesystem, process, network or provider access. Conversation-only activation needs its own owner-aware configuration, context, provider, export and budget grants. No Project configuration, ambient working directory, synthetic Project or hidden environment may stand in for them; private Node storage is not a worker filesystem grant.
- One backend. Extend the existing Session owner through protocol → SDK → Node → built CLI → applicable projections, not a second chat backend. Keep old Project Session IDs, bindings, history, events and receipts, and coordinate field mappings with the naming contract. Unsupported owner and operation combinations fail closed.
Acceptance includes an empty Workspace with two ordinary Sessions and no coordinator. This section reserves no operation name, live migration, provider call or execution profile.
Coordinator ownership and evaluation extensions
Required target extensions (R1, R3, R10), not current schemas. No operation ID or tool is reserved here.
- Owners. Protocol owns the closed global, Workspace and Project owner
variants and the authenticated Session and principal binding. A Workspace
keeps its
container_*identity until R1b. Existing Project IDs, histories and receipts stay readable through a preserving migration or an explicit refusal; there is no all-powerful synthetic operator Project. - Visibility. The Swarm ownership contract owns principals, discovery ceilings and parent-mediated delegation. Effective access is the intersection of owner ceiling, grants and operation policy, applied before any catalog, search, count, traversal, error or model export.
- Semantic evaluation. The evaluation contract needs one registry slice with typed answers, uncertainty, errors, owner/export/budget admission and receipt reconciliation, shared by internal services, SDK, CLI and granted coordinator tools. Execution workers get no direct evaluation access.
- Graph snapshots extend the observation contracts with entities, relationships, source revisions, per-owner watermarks and coverage. Semantic annotations stay advisory; no model creates ownership, dependency or lifecycle facts. Qualification: PF-22–PF-24 and DF-22–DF-24 in the portfolio suites.
Routing, context and feedback contracts
Planned (R10), not registered operations. Swarm owns routing and attention policy, Node owns journal and context state, and presentation owns device signals. Freeze versioned, closed Effect Schemas and parity tests for:
- Input origin lanes (human request, coordinator task request, worker event, feedback) with server-authenticated provenance; causality never creates human authority.
- Bounded plans over existing operations: dependencies, shared constraints, acceptance and cumulative limits, validated like any mutation (grants, inputs, revisions, expiry, digest, idempotency, confirmation).
- Work and feedback queues with real received, queued, handoff, running and result evidence, unknown outcomes, source sequences, restart behavior, finite quotas and typed overload. A receipt is not proof of execution or attention.
- Session graph facts with separately labelled annotations, coverage, context manifests and retained constraints; a central model and effort policy frozen before model use.
- Client focus snapshots bound to the exact device, view, Session, selection generation, allowed signals, age and TTL. No global focus or silent retargeting.
- Advisory scores and feedback decisions with rubric, model and source identity, freshness, consent and the originating request. Required visibility and replies stay; no score grants authority.
- AI readiness states (invalid, stale, denied, over-budget, unavailable). A required model or Jev failure blocks only the affected automation, never manual controls; retries never invent another paid attempt or provider.
SDK methods return validated typed objects; Node serializes them as JSON; the
CLI projects human output or --json/--jsonl. UI clients consume SDK results,
never CLI output or raw worker logs. PF/DF-29 covers the composed behavior.
Client presentation contracts
User-approved R9a/R10 target, not registered operations (decision). Node owns versioned, bounded, json-render-compatible presentation output through the public SDK. Presentation runtime owns product composition, the view-to-SDK map, failure isolation and multi-client limits; this section owns the protocol admission rules. A catalog is presentation vocabulary, not a second registry or authorization source.
Freeze exact request, result, error and event schemas and SDK bindings first:
- Binding. Authenticate user, device and client server-side and bind Node identity and epoch, exact owner and target set, Session, view and request identity, catalog version and digest, policy revision and selection generation. Caller identity claims, global focus and client capability declarations are not grants. Global Maia is user-level, not a synthetic Project.
- Bounded streams. One shared schema and catalog serves separate client view
instances. Specs and patches are bounded by bytes, nesting, component and
reference count, opcodes, paths, sequence and base revision, with explicit
cancellation, end, error, version refusal, gap and resnapshot, and catalog
negotiation. Unsupported or incomplete specs never partially enable actions.
No UI
eval, dynamic imports, arbitrary URLs or hidden on-render operations. - Authoritative data stays separate, as the presentation runtime's spec, data and action boundaries define; a patch is never a domain mutation.
- Trusted components. Shell, target, confirmation and receipt components are authored with protected inputs and placement; generated specs cannot supply their proofs or imitate them. UI actions map mechanically to admitted operation IDs and go through ordinary validation, confirmation, compare-and-set, idempotency and receipts.
- Policy before work. Owner, export and retention policy apply before Jev, cache lookup, query, count, graph traversal and output. Cache equivalence includes the authorized audience, policy and source revisions, request or rubric digest and catalog digest. Revocation invalidates queued work; one subscriber's cancel never cancels another's shared computation.
- Limits follow the multi-client admission contract. UI disconnect releases UI resources, not accepted work; unknown mutation outcomes reconcile the original command.
- Effect Schema is canonical. Qualify renderer-catalog compatibility without handwritten duplicate validators; json-render fetch hooks or generation endpoints never become another request boundary.
Current envelopes, schemas and container.*/execution workspace.* meanings
are unchanged. R9a freezes the delta in protocol → SDK → Node → headless
fixtures → projections. PF-27/DF-27 and PF-28/DF-28 require parity, rejection of
malformed, stale and foreign-client input, voice and target races, fault
isolation and measured multi-client fairness.
Configuration contracts
Today the protocol defines one maia.config/v1alpha1 document
(packages/protocol/src/configuration), named by config.jsonc and profiles/<name>.jsonc (no Node code reads them
today; S13 owns the configurator), and the agent home reads
~/.maia/config.toml (schema_version = 1). The V1 shapes below are the
target. Architecture owns
the .maia scopes, file layout and admission flow.
~/.maia/config.jsonc parses into UserConfigV1:
interface UserConfigV1 {
readonly schemaVersion: "maia.user-config.v1";
readonly swarm: {
readonly administratorAgentRef: `user:${string}`;
readonly defaultTimezone: string;
readonly capacity: CapacityLimitsV1;
};
readonly defaults: {
readonly connectionRef?: `user:${string}`;
readonly modelRef?: string;
};
readonly observability?: ObservabilityPolicyV1;
}
~/.maia/projects.jsonc parses into UserProjectInventoryV1, a list of:
interface ProjectInventoryEntryV1 {
readonly projectId: string; // opaque, Node-owned
readonly projectKey: string; // portable human key
readonly rootPath: string; // canonical absolute root
readonly workspaceKind: "repository" | "directory";
readonly repository?: {
readonly normalizedRemote: string;
readonly objectFormat: "sha1" | "sha256";
readonly expectedHead?: string;
};
}
The canonical root is resolved descriptor-first without following a final symlink; parent and file must belong to the current OS user and not be group or world writable. Duplicate canonical roots, or duplicate repository identity plus ref, are rejected. A move or reclone is a relocation mutation; editing the file never relocates a live Project.
<root>/.maia/config.jsonc parses into ProjectConfigV1:
interface ProjectConfigV1 {
readonly schemaVersion: "maia.project-config.v1";
readonly projectKey: string;
readonly defaults?: {
readonly agentRef?: string;
readonly connectionRef?: string;
readonly modelRef?: string;
readonly executorRef?: string;
};
readonly limits: CapacityLimitsV1;
readonly context?: {
readonly nestedAgentsMode?: "disabled" | "nearest-explicit";
readonly maxInstructionTokens?: number;
readonly maxSkillIndexTokens?: number;
};
readonly allowedConnections?: ReadonlyArray<string>;
readonly allowedExecutors?: ReadonlyArray<string>;
readonly maintenance?: {
readonly enabled: boolean;
readonly autoApplySafeChanges: boolean;
};
readonly retention?: RetentionPolicyV1;
}
interface CapacityLimitsV1 {
readonly maxLiveSessions: number;
readonly maxConcurrentActivations: number;
readonly maxChildrenPerSession: number;
readonly maxAgentDepth: number;
readonly maxPendingRoutineTicks: number;
readonly maxQueuedMessages: number;
readonly maxInputTokensPerActivation: number;
readonly maxOutputTokensPerActivation: number;
readonly maxWallTimeMsPerActivation: number;
readonly maxOutputBytesPerTool: number;
readonly maxDailyCostMicros?: number;
readonly currency?: "USD" | "EUR";
}
projectKey matches ^[a-z][a-z0-9-]{1,63}$ and must equal the admitted
inventory entry; it is metadata, not authority or a global ID. Project files may
write project:self/<id> for a definition in the same Project; admission
resolves it once to project:<opaque-project-id>/<id>, and project:self is
invalid in API, event, receipt, trace or provider JSON. CapacityLimitsV1
closes every admission dimension.
Resolution and admission
Resolution order is fixed, with no implicit shadowing:
- reviewed built-ins;
- admitted user definitions the Project references explicitly;
- admitted Project definitions;
- the Agent definition;
- activation-time narrowing.
Duplicate fully qualified references are errors, and Project definitions cannot
redefine builtin: or user: resources. ResolvedProjectSnapshotV1 records
every transitive resource with its source scope, canonical locator, content
digest, schema version, trust class and capability-evidence digest; missing or
stale evidence makes the candidate inadmissible.
Admission validates the candidate from opened file descriptors, builds one
canonical manifest, previews the exact diff, and commits the snapshot, Project
revision, ordered event and receipt in one transaction. Managed activations use
the admitted snapshot ID. Local Agent mode uses the same compiler in memory and
reports admission: "local-unmanaged".
Operation registry
Every public read or mutation is one OperationDescriptorV1 (target; today's
feature descriptors carry a subset):
interface OperationDescriptorV1 {
readonly operationId: string;
readonly since: string;
readonly stability: "alpha" | "beta" | "stable" | "deprecated";
readonly replacementOperationId?: string;
readonly owner: "protocol" | "node" | "swarm" | "provider";
readonly availability: ReadonlyArray<
"cli" | "tui" | "sdk" | "model" | "voice" | "a2a"
>;
readonly inputSchemaRef: string;
readonly outputSchemaRef: string;
readonly errorSchemaRef: string;
readonly risk: "read" | "reversible" | "consequential" | "destructive";
readonly authorizationScope: string;
readonly revisionFields: ReadonlyArray<string>;
readonly idempotency: "none" | "optional" | "required";
readonly confirmation: "none" | "required";
readonly effectClass:
"none" | "native-idempotent" | "native-reconcilable" | "external-uncertain";
readonly eventTypes: ReadonlyArray<string>;
readonly receipt: boolean;
readonly stream?: {
readonly mode: "snapshot" | "events" | "snapshot-then-events";
readonly cursorSchemaRef: string;
readonly resetSchemaRef: string;
};
readonly redactionPolicyRef: string;
}
The registry rejects duplicate operation IDs, semantic aliases with overlapping mutation ownership, missing schemas, model or voice exposure without an explicit policy, mutations without receipts, and consequential operations without idempotency and confirmation.
Package entry points
Today's operations carry a stable ID such as session.create, turn.wait or
receipt.get, strict parameter and result schemas, an Agent exposure class
(eligible, human-only or unsupported) and receipt rules. A new operation
must be classified to compile. Every schema exposes parse and safeParse,
with its TypeScript type inferred from it; Effect types do not leak into the
public API.
@maiaos/protocol exports these entries (packages/protocol/package.json).
A generated public operation catalog and device-capability schemas are planned,
not exported.
| Entry point | Contents |
|---|---|
@maiaos/protocol | Operation registry, envelopes, errors, receipts, domain schemas |
@maiaos/protocol/agent-definition | Agent definition schema, canonical digest encoding, eligibility |
@maiaos/protocol/node-profile | Node.js-only profile validation and host paths |
@maiaos/protocol/feature-registry | Static registry binding features to SDK, CLI and Node handlers |
@maiaos/protocol/voice-orchestrator | Voice orchestrator prompt and tool contracts |
@maiaos/protocol/voice-pi-relay | Voice-to-Pi relay preview, relay and reconcile contracts |
@maiaos/protocol/execution-candidate | Execution, worktree and checkpoint candidate contracts |
@maiaos/protocol/invocation-context-candidate | Invocation context candidate contracts |
Capability enablement
One durable registry object controls risk-bearing adapters and execution profiles:
interface CapabilityEnablementV1 {
readonly enablementId: string;
readonly capabilityId: string;
readonly ownerActorId: string;
readonly scopeRefs: ReadonlyArray<string>;
readonly adapterDigest: string | null;
readonly schemaDigest: string;
readonly policyDigest: string;
readonly evidenceRefs: ReadonlyArray<string>;
readonly reason: string;
readonly status: "enabled" | "revoked" | "expired";
readonly expiresAt: string;
readonly revision: number;
readonly createdAt: string;
readonly updatedAt: string;
}
scopeRefs holds 1–32 exact typed actor, Project, Connection, peer, Node or
execution-profile references; empty or wildcard scope is invalid. The registry
owns the closed capability IDs and their required evidence. Only a human local
administrator may enable a capability; a narrower authorized owner may revoke
it. Expiry and revocation are monotonic; re-enabling creates a new record.
Mutation, preview, confirmation, and receipt
Today each consequential operation has its own preview and confirm methods (for
example container.create.preview and container.create.confirm): the preview
returns an inputDigest, a proof, issuedAt and expiresAt; the confirm
repeats the input with the digest and proof and returns delivery (new or
duplicate) and a CommandReceipt (schemaVersion: 1;
packages/protocol/src/command-receipt), retrievable with receipt.get and
receipt.list. receipt.list filters on the methods that record a receipt
under their own method, listed in
packages/protocol/src/local-node/receipt-list.ts: a described operation is
listed exactly when its policy.ownReceipt is true, which the type checker
enforces, and the other entries are a reviewed inventory of undescribed
methods. Returning or reconciling
another operation's receipt (system setup, voice managed confirmation) does
not make a method filterable. The generic contract below is the target.
The pruned-history markers are optional and absent unless a prune left a hole
(D-191): oldestRetainedCursor on the session event
list result and on the terminal event list result of an unavailable terminal,
and eventsPruned: true on receipt.get. A live terminal keeps its own
50,000-event history and reports an evicted range in the list itself as a
gap event naming the oldest cursor still held. Wire decoding rejects unknown
fields, so a client built before these fields fails on a page that has a hole.
That is an alpha paired-upgrade break, first reachable when the pruner is
enabled.
operation.preview takes an exact operation ID and canonical candidate input,
authorizes the actor, resolves targets, validates revisions and returns:
interface OperationPreviewV1 {
readonly previewId: string;
readonly operationId: string;
readonly normalizedInputDigest: `sha256:${string}`;
readonly actorId: string;
readonly deviceId: string;
readonly targets: ReadonlyArray<{
readonly targetType: string;
readonly targetId: string;
readonly expectedRevision: number;
}>;
readonly risk: "reversible" | "consequential" | "destructive";
readonly summary: string;
readonly expiresAt: string;
readonly previewRevision: number;
}
confirmation.issueis allowed only to an authenticated human client. It binds the preview, actor, device, exact digest, targets, revisions and expiry.confirmation.consumeand the target mutation run in the same Node transaction. Speech and model tools may request a preview but never issue or consume a confirmation.- Every mutation retry reuses
commandId. The uniqueness key isactorId + operationId + commandId; a different normalized input under the same key returnsidempotency_conflict. Keys and receipts live at least as long as the affected aggregate and never less than 30 days.
interface OperationReceiptV1 {
readonly schemaVersion: "maia.receipt.v1";
readonly receiptId: string;
readonly commandId: string;
readonly operationId: string;
readonly actorId: string;
readonly projectId?: string;
readonly normalizedInputDigest: `sha256:${string}`;
readonly outcome: "applied" | "rejected" | "reconciled" | "outcome-unknown";
readonly targets: ReadonlyArray<{
readonly targetType: string;
readonly targetId: string;
readonly priorRevision?: number;
readonly resultRevision?: number;
readonly disposition: string;
}>;
readonly eventIds: ReadonlyArray<string>;
readonly effectIds: ReadonlyArray<string>;
readonly evidenceDigest: `sha256:${string}`;
readonly createdAt: string;
}
First-owner bootstrap operations
Source candidate. Two release-disabled operations
(releaseQualified: false, CLI-only projection) in the team-workspace
feature (packages/protocol/src/team-workspace), specified in
Team Workspaces and control Nodes:
| Operation | Risk | Confirmation | Receipt | SDK method |
|---|---|---|---|---|
team-workspace.bootstrap.preview | reversible | none | none | previewTeamWorkspaceBootstrap |
team-workspace.bootstrap.apply | consequential | trusted-client-required | required | applyTeamWorkspaceBootstrap |
- Both take a
commandIdandexpectedBootstrapRevision, and reject unknown fields and any caller-supplied ID, role, authority, credential or timestamp. - The preview result carries
inputDigest,previewIdand the reserved IDs but noproof. The confirmation is a one-shot record on a private inherited channel from the launcher to the Node. It is in no request, result, SDK type or CLI flag, and a bearer alone cannot supply it. - The apply receipt names method
team-workspace.bootstrap.apply, target typecontrol-node-bootstrapand aggregate revision1.receipt.listfilters on it because the method records its own receipt.receipt.get(commandId)returns the stored result that rebuilds the apply result. - The closed error set is
bootstrap_already_completed,bootstrap_authority_unavailable,bootstrap_binding_mismatch,bootstrap_channel_invalid,bootstrap_confirmation_required,bootstrap_integrity_failed,bootstrap_preview_expired,idempotency_conflictandrevision_conflict. Every failure mutates nothing. A bootstrap call that overlaps a running one is refused at once with the retryablesaturatederror (admission) and never queues. - Apply verifies authority after it holds the write permit and judges ownership,
revision and expiry inside the SQLite transaction against a clock read after
the database lock is held, so a request that waited for either lock cannot
commit on a stale check or time. The receipt stores a hash of the whole apply
request: a retry returns
duplicateonly when it matches exactly, and an altered name or preview ID fails withbootstrap_binding_mismatch. The mapping from the profile bearer to the created owner is Planned.
Session wait operation
Source candidate. session.wait is a read with no receipt. It takes
sessionId, states (one to nine Session states), activities (one to four
of stopped, waiting_approval, working, idle; at least one of the two is
required, any other value fails validation) and waitMilliseconds (0 to
4000). Node rereads the Session every 200 ms and answers once the Session is in
any of states or activities or the wait ends:
{ method, satisfied, session }, where satisfied: false carries the current
Session. A missing Session fails resource_not_found. One call stays inside the
5-second request deadline; the caller repeats it to wait longer, as for
turn.wait. Interrupting the call stops the rereads.
session.list, session.show and session.wait (which returns the same
Session object as session.show) add two optional fields to each Session:
activity (the Session's last announced activity, the value
session.activity.changed carries) and latestTurnState (the state of the
turn latestTurnId names, absent until a turn runs). No other operation
returns them.
Source candidate (E13). The same two operations (and session.wait) add
status and prompt. status is one of starting, working, compacting, rate_limited, idle,
question, approval, error, done, ended and unknown: the value session.status.changed last announced. prompt is
{ approvalId, expiresAt, kind, options?, summary } of the approval the Session
waits on, the text a person reads, bounded by the approval's own limits (8 KiB
of text, at most 8 options). It appears only while status is question or
approval, with a matching kind (question for the first, any other kind for
the second), and only to a reader approval.show admits: a Session principal's
session.list returns the status without the prompt. A session.list page
names the prompt by approvalId, kind and expiresAt only, with no summary
or options, because 100 Sessions of 8 KiB text pass the response limit;
session.show and approval.show return the text. The field is status, not state, because a Session already has a lifecycle
state (running, failed, stopped, ...); status is the derived view a
person or bot acts on, and the lifecycle state stays the source of truth. Both
are absent from every other operation's Session. A new field
on a response changes the exported contract: a client and its Node upgrade
together, because an older strict client rejects a Session that carries them.
Compatibility: the exported contract changes (session.wait also takes
activities, and states becomes optional when activities is given), with
no SDK source edit. A newer reader accepts an older Node's responses, because
both fields are optional. An older strict SDK rejects a response once the Node
populates the new fields, and an older Node rejects activities.
Turn queue operations
Source candidate (S26). turn.send keeps its operation ID and schema: it
queues. Two operations join the session feature (schemas in
packages/protocol/src/turn/contract.ts):
| Operation | Risk | Confirmation | Receipt | SDK method | MCP tool |
|---|---|---|---|---|---|
turn.queue.list | read | none | none | listTurnQueue | turn_queue_list |
turn.cancel | reversible | none | required | cancelTurn | turn_cancel |
- A Session holds at most
MAX_SESSION_PENDING_TURNS(8) pending turns, the running one included. Queued turns dispatch in send order, each once; recovery and all seven handoffs complete within a 100 ms test-clock budget, and a Node restart keeps them queued. turn.queue.listreturnsactiveTurn(the dispatching or running turn) andqueuein dispatch order, each entry withposition, its ownrevisionand aninputPreviewof at most 160 UTF-8 bytes.turn.canceltakes the queued turn's ownrevision, not the Session's, and records its own receipt. The cancelled turn readsfailedwith reasonturn_rejected, the state an interrupt of a queued turn already produces.- Typed errors:
turn_queue_full(429, not retryable) andturn_not_queued(409: the turn already left the queue). Both queue and mutate nothing. - Steer is Planned (S26b, D-144);
turn.sendhas nomode.
Connection grant and pool operations
Source candidate. Six release-disabled operations (releaseQualified: false, CLI-only projection) in the connection feature
(packages/protocol/src/connection), specified by
D-147. A grant offers one Connection to one exact
scope: an organizational Workspace (container_… today) or a Project. A pool
names which granted Connections a scope's Sessions rotate over.
| Operation | Risk | Confirmation | Receipt | SDK method |
|---|---|---|---|---|
connection.grant.list | read | none | none | listConnectionGrants |
connection.pool.list | read | none | none | listConnectionPools |
connection.grant.set.preview | reversible | none | none | previewConnectionGrant |
connection.grant.set.confirm | consequential | trusted-client-required | required | confirmConnectionGrant |
connection.pool.set.preview | reversible | none | none | previewConnectionPool |
connection.pool.set.confirm | consequential | trusted-client-required | required | confirmConnectionPool |
- A grant is
activeat an odd revision andrevokedat an even one; a row is never deleted. A pool isoff(no members),single(one member) orrotate(1–32 members);offkeeps its row so a compare-and-set never meets a recreated pool. - The preview takes
commandId, the exactscopeand eithergrantedor the pool'smodeandmembers. It returnscurrent(or none), the proposed state,expectedRevision,inputDigest,proof,issuedAtandexpiresAt(30 seconds). The confirm repeats the input with the digest, proof and revision; its receipt names the confirm method and target typeconnection_grantorconnection_pool. - A retried preview (same
commandId, actor, operation and input) returns a freshproofwith the originalissuedAtandexpiresAtand retires the earlier proof. The samecommandIdon another actor, operation or input isstorage_conflict. - Refused with
storage_conflictand no change: a no-op request, a revoke while the account is a pool member of that scope, a pool member without an active grant to that scope, and a staleexpectedRevision. An unknown scope or Connection isresource_not_found. - Admission: a Session binds a Connection only when an active grant offers it
to the Session's Project or its Workspace; otherwise it fails closed with
configuration_invalid. A Project's pool overrides its Workspace's, and no default crosses Workspaces. Rotation picks among the pool's members at Session admission only, by the usual account order (least usage, then free slots, then name) and each account's own cap. A revoke also stops the next launch, recovery and queued-turn dispatch on that account (queued turns failturn_rejected). The grant check and the claim of a queued turn share one synchronous storage call, so a revoke never falls between them. A turn already claimed (dispatching) or running finishes: the validation after the claim is asynchronous, and a revoke that lands there does not stop its prompt. A launch past its check finishes too: launch preparation is asynchronous, so a revoke can land between the check and the spawn.maia session stopends a live runtime. Maia never rewrites a harness credential file.
Scheduled Session operations
Source candidate (S30). Ten release-disabled operations
(releaseQualified: false) in the routine feature
(packages/protocol/src/routine); the Node store is migration 65. Decision:
D-142.
| Operation | Risk | Confirmation | Receipt | SDK method | Exposure |
|---|---|---|---|---|---|
routine.list | read | none | none | listRoutines | CLI, coordinator |
routine.show | read | none | none | showRoutine | CLI, coordinator |
routine.create.preview | reversible | none | none | previewRoutineCreate | CLI |
routine.create.confirm | consequential | trusted-client-required | required | confirmRoutineCreate | CLI |
routine.pause | reversible | none | required | pauseRoutine | CLI |
routine.resume | reversible | none | required | resumeRoutine | CLI |
routine.disable.preview | reversible | none | none | previewRoutineDisable | CLI |
routine.disable.confirm | consequential | trusted-client-required | required | confirmRoutineDisable | CLI |
routine.run.preview | reversible | none | none | previewRoutineRun | CLI |
routine.run.confirm | consequential | trusted-client-required | required | confirmRoutineRun | CLI |
- A routine is
routine_plus 32 hex characters, derived from the actor and the createcommandId, so a retried create names the same routine. States areactive,pausedanddisabled;nextFireAtis present exactly while the routine isactive. The template names a Project and a prompt, plus anexecutoror aconnectionId, and amodelonly with aconnectionId. - The preview returns
routineId, up to 3nextFires,inputDigest, a one-timeproofand a 2-minute expiry. The digest binds the actor, command ID, routine ID, name,paused, schedule and template; the Node keeps only a hash of the proof and deletes it on confirmation. Create without a valid preview fails and mutates nothing. - Confirmation checks the proof, digest, actor and expiry, then inserts the
routine at revision
1in one transaction with its receipt (target typeroutine). The optional create inputpaused: trueinserts itpausedwith nonextFireAt; aresumethen schedules the first fire, so a routine never fires before someone releases it.pause,resumeanddisable.confirmcompare-and-setexpectedRevisionand record a receipt at the new revision.resumesetsnextFireAtto the first slot after the resume time, so it never replays a slot missed while the routine was paused. disableis final, so it previews first.routine.disable.previewbinds the actor, command ID, routine ID andexpectedRevision, names the routine, and returnsinputDigest, a one-timeproofand a 2-minute expiry. Onlyroutine.disable.confirmwith that proof, digest, command ID and revision disables the routine; a replay with other values fails withstorage_conflict. A preview of a routine that isdisabledor at another revision fails the same way and issues no proof.- The closed error set of a refused create is
routine_schedule_invalid,routine_interval_too_short(consecutive fires under 15 minutes apart),routine_limit_reached(20 active or paused routines in the Project) andconfiguration_invalid: a template its Connection can never serve, under the rulesession.createapplies (a gateway withoutexecutor, anexecutorother than a vendor account's, or amodelon a vendor account). The preview and the confirmation both check it. A stale or expired preview and a staleexpectedRevisionfail withstorage_conflict; an unknown routine, Project or Connection fails withresource_not_found. - Each due slot records one
routine_firesrow, the evidence of what happened:launching, thenstartedwith the Session and first turn, orfailedwith reasonconnection_disabled,held,launch_failed,outcome_unknown,routine_not_active,saturatedorworkspace_unavailable, orskippedwith reasonoverlap.routine.showreturns the newest 20, and each started fire carries the first turn's current state and, when that turn failed,failureReasonand the runtime'sfailure(code and message) asturn.showcarries them. A fire with a Session that failed before any turn carries that Session'sfailureassession.showcarries it, withoutfailureReason. The claim of a slot and the advance ofnextFireAtcommit in one transaction, so a slot fires at most once. - A full Node refuses
session.startwith the retryablesaturatederror, the code runtime admission uses. The fire stayslaunchingas a pending admission and retries each tick until its deadline, 10 minutes after the claim on the Effect clock, then records reasonsaturated. A tick makes at most 32 launch attempts: fresh claims first, so a due routine starts within ceil(due / 32) ticks, and pending retries with the rest. After a crash, alaunchingfire resumes asstartedonly from aturn.sendreceipt by its routine's scheduler actor on the Session the fire attached; any other fire recordsoutcome_unknown. Each scheduler tick stops the live Session of a settled fire whose turns are all its own and finished, throughsession.stopat the revision the eligibility query read. - A manual run (FV8) fires an
activeroutine once now. It starts a Session, so it previews first, asdisabledoes:routine.run.previewbinds the actor, command ID, routine ID andexpectedRevisionand returns the routine's name,inputDigest, a one-timeproofand a 2-minute expiry.routine.run.confirmspends that proof in one transaction that requires the same revision, judges the run as the next cron fire would be judged, records alaunchingfire withtriggerKind: "manual"andscheduledAtat the record time, and its receipt (target typeroutine_fire; the fire ID derives from the actor andcommandId). The scheduler's next tick claims it and launches it through the cron path, so there is no second scheduler. The claim judges it again, as it judges a cron slot: a routine not active at the claim fails it withroutine_not_active, a hold withheld, a disabled Connection withconnection_disabled. A due cron slot is judged first in that claim, so a slot that meets an unsettled manual fire isskippedwithoverlap, and a run never movesnextFireAt. Only cron fires keep one row per slot. A replay with the samecommandIdand proof returns the same fire; other values, or a stale, expired or spent proof or another revision, fail withstorage_conflict. The preview and the confirmation refuse and record nothing withroutine_busy(retryable) while the previous fire's first turn is unsettled,routine_not_active,held,connection_disabledorresource_not_found; a refused confirmation keeps its proof until it expires. An unclaimed manual fire survives a restart for the next tick to judge. Neither operation is a coordinator tool. - The scheduler acts as the Node's automation (an actor kind with no
credential, one ID per purpose). Its receipts name
actor_node-automation_routine-scheduler_<routineId>, neveractor_local_user. It mayworkspace.listandsession.createin its fire's Project, creating with only a Workspace,executororconnectionId, andmodel; thensession.show,session.start,session.stopandturn.sendon its fire's own Session. Anything else failsrole_denied.
Webhook operations
Source candidate. Four release-disabled operations (releaseQualified: false) in the webhooks feature (packages/protocol/src/webhooks), E12:
| Operation | Risk | Confirmation | Receipt | Exposure | SDK method |
|---|---|---|---|---|---|
webhooks.list | read | none | none | CLI | listWebhooks |
webhooks.deliveries | read | none | none | CLI | listWebhookDeliveries |
webhooks.change.preview | reversible | none | none | CLI | previewWebhookChange |
webhooks.change.confirm | consequential | trusted-client-required | required | CLI | confirmWebhookChange |
- All four are operator-only: an attached Session credential is refused.
- The
changeis a union onkind:add { spec }, andremoveandtest{ webhookId, expectedRevision }, andredeliver { deliveryId, expectedRevision }; the webhook is the one that owns the delivery. The preview binds the action, input digest, actor, hook revision, destination and expiry; confirmation compares the revision and commits in one transaction. The effect class of the confirm isexternal-uncertain. - Webhook IDs match
hook_[a-f0-9]{32}. The receipt names methodwebhooks.change.confirmand target typewebhook. A repeatedcommandIdreplays the stored result withdelivery: "duplicate". - The record holds the secret's variable name, never its value.
Agent definition operations
Source candidate. Five release-disabled operations
(releaseQualified: false) in the agents feature
(packages/protocol/src/agents), specified by S49 and D-120:
| Operation | Risk | Confirmation | Receipt | Exposure | SDK method |
|---|---|---|---|---|---|
agents.detect | read | none | none | CLI | detectAgents |
agents.list | read | none | none | CLI, coordinator | listAgents |
agents.show | read | none | none | CLI, coordinator | showAgent |
agents.change.preview | reversible | none | none | CLI | previewAgentChange |
agents.change.confirm | consequential | trusted-client-required | required | CLI | confirmAgentChange |
agents.detecttakes no parameters and returnsharnesses(claude-codeandcodex-cli:found,version,foldersof{ scope, path }) andscopes.user(ok, orunavailablewith a boundedreason,failed,timeout,output_too_large,invalid_outputorunsupported). It is operator-only likeconnection.detect: an attached Session credential is refused before any process runs. The result holds no Skill name, binary path, account or raw error text. Project scope is deferred to AI1c.- The
changeis a union onkind:create { projectId, spec },revise { agentId, expectedRevision, spec }anddisable { agentId, expectedRevision }. The spec schema (agentSpecSchema) is the only definition of a valid Agent. A preview lives 30 seconds, bindsinputDigestto the change, actor andcommandId, and returns aproofthe Node stores hashed. Confirmation checks the proof, then compares the expected revision and commits in one transaction. - An actor and
commandIdhold one open preview. Repeating the same change inside its 30 seconds returns the same reply (Agent ID,inputDigest,proofand expiry), so a lost or late reply never retires the proof another reply carries, and takes no extra preview capacity. The Node derives the proof from a key it keeps in memory; after a Node restart inside the window, a retry returns a proof of the new key and the earlier proof stops working. A different change under thatcommandIdfails withstorage_conflictand leaves the open preview as it was. - Agent IDs match
agent_[a-f0-9]{32}and the Node picks them. The receipt names methodagents.change.confirm, target typeagentand the new revision;receipt.listfilters on it. A repeatedcommandIdreplays the stored result withdelivery: "duplicate", even after the proof expired. An unknown, reused or expired proof, a proof bound to another change, actor orcommandId, and a revision that moved fail withstorage_conflictand change nothing. session.createaccepts anagentIdand pins the Agent ID and revision to the Session. The Node checks the executor for the revision it read; a revision made after that check fails withstorage_conflictand creates no Session.readAgentandreadAgentRevisioninservices/node/src/agentsare the in-Node read API for a later Routine (S30, S50) to name an Agent without a store of its own.
Events, traces, and cursors
The shared event envelope, journal, cursors and federation are specified in Event core (Planned, D-188); the records below are the target they evolve.
events.read (source candidate, EV0c-1; packages/protocol/src/event) reads
the journal by signed cursor, event ID or sequence. Its result is a page, a
cursor_reset_required or an invalid_cursor, each with status.
Today the Node API streams observe.events over /api/v1/observations (the
maia.node.observations.v1 WebSocket subprotocol) with snapshot, event,
heartbeat, gap, reset, error and summary frames, an opaque signed cursor, at
most 64 records per batch and 256 KiB per frame
(packages/protocol/src/local-node). The same socket serves session.attach
(source candidate, S12b; packages/protocol/src/session-stream): a
subscribe names one Session and an optional { epoch, sequence } cursor,
and the Node answers attached (replay or snapshot), records frames
(at most 64 SessionStreamRecords), synchronized (the position and each
open item's text so far), then live records and heartbeats, ending with a
terminal resync or error. Records are turn.started, item.started,
item.delta (live only), item.completed and turn.completed, each with
the Session, turn, owner sequence and Node epoch; items are message,
reasoning (visible summary only), tool_call, command, file_change,
approval_request, error and usage, with an optional namespaced
extension. The records below are the target.
Every durable transition emits an event in the owning aggregate's transaction.
EventV1 below is superseded by maia.event.v2 (Event core,
D-188), which has the same single discriminator and
maps each field; it was never emitted:
interface EventV1<TType extends string = string, TPayload = unknown> {
readonly schemaVersion: "maia.event.v1";
readonly eventId: string;
readonly eventType: TType;
readonly ownerType: string;
readonly ownerId: string;
readonly ownerSequence: number;
readonly aggregateRevision: number;
readonly projectId?: string;
readonly sessionId?: string;
readonly activationId?: string;
readonly attemptId?: string;
readonly actorId?: string;
readonly causationId: string;
readonly correlationId: string;
readonly classification: "public" | "operator" | "project" | "sensitive";
readonly occurredAt: string;
readonly payload: TPayload;
}
- Owner sequence is gap-free. Cross-owner order is never inferred from clocks; correlation and causation IDs express relationships.
- A stream cursor is an opaque signed encoding of owner, last sequence, filter
digest and expiry. An expired or compacted cursor returns
cursor_reset_requiredwith a fresh bounded snapshot descriptor; clients never guess a sequence. TraceEventV1is a redacted evidence projection of an event, effect, model stream item, tool call, provider attempt or diagnostic. It always carries correlation IDs and the source event, effect or receipt ID when one exists, and never credentials, authorization headers, hidden reasoning, raw environment dumps, raw audio or unbounded payloads.
All application-facing live streams use the observation contract:
type ObservationDomainV1 =
| "agent"
| "swarm"
| "node"
| "provider"
| "voice"
| "a2a"
| "security"
| "storage"
| "scheduler";
type JsonValueV1 =
| null
| boolean
| number
| string
| ReadonlyArray<JsonValueV1>
| { readonly [key: string]: JsonValueV1 };
interface ObservationRecordV1 {
readonly schemaVersion: "maia.observation.v1";
readonly observationId: string;
readonly domain: ObservationDomainV1;
readonly importance: "authoritative" | "diagnostic";
readonly eventType: string;
readonly nodeId: string;
readonly nodeEpoch: number;
readonly partitionId: number;
readonly partitionSequence: number;
readonly ownerType: string;
readonly ownerId: string;
readonly ownerSequence: number | null;
readonly projectId: string | null;
readonly sessionId: string | null;
readonly activationId: string | null;
readonly attemptId: string | null;
readonly effectId: string | null;
readonly commandId: string | null;
readonly receiptId: string | null;
readonly connectionId: string | null;
readonly causationId: string;
readonly correlationId: string;
readonly classification: "public" | "operator" | "project" | "sensitive";
readonly occurredAt: string;
readonly observedAt: string;
readonly schemaRef: string;
readonly payloadDisposition: "inline" | "artifact" | "redacted";
readonly payload: JsonValueV1;
readonly artifactRef: string | null;
readonly coalescedRecordCount: number;
readonly droppedRecordCountBefore: number;
}
interface ObservationQueryV1 {
readonly domains: ReadonlyArray<ObservationDomainV1>;
readonly importance: ReadonlyArray<"authoritative" | "diagnostic">;
readonly projectIds: ReadonlyArray<string>;
readonly sessionIds: ReadonlyArray<string>;
readonly eventTypes: ReadonlyArray<string>;
readonly classifications: ReadonlyArray<
"public" | "operator" | "project" | "sensitive"
>;
readonly includeContent: boolean;
readonly limit: number;
}
interface ObservationWatermarkV1 {
readonly topologyRevision: number;
readonly partitionSequences: ReadonlyArray<{
readonly partitionId: number;
readonly sequence: number;
}>;
readonly evidenceProjectedThrough: ReadonlyArray<{
readonly partitionId: number;
readonly sequence: number;
}>;
readonly coverage: "complete" | "degraded" | "reset-required";
}
interface ObservationBatchV1 {
readonly records: ReadonlyArray<ObservationRecordV1>;
readonly resolvedFilterDigest: string;
readonly watermark: ObservationWatermarkV1;
readonly coalescedRecordCount: number;
readonly droppedRecordCount: number;
readonly heartbeat: boolean;
}
interface ObservationSnapshotV1 {
readonly records: ReadonlyArray<ObservationRecordV1>;
readonly resolvedFilterDigest: string;
readonly watermark: ObservationWatermarkV1;
readonly generatedAt: string;
readonly truncated: boolean;
}
type ObservationCursorV1 = string & {
readonly ObservationCursorV1: unique symbol;
};
- Empty filter arrays mean "all authorized values" only for reads and never
grant authority. Node intersects the query with the actor's grants and
returns the resolved filter digest.
includeContentis separately authorized and defaults tofalse. ObservationCursorV1signs Node and epoch, topology revision, resolved filter digest, authorization-policy revision, watermark vector, issue and expiry time, and actor and device audience. It is invalid for another filter, actor, device or policy revision.ObservationBatchV1holds 1–128 ordered records from one or more partitions, or zero only whenheartbeat: true, plus the post-batch watermark and coalescing and gap counters. Order exists only within a partition or by owner causation; array position never implies cross-partition causality.
Internal bulk consumers persist:
interface ObservationProcessorCheckpointV1 {
readonly processorId: string;
readonly processorVersion: string;
readonly partitionId: number;
readonly acknowledgedSequence: number;
readonly inputSchemaDigest: string;
readonly projectionRevision: number;
readonly state: "running" | "lagging" | "blocked" | "rebuilding";
readonly updatedAt: string;
}
interface ObservationProcessorDescriptorV1 {
readonly processorId: string;
readonly processorVersion: string;
readonly criticality: "required" | "optional";
readonly domains: ReadonlyArray<ObservationDomainV1>;
readonly importance: ReadonlyArray<"authoritative" | "diagnostic">;
readonly eventTypes: ReadonlyArray<string>;
readonly inputSchemaDigest: string;
readonly outputSchemaRefs: ReadonlyArray<string>;
readonly checkpointVersion: number;
readonly maxBatchRecords: number;
readonly maxBatchBytes: number;
readonly maxAttempts: number;
readonly retryBackoffMs: number;
readonly deadLetter: "block" | "quarantine-and-continue";
readonly allowedOperationIds: ReadonlyArray<string>;
}
- Delivery is at least once. Projection writes and checkpoint advance commit atomically; processors are idempotent by processor, partition and sequence.
- A required processor uses
deadLetter: "block"; an optional one may quarantine a bounded batch and emit a diagnostic. Output is a projection or one of its allowed normal operation requests, never an unreceipted mutation. - A descriptor has 1–16 domains (empty
eventTypesmeans every authorized type in them), batches of at most 128 records or 512 KiB,maxAttempts1–10, backoff 100–60000 ms and at most 32 operation IDs. Admission pins its digest, implementation digest, owner, capability evidence and revision outside Project configuration. - Required evidence is never sampled; sampled debug logs publish dropped-record counters. A consequential transition whose required event, receipt or outbox row cannot commit fails. JSONL projection lag degrades health but does not invalidate a committed receipt; the outbox must catch up before retention or backup advances its watermark.
Lifecycle baseline
Every lifecycle uses a registry-owned transition table with caller,
preconditions, expected revisions, event, receipt, timeout and recovery path.
Today the Session schema uses idle, starting, running,
waiting-approval, interrupted, recovering, stopping, stopped and
failed (packages/protocol/src/session/effect-schema.ts). The closed
top-level target states are:
| Aggregate | States |
|---|---|
| Project | candidate, admitted, active, held, draining, archived |
| Session | created, queued, starting, running, idle, waiting, blocked, stopping, outcome-unknown, reconciling, completed, failed, cancelled, archived |
| Activation | admitted, queued, assigned, starting, running, waiting, interrupting, outcome-unknown, reconciling, succeeded, failed, cancelled, blocked |
| Effect | planned, started, succeeded, failed, outcome-unknown, reconciled |
| Message | accepted, available, claimed, processed, dead-letter, expired |
| RoutineTick | scheduled, admitted, dispatching, session-linked, completed, skipped, coalesced, blocked, failed, cancelled, outcome-unknown, reconciling |
| Attachment | open, closing, closed, expired, revoked |
| Connection | pending, active, degraded, revoked, logged-out |
Terminal or archived parents cannot create children. waiting means the
activation yielded for a registered interaction, message or timer. blocked
means policy or evidence prevents progress and needs a recorded unblocker.
idle means no activation holds the Session lease.
Worker protocol
Today the managed Pi worker speaks protocol version 1 as line-delimited JSON on
stdin and stdout (packages/agent/src/managed-worker/protocol.ts): commands
initialize, prompt, interrupt and shutdown; events initialized,
turn.started, turn.completed, turn.interrupted, shutdown.complete and
error; 64 KiB per command and 1 MiB per output. The target below extends it.
Target: length-bounded NDJSON over inherited pipes. Every frame has
protocolVersion, workerVersion, requestId, a monotonic sequence and one
discriminated type:
hello | initialize | initialized | prompt | event | tool-call | tool-result
observation-batch | interrupt | interrupted | heartbeat | shutdown | terminal
error | ack
- Limits: 1 MiB per frame, 16 KiB instruction or message content, 64 KiB inline
tool result, 256 unacknowledged authoritative event frames, and one diagnostic
queue of at most 2,048 records or 8 MiB.
observation-batchcarries at most 128 diagnostic records or 512 KiB and is sent without waiting for a client. Larger values become artifact references. - Timing: initialization within 15 seconds; heartbeats every 5 seconds, and three misses fence the worker; graceful interrupt gets 10 seconds, then process-group terminate 5 seconds before kill. A restart always creates a new attempt and never reuses an assignment epoch.
initializecarries one immutableAgentActivationV1, admitted history, the resolved tool manifest, a provider credential handle with expiry and the execution profile; never a Node database handle or ambient Pi state.- Pi SDK messages, streams, usage, tool calls, interruption, compaction and
provider errors are normalized before leaving the adapter. Authoritative
eventframes reference their operation or effect and replay by request and sequence. Diagnostics coalesce intoobservation-batch; saturation is reported in the next batch and Node emits an authoritative gap. - Stdout is protocol-only. Stderr is a bounded crash and bootstrap fallback, not the live feed.
Machine principal, lease and evidence (MN0)
Status: Source candidate schemas in packages/protocol/src/machine-principal/
and packages/protocol/src/machine-lease/; every schema rejects unknown fields.
No operation, transport or Node handler uses them yet: MN1 (enrollment) and MN2
(placement) add those. They keep what a remote Execution Node may be told and
may report apart from how that is transported.
Bounds
A bound is sourced from the named document or new policy: a value MN0 chooses and MN1/MN2 may change only with a decision and a matching fixture.
| Bound | Value | Basis |
|---|---|---|
| Heartbeat interval | 5 s | Sourced: swarm heartbeats, operations |
| Heartbeat misses that stop placement | 4 | Sourced: same |
| Pairing challenge; rotation start | 5 min; 50% of TTL | Sourced: operations; MN1 owns them |
| Lease grant TTL (minimum; maximum) | 27 s; 30 s | New policy; the minimum is the least that keeps the credited window below |
| Clock error budget | 2 s | New policy; covers monotonic rate error and timer granularity, never wall-clock offset |
| Maximum request round trip | 5 s | New policy (one heartbeat interval) |
| Bounded stop after the local deadline | 10 s | New policy (the graceful-interrupt window of the worker protocol) |
| Spool records; bytes; oldest unacked | 10,000; 64 MiB; 24 h | New policy |
| Grants per principal; capabilities each | 16; 8 | New policy; the largest context.show projection stays under its 16 KiB cap |
| Capability code; identifier length | 32; 122 characters | New policy; 122 keeps actor_<id> within the 128-character identifier limit |
The credited window minimum TTL - round trip - clock error budget (27 - 5 - 2
= 20 s) equals heartbeat interval x misses (20 s): every accepted grant leaves
the Node a local lease of at least the span four missed heartbeats cover. The
schema refuses a grant outside the minimum and maximum TTL, and a test pins the
relation. Lease timestamps carry at most millisecond precision, the precision
the TTL arithmetic compares. There is no reassignment bound: no constant lets a
replacement launch.
Assignment identity
MachineAssignment names the exact Control Node and registration incarnation,
the Project, the Execution Environment (environmentId, which holds the
existing workspace_* identity until the
naming migration),
environmentRevision and environmentDigest. The tuple (controlNodeId, environmentId, environmentRevision) names one immutable definition: a change
makes a new revision, and a revision's digest never changes.
environmentDigest is the sha256: digest of that revision's canonical
definition, the provenance a Node checks. The Node starts the assignment only
when the environment it prepared, or received through authorized, digest-checked
artifact transfer, has exactly that digest; otherwise it refuses and reports.
The definition format and its canonical encoding belong to MN2's preparation
operation. MN0 fixes only that the digest names an immutable revision and is
compared by equality.
Lease semantics
- A grant's duration is
expiresAt - issuedAt, withminimum TTL <= duration <= maximum TTL. A grant (MachineLeaseGrant) also carries an increasingrenewalSequence, therevocationRevision, a cumulative spoolacknowledgedThroughand therequestIdof the Node request it answers. The timestamps represent the duration; no party compares them with a wall clock to decide expiry. - The deadline is anchored to the request. A one-way timestamp cannot separate
clock offset from delay: a Node 10 s behind, hearing a grant 12 s late, sees
it as 2 s old and would end its lease 8 s after Control's. So the Node asks
for every grant, acquire and renew alike, with a
requestIdit has never used before and never reuses. It takes a monotonic reading when it first sends the request and another when the grant arrives. It refuses a grant that answers no outstanding request, or took longer than the maximum round trip; a refusal leaves the earlier deadline as it was. Control issues the grant after the Node sent the request, sosend + durationnever falls after Control's expiry, given item 4. - The local deadline is
first-send reading + duration - clock error budget. A retry of an unanswered request keeps itsrequestIdand its original first-send reading, so a later retry never moves the anchor later: Control may answer the retry with the grant it issued to the first send, and a deadline anchored to the retry would outlive that grant. The Node refuses a grant whose remaining lifetime (deadline - receive reading) is not positive; within the round-trip bound the minimum TTL leaves at least 20 s. - Control enforces durations with its own suspend-aware monotonic clock, never
by comparing a timestamp with its wall clock. For each assignment it keeps one
horizon: the latest
issue reading + durationover the last grant arunningreport confirmed (item 9) and every grant issued since, because the Node may hold any of them. A later, shorter grant never lowers the horizon: a 30 s grant issued at t=0 and a 27 s renewal issued at t=0.5 whose response is lost leave the horizon at t=30, not t=27.5, while the Node may still run to t=28. A confirming report drops the grants it replaced. If Control has no monotonic source that counts across suspend, it issues no grant. A discontinuity is a Control restart, a resume, or a wall and monotonic disagreement beyond the clock error budget. At one, Control sets every horizon tonow + maximum TTL, which is late for any grant it issued before, whatever its duration, and raises a placement barrier on every assignment. A barrier clears only when an authenticatedrunningreport confirms a grant issued after the discontinuity (item 9). A step in Control's wall clock therefore never makes expiry earlier: a wall clock stepped 10 s forward would expire the 30 s lease issued at t=1 at t=21 while the Node runs to t=28, and the monotonic horizon stays at t=31 until the disagreement is detected, when it moves later and blocks placement. - The Node fails closed where it cannot establish those bounds. Its monotonic source must keep counting while the host is suspended; with no such source, or after a restart between send and receive, it refuses the grant and admits no work.
- Suspend and resume. Wall-clock time serves the Node only as a tripwire: when the wall clock and the monotonic clock disagree by more than the clock error budget since the last accepted grant, or the host reports a resume, the Node treats the lease as expired at once. It admits no new work and starts the bounded stop of item 8. A later grant makes a lease valid again but never cancels a stop already begun or revives a stopped worker.
- It applies a grant only when
(leaseEpoch, renewalSequence)is greater than the last applied pair for that assignment, andrevocationRevisionis not below its own high-water mark. A replay, the samerequestIdand the same pair delivered again, is acknowledged and changes nothing: it never recomputes or extends a deadline. An equal pair under a differentrequestId, or a lower pair, is refused. The report names the grant it applied byappliedRequestIdandappliedRenewalSequence. The schema parses each grant alone, so this ordering is the Node's and Control's state to enforce. - At the local deadline the supervisor admits no new work and starts a bounded
stop:
phase: "stopping", then"stopped"once process-tree cleanup is confirmed, within the stop bound. Failing that it keeps reportingstopping, and the assignment staysuncertain. - Placement depends on lease validity as well as on heartbeats. The Control
Node places new work on a Node only while fewer than four consecutive
heartbeats are missed, Control's horizon (item 4) has not been reached, no
placement barrier stands, no revocation or fence applies, and the Node's last
authenticated report is
runningand confirms the newest grant issued for that assignment. A report confirms a grant when itsappliedRequestId,appliedRenewalSequence, assignment and lease epoch equal that grant's. Issuing a grant proves nothing about the Node: a lost response, or a report that names an older request or sequence, leaves placement blocked until a later report confirms the newest grant. A heartbeat shows the Node is reachable, not that its lease holds. Neither missed heartbeats nor a Control-side fence prove the process stopped. - A replacement launches only after cleanup reconciliation (the Node reports
stoppedfor that assignment and epoch) or an explicit expiry reconciliation that MN2 defines with an overlapping-execution test. Expiry alone never authorizes a launch, because execution can continue after Control's expiry: with the request sent at t=0, the grant issued at t=1 and a 30 s duration, the Node's deadline is t=28 (2 s budget), Control's expiry is t=31, and the bounded stop may run to t=38. A local checkpoint never renews a lease.
Fixtures, all pending MN2 because they need a stateful Node and Control
(manifest):
lease-delayed-delivery (the 10 s behind, 12 s late example),
lease-unmatched-grant, lease-retry-anchor (a retry keeps the first-send
reading), lease-replay (no deadline change), lease-renewal-order,
lease-control-clock-step (a 10 s step in Control's wall clock never makes
expiry earlier: expiry defers and placement is blocked),
lease-shorter-renewal-lost (a lost 27 s renewal after a 30 s grant leaves the
horizon at the 30 s grant's; a discontinuity waits the full maximum TTL),
lease-grant-response-lost (a fresh grant after a discontinuity whose response
is lost, beside an old running report, keeps placement blocked),
lease-stale-report (a running report naming an older request or sequence
does not confirm the newest grant), lease-suspend-resume and
lease-placement-validity.
Evidence and spool semantics
- Each record has a per-source
sequence, contiguous from 1, so a cumulative acknowledgement (acknowledgedThrough) is unambiguous. It and the Node'struncatedThroughobeytruncated <= acknowledged <= last. A Node removes only acknowledged records, and no age rule removes an unacknowledged one. - The Node holds exactly the records after
truncatedThrough, so a status reportsretainedRecords = lastSequence - truncatedThrough. Records and bytes are both zero or both positive, andoldestUnacknowledgedAgeMsis 0 onceacknowledgedThrough = lastSequence. A status that says otherwise reports unacknowledged writes with no retained evidence and is invalid. - Reaching the record, byte or oldest-unacknowledged-age bound sets
admission: "stopped": the supervisor admits no new work and either interrupts safely within a bound or reports explicit uncertainty. A status that reaches a bound while open, or exceeds the record or byte quota, is invalid. No value represents a silently dropped record.
Identity generations
| Generation | Minted by, when | Changes on | Detects |
|---|---|---|---|
Durable Control identity (controlNodeId) | First-owner bootstrap | Never; a restore keeps it. The stored node_epoch is the bootstrap process's, not the current one | A grant or certificate for another Control Node |
Process epoch (instanceId; Node's processEpoch) | Each process start | Every restart | Stale cursors, bearer and in-flight deliveries |
| Registration incarnation | Control Node at enrollment | Re-enrollment, never rotation | A wiped Node re-enrolled under the same Node ID |
Credential/revocation generation (revocationRevision) | Control Node, per registration | Rotation, grant change, revocation | A stale certificate or grant |
| Worker incarnation | Execution Node, per start | Every worker start | Late events from a previous worker |
| Lease epoch and renewal sequence | Control Node | Acquire, handoff, renewal | A stale writer |
Restore and rollback
From one side, ordinary lag and a rollback look alike, and a restore of both sides looks like neither. The rule therefore never depends on detecting a restore.
- Stale credential (ordinary lag). After a rotation, grant change or
revocation, a Node that was offline holds a lower
revocationRevisionthan the Control Node. That proves nothing about a restore. The Control Node compares every presented certificate and grant with its current record and refuses a stale one; the Node recovers by rotation or renewal, or by a fresh enrollment after a revocation. - Proven rollback. Only a contradiction proves one: a Node reporting an
observedRevocationRevisionabove the Control Node's record (the Control Node was restored), or alastSequencebelow an acknowledgement the Control Node already recorded (the Node lost evidence). Either marks the registrationrecovery-required: no grant, renewal or machine credential is admitted for it until administrator recovery ends in a fresh enrollment, with a new registration incarnation and certificate. - Restore is administrator recovery. Restoring a Control Node or an
Execution Node from a backup is never transparent. The restore procedure
(operations), not
detection, marks every registration of a restored Control Node
recovery-requiredand makes a restored Execution Node discard its enrollment, spool and high-water store; each ends in a fresh enrollment. That holds when no contradiction exists: with both sides restored to the same point, a certificate revoked after the backup would otherwise be valid again. Copying state back outside that procedure is not detected and is outside the guarantee. - No transparent recovery. It would need a trusted recovery generation held outside the restored state. None exists, so none is offered; MN1 may add one only with E2a's secret store and a decision.
Fixtures, all pending MN1 because they need enrollment and a restore procedure:
restore-ordinary-lag, restore-unilateral (Control alone, and Node alone),
restore-matching and restore-revoked-credential
(manifest).
MN1 gap inventory
What MN1 (enroll, inspect, rotate and revoke an exact Node) still needs. Nothing below is built, and no operation ID is reserved for it.
| Need | Exists today | Missing before MN1 can ship |
|---|---|---|
| Operations | context.show (client, session and machine projection); team-workspace.bootstrap.preview and .apply (human-only, release-disabled); node.remote.* (SSH install or registration of a remote Node, D-080, D-176, not enrollment) | Challenge, enroll, inspect, rotate and revoke for one exact Node, plus the machine reads, lease renewal, report and evidence operations. Each is incomplete until its protocol entry, SDK method, Node handler, receipt policy, contract tests and CLI projection agree. |
| Managed administrator | A first owner, a User, Device and client row, and a bearer that resolves to none of them (specification) | A verified administrator credential on a running Node. The bearer is transport authentication, and the inherited descriptor of D-136 exists only while the CLI starts the Node, so neither can confirm an enrollment (first version: the D-150 ceremony). E2a's managed secret store comes first (ROADMAP), then the managed client credential and the bearer-to-owner mapping. |
| Pairing and key binding | The documented target: a one-use, five-minute challenge and a Node-generated key pair (operations) | The challenge format and storage (digest only), proof of possession of the Node's key, the certificate authority and its key custody (E2a), and how tailnet identity evidence is checked and recorded, as evidence and never as authentication. |
| Pre-credential admission | A loopback listener that authenticates a bearer before it reads a body | A tailnet listener that admits a joining Node with no credential yet: one route, bounded body and attempt counts, replay and expiry denial, no Funnel, and a decision on mTLS termination. |
| Certificate lifecycle | Targets only: rotation from 50% of the TTL, immediate revocation that fences assignments and streams | The certificate TTL, the per-request revocation check against revocationRevision, and the order in which a revocation fences leases, streams and the spool. |
| Restore | The fail-closed restore rule above | The recovery-required registration state and the administrator recovery that ends in a fresh enrollment, the restore procedure that marks every registration, and the durable high-water store on the Execution Node that yields the contradiction signals. |
| Machine transport admission | A machine projection that no transport mints | The machine operation set and its allow-list, so allowedActions stops being empty, and the transport that resolves a certificate to a principal. |
Owner decisions still open: the operation grammar (execution-node.* with
preview, confirmation and receipt, or a CLI-drafted plan); how the administrator
credential reaches a running Node (a maintenance-window enrollment that
restarts the Node with a fresh inherited descriptor is accepted meanwhile for
the first version, with revoke and narrow on the local bearer until the managed
administrator credential ends that exception,
D-150); the certificate TTL; and the new-policy values
in the table above.
Artifact and data lifecycle
Artifacts are immutable, content-addressed encrypted blobs with a metadata row:
interface ArtifactV1 {
readonly artifactId: string;
readonly digest: `sha256:${string}`;
readonly mediaType: string;
readonly sizeBytes: number;
readonly classification: "operator" | "project" | "sensitive";
readonly projectId: string;
readonly ownerType: string;
readonly ownerId: string;
readonly encryptionKeyId: string;
readonly provenance: ReadonlyArray<string>;
readonly createdAt: string;
readonly expiresAt?: string;
}
- Imports reject private-network URLs, redirects across policy boundaries, unsupported media types, digest mismatch, archive traversal, malware, detected credentials and policy-exceeding size.
- Download authorization is checked at request time. Garbage collection needs a preview, reference scan, tombstone and receipt.
RetentionPolicyV1sets a TTL per event, trace, artifact, transcript, evaluation, export and receipt class, plus legal hold, encryption-erasure behavior and whether a digest or tombstone may remain. Raw audio retention is always false in v1. Redaction happens before persistence or external transmission; a failed required redaction fails closed. No class keeps Session conversation content after transit (D-168).
Schema-only inspection
Approved target for R1 (PF-26/DF-26); the CLI has no --schema option today.
Schema inspection is an option on each registered resource command, not a
separate contract command family
(decision).
- Syntax.
--schemameans--schema=output; the only other value is--schema=input(no spaced value).--jsonis accepted but redundant. Schema mode needs only a known registered command path, not target IDs, revisions, command IDs, confirmation material or required input. - Offline. Resolve schema mode after bounded command-path parsing and before execution arguments, stdin, configuration, profile or credential discovery, SDK transport creation or Node startup. Emit the installed CLI's bundled public schema. Never execute the command, start or contact a Node, contact a provider or network, load runtime plugins, query live permissions, reserve IDs, issue confirmations or receipts, write files or start workers.
- Rejections. Reject runtime operands and options (including
--profile,--node, stdin and confirmation flags and output paths), streaming or watch flags, duplicate selectors, unsupported values and unknown commands before any effect; never fall through to execution when a schema is missing.--schema-digestis not--schema. Recognize the option only at registered resource-command boundaries; tokens after--and themaia agentpassthrough grammar are not intercepted. - Output. On success stdout is exactly one complete JSON Schema document and
a newline, exit 0, with a
$schemadialect, a versioned$idand bundled references. On failure stdout is empty, exit is nonzero and stderr has a bounded error that never echoes secrets or full inputs. Never a partial schema or fetched references. Freeze output bounds in fixtures first. - Meaning.
inputdescribes the normalized public operation parameters, not CLI flags (that is--help).outputdescribes the command's full ordinary non-streaming JSON success and failure responses, including the CLI envelope. Existing output formats do not change for this feature. - Generation. Generate or mechanically check the documents against the owning Effect schemas and exact descriptor versions; keep representable constraints, describe runtime-only refinements, and fail explicitly rather than widening a schema. Identical artifacts give identical bytes: no request IDs, time, user paths, credentials or live examples.
- Reuse. SDK schema access and eligible model and voice tool descriptions reuse the same generated definitions, with distinct surface envelopes. No shelling out to the CLI, hand-written validators or universal model dispatcher. A static schema grants neither tool exposure nor invocation.
- No live claims. Inspection makes no claim about another Node's version, capabilities, grants or release status. A release-disabled command may expose its schema; an unregistered one fails. Example plural commands do not become aliases.
Examples once R1 lands: maia session show --schema (no Session ID needed) and
maia session create --schema=input. Commands such as
maia workers list --schema follow only when their own slices exist.
Generation and conformance
The registry generates or mechanically checks:
| Artifact | Source |
|---|---|
| Effect request/result/error types | operation descriptors and referenced schemas |
| CLI grammar metadata | operation descriptors plus CLI presentation file |
maia help --json | registry plus implementation capability manifest |
| SDK methods | operation descriptors |
Per-command --schema documents and SDK/tool schema projections | owning Effect schemas, exact operation descriptors and surface envelope metadata |
| model action schemas | eligible descriptors plus Agent policy |
| voice action schemas | eligible descriptors plus Voice policy |
| event and receipt catalogues | descriptor event/receipt metadata |
| OpenAPI | descriptors explicitly exposed over HTTP |
Today scripts/quality/registry-parity.mjs (run by pnpm test:tooling)
mechanically checks registry, SDK, Node handler and CLI help parity. The
maintainer commands maia contract generate, maia contract check --json and
maia contract fixtures verify are targets (CLI); until they exist,
handwritten projections cannot be labelled generated.
- Each operation fixture covers valid input and output, every typed error, preview and confirmation when applicable, emitted events, the receipt, a redaction scan, idempotent replay, stale-revision rejection and cross-version decode. Every vertical slice changes registry, service, SDK/CLI, docs, fixtures and tests together.
- Observation fixtures also cover snapshot-then-events, aggregate filters, 32-partition watermark vectors, owner causality, duplicate batches, checkpoint atomicity, restart replay, diagnostic coalescing and gaps, slow-consumer disconnect, cursor expiry and reset, policy-revision changes, content-grant denial, Node epoch change, loopback WebSocket and remote WSS and schema equivalence.
Imported from Maia OS docs at 3dc3e07