@repo/clone-contract is the internal contract the CLI and the (planned) control
plane are built against. It defines what a clone is and how a provider drives
one, with no runtime or Docker specifics — that’s what lets local and cloud share
one command surface. This page documents that contract for reference, so you
understand how clones behave when you point an agent at one.
The CloneProvider interface
The seam. Every method maps to a CLI verb, and implementations must throw a
named error, never a bare Error.
LocalDockerProvider (today — runs in the
CLI, shells to Docker) and CloudProvider (planned — a thin client to the
control-plane API). Same interface, different engine.
Core types
CloneSpec — input to create
What the caller wants; the provider fills in the allocated fields.
| Field | Type | Notes |
|---|---|---|
template | string | Must exist in the catalog. |
mode? | CloneMode | Defaults to the template’s first mode. |
name? | string | Else a stable id is generated. |
group? | string | Environment to join, by name; creates it if new. |
seed? | SeedSpec | Seed on create; omit to start empty. |
portRange? | PortRange | Override the default range. |
expose? | boolean | Default false (localhost-only). |
CloneRecord / CloneInstance
CloneRecord is the JSON-serializable row persisted to
~/.asymmetric/registry.json: id, template, mode, composeProject,
database, redisPrefix, ports, state, seed, expose,
image / imageDigest, tokens, and createdAt / updatedAt / syncedAt. A
clone does not carry its environment — membership lives on the
Environment as the single source of truth. CloneInstance is a
CloneRecord plus resolved endpoints (api, web) — what create returns.
Three fields are optional and additive (older records and templates without them
simply omit them):
image/imageDigest— the published image ref the clone was spun from and the content digest (<repo>@sha256:…) resolved at spin time. Pinning the digest keepsresetbyte-identical even as:latestmoves;asym upgraderepins to the current image.imageDigestisnullfor local builds with no registry digest.tokens— programmatic-access credentials minted by the clone’s default app at provision time (appId,workspaceId,botUserId,adminUserId, plus a Slack-shapedbotToken/userToken). The clone DB stores only hashes, so these are persisted here (in the 0600 registry) forasym tokens <id>to reprint. They’re redacted fromstatus/lsoutput and shown byspin/tokens.
CloneState
gone is terminal and never persisted — it lets status report a clone that
vanished outside the CLI, which doctor reconciles.
SeedSpec / SeedResult
SeedResult reports written, rejected, a bySource breakdown, and
warnings[]. AI partial failures are reported here, not thrown — a few bad
records don’t fail the seed.
CloneStatus / ServiceStatus
The live health view (distinct from the persisted CloneState): per-service
runtime state (running / exited / unhealthy / missing), port, and
healthy, rolled up into a clone-level healthy boolean.
Environments
Environment is the top-level composition primitive: name, members (clone ids,
the source of truth for membership), spec (path to the environment.yaml, or
null for ad-hoc), state, and timestamps. EnvironmentState is
running / partial / stopped / failed. EnvironmentResult (per-member
outcome) is reserved for the deferred multi-clone engine (e.g. an env reset loop).
See Environments.
RegistryFile
The on-disk shape of ~/.asymmetric/registry.json: a version, an array of
clones (CloneRecord[]), and an array of environments (Environment[]).
REGISTRY_VERSION is 2; the CLI migrates older files on read (v1 → v2 renamed
groups to environments and rebuilt environments from legacy per-clone group
tags, so upgrades lose no membership).
FidelityScore
Surfaced by verify and produced by the /verify-api
workflow: score (0–1 coverage), operations (ours / theirs / matched),
checkedAt, and source. Cached under ~/.asymmetric/fidelity/<template>.json,
and never hard-gates spin.
The TemplateManifest
How a template declares its services, modes, database/migrations, Redis prefix,
and seed sources — a clone.manifest.json at the template’s app root. Walked
through in Templates. This reference is provided so you
understand how clones are structured; templates themselves are built and
maintained by asymmetric.
Each service declares either a build context (built locally) or a published
image ref (pulled, with no source present) — image is the distribution
default, e.g. ghcr.io/asymmetric-ai/asymmetric-slack-backend:latest. A service
marked optional: true is only run in the modes that name it.
Six templates ship today, all on the same manifest shape:
| Template | realProduct | Modes | AI seed entities |
|---|---|---|---|
| Slack | slack | api, full | users, channels, messages |
| Linear | linear | api | users, teams, issues, projects |
| Stripe | stripe | api | customers, products, prices, invoices |
| Notion | notion | api | users, databases, pages |
| HubSpot | hubspot | api | contacts, companies, deals |
| GitHub | github | api | users, repositories, issues |
full mode today — it ships a build-based
frontend service alongside its image backend. The other five are backend-only
(api) services pulled as images.
Design rules
Providers run client-side
LocalDockerProvider shells to Docker from the CLI. CloudProvider is a thin
client. The interface is the only shared surface.Throw named errors
Never a bare
Error. The CLI switches on the error code to render a clear
message. if (isAsymmetricError(e)) ...Records are JSON
Everything in
CloneRecord is JSON-serializable — it lands verbatim in the
registry file.One label
Docker resources carry the
asym.clone label so they’re discoverable and
reconcilable.