Hanzo

Sync

Package sync is data sync: link two endpoints and keep them in step, on a webhook, on a schedule, or on demand.

Package sync is data sync: link two endpoints and keep them in step, on a webhook, on a schedule, or on demand.

Base URLhttps://api.hanzo.ai
Operations6
AuthAuthorization: Bearer $HANZO_API_KEY

Specification

HIP-1154 · Sync — Two Endpoints Kept in Step — Draft · read the specification →

/v1/sync is data sync: a Sync names two endpoints and the engine keeps them in step, on a webhook, on a schedule, or on demand. Git — GitHub/GitLab in either direction with native Hanzo Git — is the one provider registered today; another kind is another Provider, with nothing in the engine to change. It is implemented in hanzoai/cloud at apps/sync, and its safety property is a single rule: the forge is canonical and an inbound sync may only fast-forward it.

Motivation

Customers arrive with their code on a host they are not leaving on day one, and the forge is only useful if it holds the same history without anyone pushing twice. Before this capability the pieces were scattered: triggers synced directly, the git hardening lived beside a retired object store, and no record existed of what an advance had done. Routing every trigger through one engine makes "why did this ref move" a question with one answer in one store.

Specification

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.

The store

Per-org SQLite files through the org-store registry: storeFor is the one way the package reaches a store, naming the database through the single door a validated org walks through (apps/sync/sync.go, storeFor). The store holds the sync intent, the engine's cursor, and the two facts the forge cannot hold — what an advance did, and where a repo replicates to. A sync is org-scoped, not project-scoped: a link binds two endpoints within one org.

The address

Six operations under /v1/sync: list, create, get, patch, delete, and POST /v1/sync/{id}/run for a manual reconcile. All are typed except the delete, which answers 204 with no body — there is no value to type. Triggers (the GitHub App webhook, the forge push webhook) resolve to Syncs and call the registered reconcile — they never sync directly, so the engine is the single seam. The same reconcile and the git object seams are offered on the internal plane to the processes the triggers actually land in (apps/sync/run_plane.go, apps/sync/import_plane.go).

The engine

One place a sync happens: resolve → loop-guard → cursor dedupe → provider.Apply → chain, hop-bounded, kind-agnostic (apps/sync/engine.go). A trigger is webhook, poll or manual; poll freshness is a periodic reconcile sweep that stays dormant until CLOUD_SYNC_RECONCILE_INTERVAL is set on the deployment (apps/sync/scheduler.go).

Fast-forward only

An advance moves one ref from one git host to another and MUST refuse to move it any way but forward: if the upstream's tip is not a descendant of what the forge holds, the two have diverged and the answer is a CONFLICT with the forge untouched — never an overwrite (apps/sync/advance.go). Getting this wrong is silent — a force-push that eats a colleague's commits leaves a repository that looks fine — so the rule is enforced by the transfer itself, a non-forcing refspec, not by a check beside it.

Tenancy

The org is principal.Acting from the validated principal (apps/sync/sync_api.go:110); storeFor accepts only an already-validated org, so which file a request touches has one answer from one input.

Money, events, observability, stage

It is free — the surface declares cloud.Free (plugin/sync/main.go). It publishes nothing on the bus: webhooks are its inputs, not its outputs. It emits nothing beyond the request span every route gets; the advance record in the store is the durable account of what a reconcile did. The stage is ga: it is developer-tools core, the mechanism that keeps the forge in step with the hosts customers already use.

Upstream

The one external program is the git client (GPL-2.0), resolved once via LookPath and run as a subprocess — never linked (apps/sync/gitexec.go). GitHub and GitLab are remote protocols spoken, not code embedded. Everything else is the fleet's own.

Rationale

The alternative to one kind-agnostic engine is a git-sync feature, and then a second engine when the second kind arrives. The Provider seam costs one indirection and buys the loop-guard, cursor dedupe and hop bound being written once. The alternative to fast-forward-only is conflict resolution, which for a source of truth is a euphemism for choosing whose commits to destroy; refusing with the forge untouched leaves the decision to a person holding both histories.

Security Considerations

The dangerous shape is fixed: the upstream URL is tenant-influenced, the credential is ours, and the process runs in our pod. Every rule the hardened subprocess seam enforces exists for one attack — an arg slice, never a shell string, so a URL is never interpreted; a minimal environment inheriting none of the server's secrets; the credential carried only by env-injected http.extraHeader, never argv (world-readable in /proc) and never the URL (written to logs and reflogs); http/https only with redirects refused, so a source cannot smuggle a file:///ext:: transport or bounce a fetch onto an internal address; an SSRF guard on every tenant-supplied host; pack subprocesses bounded in count and memory; stderr capped and redacted (apps/sync/gitexec.go). The wrong implementation of any one of these hands a tenant our git credential or a request forger inside the cluster network.

Four surfaces

SurfaceReaches this capability asCoverage
RESTsync at its own prefix6 operations
CLIhanzo sync …6 of 6
SDKSyncApi in every published client6 methods
MCPtool sync on https://api.hanzo.ai/v1/mcp6 operations, 2 under the document's own id — ask describe for the rest

Quickstart

export HANZO_API_KEY=sk-...   # console.hanzo.ai → API keys

Then the first call — a read that needs nothing but the key. GET /v1/sync, operation get_sync:

hanzo sync list

Answers 200 with object — ok.

Endpoints

EndpointWhat it does
POST /v1/sync/{id}/runRun reconciles one sync now — the manual re-sync, and the initial import for a link created without run=true.
GET /v1/sync/{id}Get returns one sync by id.
PATCH /v1/sync/{id}Patch updates one sync's mutable policy — direction, trigger and actor — in place.
DELETE /v1/sync/{id}Delete removes one sync and tears down the outbound mirror it derived, answering 204.
GET /v1/syncList returns every sync link the caller's org has, each with its two endpoints, its direction and trigger policy, and the time it last reconciled.
POST /v1/syncCreate declares a sync between two endpoints and returns it.

All Hanzo APIs · Interactive reference

How is this guide?

On this page