# Hosted Sync

A hosted vault serves a signed catalog over HTTPS. You pair each machine with a short code, admit it from the browser, and from then on it pulls skills and verifies every release against a key it pinned when it enrolled. Signing stays on your machine; Cloud never holds a signing key.

## Pair a machine

Run this on the machine you want to sync. No argument: the slug is something the machine learns, not something you type.

```bash
autovault link
```

The CLI generates an Ed25519 keypair, asks Cloud for a pairing code, and prints it. Codes look like `BKDF-QMTW`: eight characters from a 20-letter alphabet with no vowels, so a code can never spell a word. The CLI opens your browser and polls every 5 seconds.

Confirm the code at `/cloud/pair`. Check the fingerprint on screen against the one in your terminal before confirming. Confirming is the admission in this flow: the machine comes back active with no second dashboard step. Codes expire after 15 minutes; run `autovault link` again to mint another.

## Admit and revoke

Machines are listed under Machines on the cloud dashboard, identified by a fingerprint: the first four and last four characters of the public key. The console never renders a full key.

- **Admit** moves a machine from pending to active. This is for a machine enrolled with `autovault link <slug>`, which lands pending; confirming a pairing code already admitted that machine.
- **Revoke** moves it to revoked, effective on that machine's next request. This needs no active subscription, so a lapsed account can still remove a machine it no longer controls.
- **Deny** refuses a waiting pairing code. It writes a tombstone rather than deleting the record, so the CLI is told it was refused instead of timing out against a 404.

Re-admitting a revoked key from the console is deliberately not possible; pair that machine again from the machine itself.

## What a machine may read

| Status | May read | May not read |
| --- | --- | --- |
| pending | catalog.json, its own device record | bundles |
| active | catalog.json, bundles, its own device record | nothing withheld while the subscription is live |
| revoked | its own device record, so the CLI can report it and exit | catalog.json, bundles |

The catalog being readable at pending is deliberate rather than an oversight: `autovault link` reads it the moment it enrols, to pin `catalog.public_key` before you have admitted anything. Bundles are where skill content lives, so they need active status and a live subscription.

## How a request is signed

Every request under `/v/<slug>/` is signed. No bearer tokens, no cookies.

```text
X-AutoVault-Device      base64url Ed25519 public key
X-AutoVault-Timestamp   whole seconds since epoch
X-AutoVault-Signature   base64url Ed25519 detached signature
```

The signed message is the HTTP method, the request path, and the timestamp, newline separated. Timestamps outside a 300 second window either side are rejected, which bounds replay rather than preventing it.

Enrollment is self-attested: the request that enrols a key is signed by that key and the body repeats it. Admitting is what grants access, not enrolling.

## How content lands in your vault

There is no publish API, and the CLI has no publish command. AutoVault consumes the catalog rather than producing it. The key that signs a release lives on the owner machine and never reaches Cloud, which is what makes the signature worth checking. Signed catalog and bundle objects are placed in your namespace out of band while this is in private beta.

**A newly reserved vault serves nothing.** Its catalog returns 404 until the first release is published to it. Your machine will pair and be admitted normally, then report an empty catalog. That is expected.

## Limits worth knowing

- Key rotation breaks enrolled machines. Each pins `catalog.public_key` at enrollment, so changing it hard-fails all of them.
- Scope is machines, not people. No seats, roles, or invitations.
- One vault per account. Namespaces cannot be renamed, transferred, or deleted.
- Skill drafts submitted from the dashboard are stored and never read back. There is no review queue yet.

## The protocol underneath

Hosted sync is one transport for a smaller thing: a signed catalog of releases, plus a bundle per release. Cloud is a convenient place to put those files, not where their trustworthiness comes from.

A catalog is one JSON document. Every release inside it carries its own detached signature, so the catalog is a manifest rather than an authority, and verification happens on the consuming machine against a key it pinned.

```text
catalog.json
  schema_version  1
  id              vault identifier
  name            display name
  public_key      base64url Ed25519, pinned by each machine
  releases[]      one entry per publishable thing

releases[]
  kind            skill | agent | mcp_server | collection
  name            stable identifier
  version         semver, compared on every check
  channel         stable, beta, or your own
  publisher       who signed it
  policy          auto_apply | user_approve | admin_hold
  capabilities    network, filesystem, tools[]
  breaking        refuse a silent upgrade
  file_hashes[]   path plus sha256, per file
  bundle_hash     sha256 of the bundle
  bundle_path     bundles/<bundle_hash>.json
  signature       ed25519 over the release, domain-separated
```

Two fields carry most of the weight. `policy` decides what may happen without a human: auto_apply updates silently, user_approve waits for a person, admin_hold refuses until somebody releases it. `capabilities` travels inside the signature, so what a skill may reach is part of the signed payload rather than a claim made after installation.

The release signature uses the domain-separation prefix `autovault-sync-release-v1`. That prefix is the trust boundary: a signature minted for another purpose cannot be replayed as a release, and changing the string invalidates every signature ever issued.

`bundle_path` is inside the signature, and the client re-derives it as bundles/<bundle_hash>.json relative to the catalog. Bundles cannot be renamed, moved, or redirected. A downloaded bundle is checked against bundle_hash and against every entry in file_hashes before any byte reaches the vault.

## Upstreams

A vault holds a list of upstreams. Each records where a catalog lives, the public key pinned for it, and this machine's own enrollment. `autovault link` adds one. There are two kinds, and the difference is transport only.

- `https` points at a catalog URL. Requests are device-signed, which is what enrollment and admission are for. AutoVault Cloud is one of these, and so is any HTTPS host you run.
- `file` points at a catalog path: a directory, a network mount, a checkout on disk. No server, no enrollment handshake, no account.

Both run the same verification. A file upstream is not the trusting option; the release signature is checked exactly as it is over HTTPS, because a shared drive is not a trust boundary either.

```bash
autovault link acme-skills                              # a Cloud slug
autovault link https://skills.acme.dev/catalog.json     # your own host
autovault link ./team-catalog                           # a directory
```

The argument decides the kind. Anything that parses as a URL is https. Anything containing a path separator, starting with . or ~ or /, or ending in .json is file. A bare lowercase word is treated as a Cloud slug and expanded against autovault.dev. Slugs are lowercase, and a capitalised one is rejected with the lowercase spelling rather than silently downcased.

## Self-hosting a catalog

A catalog is a static file tree, so anything that serves JSON over HTTPS can host one.

```text
your-catalog/
  catalog.json
  bundles/
    3f1a...c92e.json
    a704...11bd.json
```

Point a machine at it with `autovault link https://your-host/catalog.json`. Self-hosted catalogs carry no device enrollment, so there is no admit step and no console. Access control is whatever the host already does, and the signature is what makes the content trustworthy in either case.

**Self-hosting does not solve the publishing gap. It relocates it.** The CLI consumes catalogs and has no command that produces one: link, add, and sync-profiles all read, and nothing signs a release. The signing primitives exist in the source and are reachable from the test helpers, not from a terminal. Hosting your own catalog today means generating and signing it yourself against the shape above.

So the choice is narrower than it looks. Cloud gives enrollment, per-machine admission, and revocation, with hands-on publishing. Self-hosting gives the same verification with no account and no per-machine gate, with hands-on publishing. Neither has a publish command yet.
