Skip to main content
@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.
interface CloneProvider {
  readonly kind: 'local' | 'cloud';

  create(spec: CloneSpec): Promise<CloneInstance>;
  start(id: string): Promise<void>;
  stop(id: string): Promise<void>;
  destroy(id: string): Promise<void>;
  status(id: string): Promise<CloneStatus>;
  logs(id: string, opts?: LogOptions): AsyncIterable<LogLine>;
  seed(id: string, spec: SeedSpec): Promise<SeedResult>;
  reset(id: string): Promise<void>;

  // Phase 2, optional — LocalDockerProvider may omit:
  snapshot?(id: string, name: string): Promise<void>;
  restore?(id: string, name: string): Promise<void>;
}
Two implementations are envisioned: 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.
FieldTypeNotes
templatestringMust exist in the catalog.
mode?CloneModeDefaults to the template’s first mode.
name?stringElse a stable id is generated.
group?stringEnvironment to join, by name; creates it if new.
seed?SeedSpecSeed on create; omit to start empty.
portRange?PortRangeOverride the default range.
expose?booleanDefault 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 keeps reset byte-identical even as :latest moves; asym upgrade repins to the current image. imageDigest is null for 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-shaped botToken / userToken). The clone DB stores only hashes, so these are persisted here (in the 0600 registry) for asym tokens <id> to reprint. They’re redacted from status / ls output and shown by spin / tokens.

CloneState

creating → running → stopped → running → destroying → gone
running → unhealthy → running   (health flaps)
creating → failed
gone is terminal and never persisted — it lets status report a clone that vanished outside the CLI, which doctor reconciles.

SeedSpec / SeedResult

interface SeedSpec {
  fixture?: string;              // deterministic SQL pack
  ai?: AiSeedSpec | boolean;     // true = template defaults
}

interface AiSeedSpec {
  entities?: string[];                                  // e.g. ["users","channels"]
  volume?: 'small' | 'medium' | 'large' | number;
  model?: string;                                       // e.g. "claude-haiku-4-5"
  prompt?: string;                                      // steering brief
}
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:
TemplaterealProductModesAI seed entities
Slackslackapi, fullusers, channels, messages
Linearlinearapiusers, teams, issues, projects
Stripestripeapicustomers, products, prices, invoices
Notionnotionapiusers, databases, pages
HubSpothubspotapicontacts, companies, deals
GitHubgithubapiusers, repositories, issues
Slack is the only one with a 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.