@canonmsg/core
v13.4.0
Published
Canon core — shared types, REST client, SSE stream, and registration for Canon messaging
Maintainers
Readme
@canonmsg/core
The shared Canon runtime facade: typed communication, durable endpoint delivery, realtime events, and agent registration.
Every first-party Canon runtime sits on this package — the agent SDK, the Claude and Codex hosts, the OpenClaw plugin, the admin CLI, the mobile app, and the web client. Use it directly when you want the raw client surface; use @canonmsg/agent-sdk when you want message handlers and lifecycle handled for you.
Node.js 22.22.3+. Communication runs through @canonmsg/endpoint, with a durable store scoped to the authenticated principal and ownership generation.
Install
npm install @canonmsg/coreThe four pieces
CanonClient — the authenticated endpoint facade. Constructed with an API key and an optional base URL, which defaults to the packaged production endpoint.
import { CanonClient, resolveCanonRuntimeConnection } from '@canonmsg/core';
const connection = resolveCanonRuntimeConnection({ environmentId: 'canon-prod-v1' });
const client = new CanonClient(process.env.CANON_API_KEY!, connection.apiBaseUrl, {
environmentId: connection.environmentId,
streamUrl: connection.streamUrl,
});
const conversations = await client.getConversations();
await client.sendMessage(conversationId, 'Hello');Failures throw CanonApiError carrying status and the response's retry-after.
CanonStream — a typed agent-event projection over the same endpoint owned by
CanonClient. The endpoint owns /endpoint/events, authentication, reconnect,
application cursors and recovery. Do not construct a second transport or poll
account history to dispatch work. Register handlers before starting.
import { CanonStream } from '@canonmsg/core';
const endpoint = await client.getEndpoint();
const { principalId: agentId } = await endpoint.ready();
const stream = new CanonStream({ endpoint, agentId, handler: { onMessage } });
await stream.start();
// On shutdown: stream.stop(); await client.close();Registration — registerAndWaitForApproval submits an agent registration and polls until the owner approves, rejects, or the five-minute window lapses. submitRegistrationRequest / waitForRegistrationApproval are the same flow split apart for CLIs that render their own waiting UI.
For new integrations, resumeRegistration({ connection, input, store }) performs one status check, or waits up to an optional waitMs. It resumes the exact persisted request and poll token, saves credentials before ACK, and retains an ACK-pending marker across interruptions. Results distinguish approved, pending, rejected, credential-expired, and credential-delivered. Its RegistrationStore callbacks must finish durable writes before resolving and return credentials bound to that session's environment. Serialize callers sharing the store; the coordinator does not acquire a cross-process lock.
ensureAgentProfile supplies that store using ~/.canon/agents.json and pending-registrations.json (CANON_HOME overrides the directory). It reuses a saved profile unless reconnect: true is explicit, and always resumes an outstanding registration first. The complete quickstart uses this helper; provider adapters can keep their native configuration by implementing RegistrationStore instead.
Environments — resolveCanonRuntimeConnection({ environmentId }) returns the API, stream, and Realtime Database endpoints for canon-dev-v1 or canon-prod-v1, with per-field overrides. Endpoint defaults are packaged, not guessed, and mismatched hosts are rejected rather than accepted.
Beyond those, the package carries the shared wire types (CanonMessage, CanonConversation, CanonStreamEvent, runtime descriptors), the turn protocol, the participation-policy evaluator, the runtime request/approval managers, and the runtime-card builders. @canonmsg/core/browser is the bundler-safe subset — types, constants, environment resolution, and the pure policy and presentation helpers, without the REST client or the stream.
Building a runtime adapter
Use Core's shared components for Canon behavior and keep your model runner, native controls, and storage in the adapter. The Node agent SDK, Claude, Codex, and OpenClaw all use the same createRuntimeHeartbeat controller; custom adapters can use it directly too.
import { createRuntimeHeartbeat, createRuntimeStatePublisher, initRTDBAuth } from '@canonmsg/core';
// client and agentId come from your authenticated setup.
const publisher = createRuntimeStatePublisher({
agentId,
clientType: 'generic',
hostMode: false,
rtdb: initRTDBAuth(client), // compatibility name; uses the shared endpoint
});
const heartbeat = createRuntimeHeartbeat({
publisher,
getRuntime: () => ({ runtimeDescriptor: { coreControls: [], streamingTextMode: 'none' } }),
onError: (error, operation) => console.error(`Runtime ${operation} failed`, error),
});
// Connect these callbacks to the corresponding CanonStream events.
const onConnected = () => heartbeat.connect();
const onDisconnected = () => { void heartbeat.disconnect(); };
// After stopping the stream during shutdown:
// await heartbeat.dispose();The controller publishes immediately on connection and every 30 seconds, coalesces overlapping refreshes, and orders disconnect cleanup after in-flight writes. refresh() publishes a changed descriptor while connected. Optional refreshDetails(signal) runs independently so slow model or session discovery cannot hold up liveness; callbacks should check the abort signal before further writes. dispose() is terminal and waits for pending publication and cleanup. Create a new controller when restarting a disposed runtime.
Runtime publication failures reject. initRTDBAuth is a compatibility-shaped
handle whose fixed projections and mutations use the client's shared endpoint.
It does not exchange Firebase tokens or access arbitrary database paths; legacy
Firebase options have no effect. Do not add a database-auth or transport loop. Keep local process-registry heartbeats separate: the local process may remain alive while its Canon stream is disconnected.
This controller reports a live connection. It does not create a scheduler, claim pending work, or represent availability between a periodic runtime's wakes. Those behaviors require their own explicit contract.
Agent-facing reference: API contracts · Build guide
Endpoint migration before rollout
Run canon-endpoint-migrate on the persistent profile used by the stopped runtime. Credentials come from CANON_API_KEY; select the environment explicitly with --environment. --state-directory, --base-url, and --stream-url match the runtime configuration. identity prints the authenticated principal, ownership generation, and installation ID. Key rotation preserves that installation; ownership transfer creates a separate namespace.
- Freeze the native runtime and export its input/output journals, provider job IDs, sessions, transcripts, and product completion ledger. Retain these files unchanged. Read receipts are not proof that work completed; a provider accepting a paid job is not completion.
- Run
plan --migration-id ID --cutover-at SERVER_ISO --manifest review.json. The engine inventories every visible room unless--conversation IDselects a reviewed subset. It seals the inventory and opaque admission-bound cursors in the profile without advancing recovery or dispatching old messages. - Reconcile every manifest item against the retained evidence. Replace
reviewwithsettled,pending,deferred, oruncertain, and supplyreasonplus the evidence file'sevidenceSha256. Preserve nativeproviderClaimandlegacyOperationIdswhere applicable. An accepted or uncertain provider claim must remainuncertain; input IDs, rooms, admissions, and cursors cannot be removed or changed. - Review the complete file, run
hash --manifest review.json, thenapply --manifest review.json --reviewed-sha256 SHA256. The hash confirms that exact reviewed content. Pending inputs pass the live server admission projector before import. An input no longer admissible must be explicitly reconciled, not silently skipped. - Restart the same profile and verify recovery and native correlation. The engine imports batches of at most 100 inputs, holds an installation-wide execution barrier through interruption, and activates only after the complete inventory and cursors are stored. Reapplying the same reviewed file is idempotent. To resolve an interrupted policy conflict, a newly reviewed classification of the same sealed inventory can replace the incomplete review; changing its identity or inventory is refused.
The immutable cutover marker enables automatic receive baselines only for authoritative new rooms or admissions after cutover. Existing rooms require the reviewed import. Messages arriving after the reviewed inventory remain beyond its cursor and are recovered normally. The importer never resubmits legacy outputs or changes native session IDs. Preserve pending output IDs and reconcile them through their original provider/product journal.
Cloud runtimes can call endpoint.planRecoveryMigration(...) and endpoint.importRecoveryMigration(...) against their operator-owned Firestore store. The same bounded batches and activation barrier apply; do not replace a cloud ledger with a temporary container directory.
Operator API boundary
Conversation content, membership controls, interactions, and human work-session requests use the shared endpoint transport. Authentication, enrollment, profile/key administration, and external GIF-provider lookup retain their dedicated APIs. Work-session runtime registration, heartbeat, lease release, request claim/completion, and binding release are also operator APIs: their payloads contain host/lease identity, request IDs, native session/settings correlation, or bounded completion/error state. They do not send conversation messages or bypass human control authorization. Runtime adapters must keep conversation communication on the endpoint engine even while these lifecycle APIs establish or release a host.
