Registering a marketplace¶
Registration tells the gateway which upstream repository it is willing to talk to. It is the first trust boundary. It reads the upstream's list of references, to confirm the gateway can read it and has a default branch to pin, and it fetches no content.
If the skills are your organisation's own and there is no upstream to point at, register a hosted marketplace instead and push to the gateway directly — see Publishing first-party skills. The rest of this page is about upstream marketplaces.
In the portal¶
Marketplaces → Register marketplace. The dialog states the constraint up front: "The gateway ingests the upstream default branch; the ref is not selectable."
| Field | Rule |
|---|---|
| Name | ^[a-z0-9][a-z0-9_-]{0,62}$ — 1 to 63 lowercase letters, digits, - and _; must not start with - or _. |
| Clone URL | A valid URL whose scheme is on the allowlist. |
Over the API¶
$ curl -X POST localhost:8080/api/v1/marketplaces \
-H 'Content-Type: application/json' \
-d '{"name":"acme","url":"https://github.com/acme/skills.git"}'
Responses:
| Status | Cause |
|---|---|
| 201 | Registered. warnings names any non-blocking issue with the registration (see below); empty when there is none. |
| 400 | URL scheme not allowlisted, a credential embedded in the URL, or a ref other than main. |
| 409 | A marketplace with that name already exists. |
| 422 | The name fails the pattern. |
| 502 | The upstream could not be read, or has no default branch. Nothing was registered; see below. |
What is validated, and why¶
The URL scheme must be on skills-gateway.allowed-url-schemes (default
http, https). The check fails closed: a URL that does not parse, or carries
no scheme at all, is rejected rather than passed through. This is what keeps
file: and ssh: out of a component that will later clone the URL.
The ref must be absent or exactly main. You cannot register
release/1.x. Which ref is ingested is the gateway's decision — see
Compatibility and allowlists.
No credential in the URL. A URL with userinfo, such as
https://user:token@host/…, is refused with 400. It would be stored with the
marketplace and shown wherever the URL is. A private upstream is read with an
upstream credential from configuration instead.
The name doubles as a path segment on the facade (/git/acme), so it is
constrained to a character set that cannot traverse directories.
The upstream must be readable. Once every check above has passed, the
gateway lists the upstream's references and resolves its default branch. It uses
the same connection path that ingestion uses to fetch. A request refused by an
earlier check never contacts the upstream. If the upstream cannot be read, the
registration is refused with 502 and nothing is created. The problem says why,
in three properties:
{"status":502,"title":"Upstream not readable",
"detail":"the upstream could not be read, so nothing was registered: repository not found or requires authentication (…). Check the clone URL for typos; if the repository is private, configure an upstream credential for its URL prefix whose token can read it.",
"reason":"repository not found or requires authentication",
"rootCause":"https://github.com/acme/skils.git/info/refs?service=git-upload-pack not found: Not Found",
"nextStep":"Check the clone URL for typos; if the repository is private, configure an upstream credential for its URL prefix whose token can read it."}
reason |
Typical cause |
|---|---|
repository not found or requires authentication |
A typo in the URL, a private repository with no upstream credential, or a credential whose token cannot read it. Forges answer all of these with 401 or 404, so the gateway does not guess which. |
the upstream host could not be resolved |
A typo in the host name, or DNS the gateway cannot use. |
the upstream could not be reached |
The host refused or did not answer: it is down, or a proxy or firewall blocks it. |
the TLS connection to the upstream failed |
The upstream's certificate is not trusted by the gateway's Java trust store. |
the upstream has no default branch |
An empty repository. Push a commit first. |
the upstream fetch failed |
Anything else. rootCause carries the detail. |
An upstream credential's token is never repeated in a response, the log or the ledger. There is no separate "test connection" call: registering is the test. For marketplaces declared in the estate, see Declaring the estate.
Duplicate upstream URLs¶
Tracking one upstream repository under two marketplace names is legitimate —
it is how you test a marketplace before promoting it — so the gateway never
refuses a repeated URL. It does warn: registering a clone URL that matches
another registered upstream marketplace returns 201 with the new marketplace
in warnings, naming every existing marketplace with the same URL, for
example "url already registered as acme".
The comparison normalizes both URLs first — lowercased scheme and host, no
trailing slash, and no .git suffix — so https://github.com/acme/skills,
https://GitHub.com/acme/skills/ and https://github.com/acme/skills.git are
all the same registration as far as the warning is concerned. The repository
path itself keeps its case, since most forges treat it as case-sensitive. The
portal's own client-side check uses the identical rule and asks you to
acknowledge the collision before it will submit the form; the warning in the
response is what reaches every other caller, including a script calling the
API directly.
Ingesting the first snapshot¶
Registration captures nothing. Press Ingest in the marketplace's header, or:
{"id":1,"marketplaceId":1,"sha":"3f9c2ab...","state":"held",
"violation":null,"createdAt":"...","decidedBy":null,"decidedAt":null}
The gateway clones the upstream default branch into quarantine, pins the tip
commit as refs/snapshots/{sha}, and creates a snapshot in state held.
Nothing is served yet.
| Status | Cause |
|---|---|
| 201 | Snapshot captured. A manifest that breaks policy still captures one, in state rejected. |
| 404 | Unknown marketplace. |
| 502 | Ingestion failed. The problem carries reason, rootCause and nextStep, as for registration above. |
Ingesting the same upstream commit twice does not create a second snapshot.
A first ingest can outlast your proxy's timeout
The ingest runs inside the request, and the first one downloads the upstream's whole history. For a repository with a long history that takes minutes, not seconds: one with 1,900 commits and a 370 MiB pack took about 100 seconds, nearly all of it the download (vetting took 6). A later ingest fetches only what changed and returns in under a second.
A proxy in front of the gateway may give up first. An nginx ingress and an AWS Application Load Balancer both default to 60 seconds, and the portal then shows a timeout. The ingest carries on regardless: it finishes in the gateway and is recorded as the last ingest (below), so refresh rather than press Ingest again. To keep the response, raise the proxy's timeout; see Deploying on Kubernetes and Deploying without Kubernetes.
When an ingest fails¶
Every ingest attempt is recorded, whether it was run by hand, by the sync sweep, by a forge webhook or by a push:
- On the marketplace:
lastIngestAt,lastIngestOutcome(succeededorfailed) andlastIngestReasononGET /api/v1/marketplaces. The portal states a failed last ingest in the marketplace's header, with when it happened and why. - On the ledger: an
ingest-failedentry, by whoever or whatever triggered the attempt, with the reason as itsdetail. - In the log: a
WARNline naming the marketplace and the reason.
The next successful ingest replaces the record.
Keeping it current¶
There is no upstream watcher. Something outside the gateway decides when to look for new commits — a cron job, a CI schedule, or a forge webhook calling the ingest endpoint:
Ingestion is always safe to run
A new upstream commit produces a new held snapshot and does not touch what clients receive. Ingesting frequently costs quarantine storage, never availability.
Removing a marketplace¶
A marketplace registered with a typo, one whose upstream is abandoned, and one whose upstream moved all end the same way: remove it, and register again if there is anything to register.
On the marketplace's Settings, choose Remove name…, write the reason, and confirm. The portal returns to Marketplaces. See Remove marketplace.
$ curl -X DELETE -u ... https://skills.corp.example/api/v1/marketplaces/acme \
-H 'Content-Type: application/json' -d '{"reason":"upstream moved"}'
See the reference.
Removal withdraws everything the marketplace serves and stops it being synced or fetched, but keeps its snapshots and their history. It needs an administrator, and it states a reason, which the ledger records.
An upstream that moved. The URL cannot be changed — that would relabel the provenance of content already approved from somewhere else — so remove the marketplace and register the same name against the new URL. Clients keep their clone URL. They see nothing served until a snapshot of the new upstream is approved: the old approvals were decisions about a different upstream and do not carry over.
What carries over, and what does not
- Fetch grants carry over. A token scoped to fetch
acmefetches whichever marketplace is calledacmenow. That is what keeps clients working; revoke a token that should not reach the new one. - Publication grants do not. Removal takes
acmeout of every token's push scope, and the ledger names the tokens. A publisher of the newacmeneeds a token granted on it. - Approver grants do not. Grant again on the new marketplace.
- The declarative estate does, unless you change it. A marketplace still
declared in
skills-gateway.estate.marketplacesis registered again, as a new marketplace, on the next reconciliation. Remove the declaration first; until then the portal does not offer the removal.
The removed marketplace's vetter toggles and chain settings are kept in the database but no longer listed; set them again on the new marketplace.
The ledger tells the two apart: every entry carries marketplaceId beside the
name. See Audit ledger.
Forge metadata¶
Registration captures forge, project, description and last-upstream-update on a best-effort basis. It is displayed on the marketplace's Settings and is informational only; nothing depends on it.