Compatibility and allowlists¶
What the gateway accepts, what it rejects, what it does not support yet, and what its own API promises in return. Everything on this page is enforced in code or in CI, not merely recommended — where a rule is not mechanically enforced, it says so.
URL schemes¶
Applied to every operator-supplied outbound URL. The scheme is lower-cased and
matched against skills-gateway.allowed-url-schemes.
| Scheme | Default | Notes |
|---|---|---|
https |
Allowed | The intended production scheme. |
http |
Allowed | Present for local development and internal forges. Consider narrowing to https alone in production. |
ssh, git, git+ssh |
Rejected | Not in the default allowlist. The gateway clones over HTTP(S) with JGit. |
file |
Rejected | Would let a registration read the gateway's own filesystem. |
ext, and any other scheme |
Rejected | Not in the allowlist. |
| (no scheme, or unparseable) | Rejected | The check fails closed — an unparseable URL is a rejection, not a pass-through. |
Rejection is HTTP 400 with a ProblemDetail naming the allowed schemes.
Widening the allowlist widens a trust boundary
allowed-url-schemes governs what a registration can make the gateway
connect to. Adding file or ext would let an operator-supplied string
reach local resources. Treat changes to it as a security decision.
An empty list is not configurable — it falls back to the default.
Refs¶
| Input | Behaviour |
|---|---|
ref absent |
Accepted. The default branch is ingested. |
ref: main |
Accepted — identical to omitting it. |
| Any other ref | 400. |
Which ref is ingested is the gateway's decision, not the registrant's. This is a
deliberate trust-boundary constraint rather than a missing feature: multi-ref
support will arrive as promotion per (upstream, ref), with each ref advancing
independently through the same approval gate, not by relaxing this check.
Consequently marketplace add <url>#release-1.x against the facade will not
find a ref — only main exists on the published repository.
Names¶
Marketplace names must match ^[a-z0-9][a-z0-9_-]*$: lowercase letters, digits,
hyphen and underscore, not starting with a hyphen or underscore. A violation is
422.
The name is also a path segment on the facade (/git/{name}), which is why the
character set is constrained.
Plugin sources inside a marketplace¶
The current scope accepts local sources only.
| Source type | Status |
|---|---|
| Relative path inside the marketplace repository | Accepted |
github, url, git-subdir, npm, archive, or any external source |
Rejected fail-closed at ingestion, with the reason recorded as the snapshot's violation |
This removes transitive resolution and source rewriting from the current scope entirely: relative sources resolve inside the served snapshot by themselves, so every URL a client dereferences already resolves inside the gateway.
Supporting external sources means mirroring the closure and rewriting the manifest — a designed future capability, not a configuration flag.
Clients¶
The facade is plain read-only git smart-HTTP, so anything that clones works.
| Client | Support |
|---|---|
Claude Code (claude plugin marketplace add) |
The primary target. |
Copilot / Cursor (open Agent Skills SKILL.md repositories) |
Works — the facade serves a git repository. |
CI pipelines, bare git clone |
Works with a PAT. |
Anything attempting git push to /git/** |
Rejected by construction. (A hosted marketplace is published to on /publish/**, a separate endpoint.) |
Enforcement mechanisms¶
Making the gateway the only path is outside the gateway itself:
| Mechanism | Availability |
|---|---|
Claude Code strictKnownMarketplaces (managed settings) |
Available today — a hard client-side allowlist. |
Claude Code extraKnownMarketplaces, enabledPlugins |
Available today — pre-register and force-install. |
| Copilot / Cursor equivalent | None. Egress policy carries the load. |
| Network egress blocking of upstream marketplace hosts | Your network, not the gateway. |
The API contract¶
Everything above is about what the gateway accepts. This is about what its own HTTP surface promises.
Within a major, /api/** only grows. Endpoints, fields and enum values may
be added; nothing a deployed client could depend on is removed, narrowed or
renamed. A breaking change is allowed — it is not free. It moves the path prefix
and ships as a major release, so a client that pinned neither is never
surprised.
| Change | Additive? |
|---|---|
| A new endpoint, a new optional field, a new enum value | Yes. Ships as a minor. |
/api/v2/... added while /api/v1/... remains |
Yes — nothing was taken away. Deprecate first, remove in a later major. |
| Removing or renaming an endpoint or field, making an optional field required, narrowing a type | No. New prefix, and a major. |
The prefix does not exist yet
Endpoints are unversioned today (/api/marketplaces, not
/api/v1/marketplaces). The version segment arrives with the change that
introduces API versioning; until then the promise binds, and the check below
enforces it, against the unversioned paths.
How it is enforced¶
Three mechanisms, and it is worth being clear about which is which.
| Rule | Enforced by |
|---|---|
| The published contract is the contract the gateway serves | A build test. src/main/frontend/openapi.json must equal the document the running application serves, or ./mvnw verify fails naming the command that regenerates it. |
| A breaking change is detected | The API contract workflow diffs every pull request's contract against the one it forked from with oasdiff, and fails on a breaking classification. |
| A breaking change is declared | The same workflow requires the PR title — which becomes the squash commit subject, and so the release version — to carry ! or BREAKING CHANGE. |
| The prefix was moved | Nobody. No check can tell that a break should have been versioned instead; that is review's job. |
Deliberately breaking the contract takes two visible acts: the
⚠️ BREAKING CONTRACT label on the pull request, and the break declared in its
title. Both stay in the record. Keeping the old surface alongside the new one
needs neither — that path is additive, and the gate passes on its own.
The document served at /v3/api-docs declares the release it describes, derived
from the build. The copy published in the repository carries a placeholder there
instead: it is regenerated by hand, and a version that changes on every commit
would make it differ from the build on every commit.
Platform¶
| Component | Requirement |
|---|---|
| Runtime | Java 25 (Temurin); GraalVM CE 25 for native builds |
| Database | PostgreSQL |
| Identity | An OIDC provider supporting authorization-code flow |
| Container | Distroless image built from the native binary |
| Kubernetes | helm/skills-gateway; bring your own PostgreSQL and OIDC provider |