Skip to content

Lifecycle — quarantine to serve

Every byte the gateway ever serves passes through three stages, in order. The stages are separated by storage, not by a flag on a row: quarantined content physically lives in a different git repository from published content.

This page is the product. Everything else is detail around it.

Skills Gateway snapshot lifecycle Flowchart of the quarantine-to-serve lifecycle: an upstream commit is ingested into a quarantined snapshot, vetted by the connector chain, decided by a reviewer, and only the ApprovalService publishes it across the trust boundary to the read-only facade; re-vetting can later revoke it, and every transition lands on the append-only audit ledger. QUARANTINE · NEVER SERVED SERVED · READ-ONLY GIT FACADE DISALLOWED OK REJECT APPROVE VIOLATION RE-APPROVE Upstream pushes a commit Ingest POST /api/marketplaces/{name}/ingest Refused fail-closed violation on the ledger Snapshot held refs/snapshots/{sha} Vetting chain one verdict per connector Approve? reviewer inspects evidence Rejected prior approval keeps serving GATE ApprovalService publishes refs/heads/main → {sha} Clients fetch the new SHA git clone /git/{name} Re-vetting sweep chain runs again, later Revoked published refs removed every transition above is appended here LEDGER Append-only audit ledger actor · marketplace · commit SHA, for every step LEGEND publish gate trust boundary asynchronous pinned storage
Skills Gateway snapshot lifecycle Flowchart of the quarantine-to-serve lifecycle: an upstream commit is ingested into a quarantined snapshot, vetted by the connector chain, decided by a reviewer, and only the ApprovalService publishes it across the trust boundary to the read-only facade; re-vetting can later revoke it, and every transition lands on the append-only audit ledger. QUARANTINE · NEVER SERVED SERVED · READ-ONLY GIT FACADE DISALLOWED OK REJECT APPROVE VIOLATION RE-APPROVE Upstream pushes a commit Ingest POST /api/marketplaces/{name}/ingest Refused fail-closed violation on the ledger Snapshot held refs/snapshots/{sha} Vetting chain one verdict per connector Approve? reviewer inspects evidence Rejected prior approval keeps serving GATE ApprovalService publishes refs/heads/main → {sha} Clients fetch the new SHA git clone /git/{name} Re-vetting sweep chain runs again, later Revoked published refs removed every transition above is appended here LEDGER Append-only audit ledger actor · marketplace · commit SHA, for every step LEGEND publish gate trust boundary asynchronous pinned storage

The repositories

The gateway maintains three repositories per marketplace. The three roles are the model; where they physically live depends on the configured storage backend, and nothing above the storage seam can tell which one answered.

Repository Refs
Quarantine One refs/snapshots/{sha} per ingested commit. Never reachable through either endpoint.
Published Exactly one served ref, refs/heads/main, plus the refs/snapshots/{sha} approval pinned alongside it. Created only by approval.
Origin (hosted only) The publisher's own refs/heads/main, written by push and read only by ingestion.

Three directories of bare repositories under skills-gateway.data-dir:

Repository Path
Quarantine {data-dir}/quarantine/{marketplace}.git
Published {data-dir}/published/{marketplace}.git
Origin {data-dir}/hosted/{marketplace}.git

Three key prefixes in one bucket, which keeps the roles separate for the same reason three directories do. Objects are immutable and content-named; the reference map is one small manifest object per repository, rewritten by a conditional write, and that compare-and-swap is the only serialization point in the system.

Key Holds
{prefix}/{role}/{marketplace}/manifest The reference map, the write-ahead sequence it reflects, and the live pack set
{prefix}/{role}/{marketplace}/wal/{seq} One immutable entry per accepted transition
{prefix}/{role}/{marketplace}/objects/pack/… Immutable, content-named packs, durable before anything references them

{role} is quarantine, published or hosted. Local disk holds only a bounded pack cache; nothing in it is authoritative and deleting it is always safe.

The facade resolves repositories through a method that opens only the published path and returns nothing unless refs/heads/main resolves. There is no code path from a facade request to the quarantine tree — nor from a publish request to either quarantine or published: it resolves only the origin path, and only for a marketplace the gateway hosts.

Stage 1 — ingestion

Ingestion has three triggers, chosen per marketplace as its sync mode: an operator's explicit call (on-demand, the default), the gateway's own polling sweep (scheduled), or a signed forge push webhook (webhook). The trigger is the only thing the mode changes — every path below runs identically whatever pulled the commit in. See Syncing from upstream automatically.

It clones the source's default branch with JGit (the gateway never shells out to git), resolves the tip commit, and writes it into quarantine as refs/snapshots/{sha}. The resulting snapshot starts in state held.

For a marketplace the gateway hosts there is no upstream: the source is its own origin repository and the trigger is the publisher's push. Everything from the snapshot pin onward — the manifest check, the vetting chain, the approval gate — is the same code on the same content.

Registration has already constrained what can be ingested at all: the URL scheme must be on the allowlist, and the ref is the gateway's decision, not the registrant's. See Trust boundaries.

Ingesting the same upstream commit twice does not create a second snapshot — the snapshot is keyed by the upstream SHA, and the chain is not re-run for one that already exists.

Once a snapshot is recorded as held, the vetting chain runs against the content just pinned: each connector answers with a verdict, the verdicts are recorded against the snapshot, and they aggregate fail-closed into a clear or blocked outcome. The chain never changes the snapshot's state — a vetted snapshot is still held. What it decides is whether approving it is an ordinary act or one that needs a written reason. See Vetting — the connector chain.

Stage 2 — approval

A held snapshot is inert. It is stored, it is inspectable in the portal, and it is serving nothing.

Approval is gated by the chain: a snapshot whose effective outcome is blocked — its latest run objects and some blocking finding is not covered by an active waiver, which includes a snapshot the chain never ran against — is refused. There is no override flag. The way past a finding is a scoped, justified, expiring waiver for it, and every waiver that let an approval through is written to the ledger.

ApprovalService.approve is the only code in the system that publishes. It fetches the pinned refs/snapshots/{sha} from quarantine into the published repository and force-updates refs/heads/main to that exact SHA. Rejection marks the snapshot rejected and touches no repository at all.

sequenceDiagram
    actor Rev as Reviewer (OIDC session)
    participant API as AdminController
    participant Svc as ApprovalService
    participant Q as Quarantine repo
    participant P as Published repo
    participant L as Audit ledger

    Rev->>API: POST /api/snapshots/{id}/approve
    API->>Svc: approve(id, principal)
    Svc->>Svc: refuse unless state == held (409)
    Svc->>Q: read refs/snapshots/{sha}
    Svc->>P: fetch objects, force-update refs/heads/main → sha
    Svc->>L: admin entry (who, what, when)
    Svc-->>API: snapshot {state: approved, decidedBy, decidedAt}
    API-->>Rev: 200

Both decisions record the deciding principal and timestamp and append to the ledger. Both return 409 Conflict on a snapshot that is already approved or rejected — you cannot re-approve, and you cannot approve something already rejected.

A revoked snapshot is the one exception, and it is decidable through these same two endpoints. That is how content retracted by continuous re-vetting comes back: the same gate, a new reviewer, a new timestamp.

Approval is not permanent

Publication holds until something takes it back, and one thing can: continuous re-vetting re-runs the chain over approved content on a schedule. If a run objects to the content and no active waiver covers the finding, the violation is recorded and announced — and, when the gateway is configured to enforce, the snapshot moves to revoked and its published refs are removed.

That path only ever unpublishes. ApprovalService.approve remains the sole publisher, which is why a revoked snapshot returns to being served only by going back through it.

Stage 3 — serving

The facade serves the published repository read-only over git smart-HTTP at /git/{marketplace}, authenticated by personal access token. Clients see one branch, main, pointing at the approved SHA.

See the git smart-HTTP facade reference for the wire protocol, authentication and the write-rejection guarantee.

One more repository rides on this stage without adding one to the lifecycle: the virtual catalog is derived content — re-synthesized from what every published repository is serving whenever an approval or revocation changes that set. It introduces no new state and no new way in; it is a view over stage 3, never a bypass of stage 2.

Why upstream movement is safe

This is the property the whole design exists to provide.

sequenceDiagram
    participant UP as Upstream
    participant ING as Ingestion
    participant P as Published (refs/heads/main)
    participant CLI as Client

    Note over P: serving abc123 (approved)
    UP->>UP: push def456
    ING->>ING: ingest → snapshot def456, state = held
    Note over P: still serving abc123 — unchanged
    CLI->>P: git fetch
    P-->>CLI: abc123
    Note over ING,P: nothing moves until a reviewer approves def456

A new upstream commit produces a new held snapshot. It does not touch the published ref. Clients keep receiving the previously approved SHA until — and only until — a human approves the new one. If the new snapshot is rejected, the old one keeps serving indefinitely.

That is the difference between a mirror and a gateway: a mirror propagates whatever upstream did, and a gateway requires a decision.