Skip to content

Approving and rejecting snapshots

Approval is the gate. It is the only way content reaches a client, and today it is a human pressing a button.

That is a smaller claim than it sounds. The security property does not come from the sophistication of the vetting; it comes from the fact that nothing is served until someone decides, and that the decision is recorded against an immutable SHA.

Reviewing

A held snapshot gives you four things.

The content itself

The Preview files toggle on the marketplace detail page opens the pinned commit's actual file tree, each file rendered inertly (Markdown without any HTML interpretation, binary files described rather than shown), and — the part that usually decides a successor snapshot — Diff vs served: exactly which paths were added, modified or removed against the commit consumers are currently receiving, with per-file text diffs. A snapshot of a marketplace serving nothing shows every path as new: approving it serves all of it.

The same reads exist on the API (GET /api/snapshots/{id}/files, .../file, .../diff — see the API reference). With role enforcement enabled they require admin or an approver grant for the marketplace, because they return held quarantine content.

Contents

GET /api/snapshots/{id}/content enumerates what the snapshot declares — each plugin with its name, source and description, and the skills found under each. In the portal this is the Show contents toggle on the marketplace detail page.

$ curl localhost:8080/api/snapshots/1/content
{"snapshotId":1,"sha":"3f9c2ab...","state":"held",
 "plugins":[{"name":"acme-tools","description":"...","source":"./plugins/acme-tools",
             "skills":[{"name":"deploy","path":"skills/deploy/SKILL.md"}]}]}

This works on held snapshots by design — reviewing must not require serving.

Provenance

Where it came from and who has touched it:

$ curl localhost:8080/api/snapshots/1/provenance
{"snapshotId":1,"marketplace":"acme","upstreamUrl":"https://github.com/acme/skills.git",
 "upstreamSha":"3f9c2ab...","state":"held","violation":null,
 "ingestedAt":"...","decidedBy":null,"decidedAt":null}

Violations

If ingestion flagged the snapshot — an external plugin source, for instance, which is rejected fail-closed in the current scope — the reason is on the snapshot row and rendered in the portal as destructive text.

Vetting verdicts

Before anything else, read what the vetting chain concluded. The snapshot card on the marketplace detail page — and the approve dialog itself — shows the chain outcome and, per connector, its verdict and every finding with the file and line it came from.

A blocked outcome means at least one connector failed, errored or has not answered, or that the chain never ran for this snapshot at all.

A clear outcome is not a clean bill of health

The built-in connectors are pattern matchers. They catch known credential shapes and known prompt-injection markers; a paraphrased instruction or an unshaped secret goes straight past them. Treat clear as "nothing known matched", and keep reading the content. The limits of each connector are listed under What these connectors can and cannot see next to the verdicts, and in Vetting — the connector chain.

What to look for

The threats that matter here are the ones no scanner catches:

  • Instructions, not just code. SKILL.md, commands and agent definitions are read by an agent as instructions. Look for anything directing the agent toward credentials, network calls, or files outside the skill's stated purpose.
  • Hooks and MCP servers. These execute without the user ever invoking a skill. A plugin that registers them deserves more scrutiny than a markdown-only skill.
  • The diff, on updates. A skill that grows a scripts/ directory has changed category, and that transition is itself worth a closer look.

Deciding

On the Marketplaces page, held snapshots show Approve and Reject buttons in their row. Reject fires immediately. Approve opens a dialog showing the vetting verdicts; if the chain blocked the snapshot, the confirm button stays disabled until every blocking finding is waived from that same dialog.

$ curl -X POST localhost:8080/api/snapshots/1/approve
$ curl -X POST localhost:8080/api/snapshots/1/reject

Neither takes a request body. A snapshot the vetting chain blocked is refused with 409 until each blocking finding is covered by an active waiver; the problem document lists them in uncoveredFindings.

Approve publishes: the pinned refs/snapshots/{sha} is fetched from quarantine into the published repository and refs/heads/main is force-updated to that SHA. From the next client fetch onward, this is what the marketplace serves.

Reject marks the snapshot rejected and touches no repository. Whatever was already approved keeps serving.

Status Cause
200 Decided.
404 Unknown snapshot.
409 The snapshot is neither held nor revoked, its effective vetting outcome is blocked (see uncoveredFindings), a policy rule denied it, it has not yet cleared the minimum release age, or an enforcing four-eyes rule refused it (see conflicts).

An approved snapshot cannot be re-decided

Approving is not reversible by hand. A snapshot that is approved or rejected returns 409 from both endpoints, and there is no un-approve.

To move a marketplace back to earlier content, re-ingest the desired upstream commit and approve that snapshot. Approving an older one is not a rollback mechanism.

Separation of duties

An approval is worth what the independence of the approver is worth. The gateway therefore checks, at approval time, whether the reviewer is on the snapshot's supply side — whether they, personally, are any of:

Conflict You
registered-by registered the marketplace this snapshot came from
ingested-by triggered the ingestion that pinned it
waiver-author wrote a waiver this approval relies on

The third is not a technicality. Waiving a finding and then approving past your own waiver is one decision wearing two hats, and it is the route around the other two: a reviewer refused for having ingested a snapshot could otherwise accept the objection on a fresh copy and approve that instead.

The automated sync triggers never conflict. A snapshot the polling sweep or a forge webhook brought in records scheduler or webhook as its ingestion actor, and neither is a person whose independence is being protected — so an estate on automatic sync is approvable by anyone the role model allows. Nor does an unrecorded actor conflict: marketplaces and snapshots that predate this release carry none, so the rule tightens only from here on.

What a conflict does is a deployment decision, skills-gateway.approval.four-eyes.mode:

  • warn, the default. The approval proceeds and the conflict is written to the audit ledger. This is what keeps a single-administrator deployment — a first evaluation, a small team — working: there is nobody else to ask, and refusing would only mean nothing could ever be published. The record is what makes that visible rather than silent.
  • enforce. The approval is refused with 409, and the snapshot stays held with nothing published. Somebody else has to decide.

There is deliberately no way to switch detection off. A conflict reaches the ledger in both modes as a four-eyes-conflict entry naming the acting identity, the snapshot, the mode, whether the approval proceeded, and each conflicting act — which is also how you size up enforce before turning it on: run warn, then read how many approvals would have been refused, and by whom.

Neither mode gates Reject. Refusing suspicious content quickly must never wait for a second pair of eyes.

You can see where you stand before deciding. The portal's approve dialog says which acts conflict — as a warning under warn, and with the confirm button shut under enforce — and the API answers the same question without deciding anything:

$ curl localhost:8080/api/snapshots/1/four-eyes
{"mode":"ENFORCE","refused":true,
 "conflicts":[{"role":"registered-by","principal":"dana","waiverId":null},
              {"role":"ingested-by","principal":"dana","waiverId":null}]}

A refused approval answers with the same list in its problem document, under conflicts, alongside the configKey that imposed it.

enforce needs a second approver per marketplace

Under enforce, a marketplace whose only approver also registered it or ingests its content has nobody left who may publish it. Give every marketplace that needs deciding a second identity with approval rights — a second admin, or an approver scoped to it under delegated administration — before switching.

Waiting out the minimum release age

If your deployment configures skills-gateway.vetting.minimum-release-age, a snapshot cannot be approved until the gateway has been holding its commit for that long. The portal shows the approve control disabled and reading Eligible in 2d 4h; the API answers 409 with the setting, the current age and the time remaining, and GET /api/snapshots/{id}/release-age answers the same question without attempting a decision.

The window exists for a threat no scanner covers: a compromised release is usually noticed by the wider world — often by the project's own community — within hours of being pushed, and it is frequently pulled again just as fast. Adopting a commit the moment it lands forfeits that detection entirely.

Three things about it are worth knowing before it surprises you.

  • Nothing has to happen for the wait to end. The age is compared on each approval request, so the snapshot becomes approvable by itself. There is no queue to re-run and no state to clear.
  • The clock is this gateway's first sighting of the commit, not the commit's date, and re-ingesting the same commit does not restart it. A backdated or re-pushed commit gets no credit.
  • There is no override. The wait applies to a newly registered marketplace's very first snapshot too. Shipping something before the window elapses means changing the configuration, which is a deployment someone reviews — deliberately a heavier act than clicking past a dialog.

Both outcomes are on the audit ledger: an approval that went through records how long the commit had been held (snapshot-approved, detail ingestion-age=…), and one the window turned away is its own entry (snapshot-approval-refused). What the wait was worth is therefore answerable from the ledger alone.

Reviewing is unaffected: the contents, the provenance and the vetting verdicts are all readable during the wait, so the waivers a blocked snapshot needs can be recorded while the window runs down rather than after it.

Re-approving a revoked snapshot

A snapshot that continuous re-vetting revoked is decidable again, and the route back is this same Approve — deliberately, because a retraction the gateway made without a person has to be answerable by one.

Nothing about the gate is relaxed for it. The effective vetting outcome is evaluated exactly as for a held snapshot, so the finding that caused the violation has to be waived (or fixed by re-ingesting a corrected upstream commit) before the approval succeeds. What the decision records is fresh: a new reviewer, a new timestamp, and the revocation marks cleared. What the snapshot was revoked for stays in the audit ledger.

Rejecting it instead is the terminal answer, and the right one when the finding is not something anyone intends to accept.

When an approval fails

An approval publishes by moving the marketplace's served references onto the snapshot. That is a single all-or-nothing transition, and it can be refused — by a competing writer holding the reference, by the storage underneath it, or, on the object-store backend with more than one replica, by an ordinary lost compare-and-swap.

A refused publication fails the approval. Nothing is published, the snapshot stays exactly as it was — held, or revoked, with its revocation intact — the audit ledger records no approval, and no lifecycle event is emitted. Retry the approval; there is nothing to clean up first.

Do not treat a failed approval as a published one

Earlier versions reported such an approval as successful: the snapshot was recorded as approved, the ledger said so, and subscribers were notified, while the facade went on serving the previous tip. If you are looking at an estate where the database and the facade disagree about what is served, that is the cause, and re-approving the affected snapshot is the fix.

Rejection is not deletion

The rejected snapshot stays as evidence — it is the record that someone looked at this exact upstream commit and said no.

Who may approve

Two independent questions, answered in this order.

May this principal approve here at all? That is delegated administration: with role enforcement enabled, an admin or an approver scoped to the marketplace. With it disabled — the default — any authenticated session can register, ingest, approve and reject, and access to the portal is itself the reviewer privilege.

May this principal approve this snapshot? That is the four-eyes rule above, and it is a different question: it is about what the reviewer already did to the content in front of them, not about what they are permitted to do in general. A principal can hold every role there is and still be the wrong person to approve one particular snapshot.

After approval

The content is live. Verify it and point clients at it — Consuming approved skills.

If the chain objected and you accepted the risk, see Waiving a vetting finding for what the acceptance covers and when it lapses.