HTTP API reference¶
ArtiGate is a single binary with two roles that never share routes: artigate low (the collector/exporter — every /admin/*/collect, re-export, watch, and the low dashboard) and artigate high (a read-only mirror that serves imported bundle contents). This page documents every HTTP endpoint on both sides, with the exact request-body fields and response shapes taken from the Go structs in cmd/artigate.
Two roles, two route tables
The low side is an exporter only — it has no package pull-through. Anything that is not /admin/*, /healthz, /readyz, /metrics, /, or /ui* returns 404. Only the high side serves package contents to clients. See Architecture for the full model and Low side / High side for operations.
Conventions¶
- Bundle IDs are
<stream>-bundle-%06d, e.g.go-bundle-000042,python-bundle-000007. Each bundle is three files:<id>.tar.gz,<id>.manifest.json,<id>.manifest.json.sig. - Streams are the twenty-three ecosystems, each independently sequenced:
go,python,maven,apt,rpm,hf,containers,npm,crates,terraform,helm,nuget,apk,conda,rubygems,composer,vsx,galaxy,cran,snap,git,osv,uploads. Thegostream keeps the legacy single-stream numbering. - Error codes: collect and re-export errors are
400; watch validation400, watch store failures500; high-side import/status failures500; UIdetailnot-found404;reposfor a wrong ecosystem400. Non-read methods on serving/UI routes return405. - Auth: only the low dashboard can require login (
ARTIGATE_LOW_AUTH). The high side is never authenticated. See Security & trust and TLS / HTTPS.
LOW side¶
LowServer.ServeHTTP tries serveLowAdmin, then serveLowUI, else 404 not found.
| Route | Method | Purpose |
|---|---|---|
/admin/{eco}/collect |
POST | Collect + export a bundle for one ecosystem |
/admin/reexport |
POST | Re-transmit already-archived bundles |
/admin/watches |
GET / POST | List / create scheduled pulls |
/admin/watches/update |
POST | Edit a watch's label, interval, or spec in place |
/admin/watches/{run,enable,disable,delete} |
POST | Act on a watch by id |
/admin/bundles |
GET | Per-stream export status |
/healthz |
any | Liveness (ok\n) |
/readyz |
GET/HEAD | Readiness — 200 ok / 503 with the failing checks |
/metrics |
GET/HEAD | Prometheus telemetry |
/, /ui, /ui/ |
GET/HEAD | Dashboard HTML |
/ui/api/status |
GET/HEAD | Same payload as /admin/bundles |
Collect endpoints¶
Every ecosystem exposes POST /admin/{eco}/collect. The dispatch is POST-only; a non-POST request falls through to UI routing. Without ?stream=1 the handler returns a single buffered JSON ExportResult on success, or http.Error at 400 on failure. An empty body is JSON-valid but every collector then rejects it for missing required fields.
Every collect request additionally accepts one shared field:
| Field | Type | Notes |
|---|---|---|
force |
bool | omitempty; true bypasses the export-dedup index for this collect — everything is downloaded and packed even when already forwarded, producing a full self-contained bundle (disaster recovery / rebuilding a high side from scratch) |
Shared response — ExportResult¶
{
"stream": "python",
"sequence": 7,
"exported_modules": 12,
"bundle_id": "python-bundle-000007",
"skipped": false,
"message": "",
"skipped_modules": [
{ "module": "github.com/foo/bar", "version": "v1.2.3", "error": "..." }
]
}
| Field | Type | Notes |
|---|---|---|
stream |
string | omitempty |
sequence |
int64 | omitempty; the sequence this bundle consumed |
exported_modules |
int | always emitted; a unit count (Go modules, Python projects, container repos, Maven artifacts…) |
bundle_id |
string | omitempty |
skipped |
bool | omitempty; true when export dedup found every resolved file already forwarded on this stream — no bundle written, no sequence consumed |
prior_files |
int | omitempty; the count of manifest entries that reference already-forwarded content (a delta bundle): listed and verified on import, but neither re-downloaded where the upstream declares hashes nor packed into the archive |
message |
string | omitempty; "no new content since the last export" on a dedup skip, or "re-exported from archive" on a replay |
skipped_modules |
[]FailedModule |
omitempty; per-item fetch failures that were skipped so the rest of the batch still exports. FailedModule = {module, version, error} |
diode_error |
string | omitempty; set when the upload to the HTTP diode endpoint failed. The bundle itself is committed, archived, and still staged — a "re-transmit me" signal, not a lost export |
skipped:true consumes no sequence
A dedup skip writes no bundle and burns no sequence number. The high side must not wait on a sequence that was never produced.
Which collectors populate skipped_modules
Go, containers, AI models, Python (failed sdist opt-ins and source-only packages the pip run cannot mirror), NPM (git-URL / otherwise-unfetchable packages), crates, Terraform, Helm, NuGet, apk, conda, RubyGems, Composer, VS Code, Galaxy, CRAN, and OSV report per-item failures here. APT, RPM, Maven, git, and uploads never populate the field — they either fully succeed or return a single top-level error. If all items fail, the whole request errors at 400 (e.g. Go "no modules could be fetched: …", containers "no images could be fetched: …") rather than writing an empty bundle.
Go — POST /admin/go/collect¶
GoCollectRequest. Body limit 8 MiB. See Go modules.
{
"modules": ["golang.org/x/text@v0.14.0", "rsc.io/quote", "example.com/m@latest"],
"resolve_deps": false,
"go_mod": "",
"go_sum": ""
}
| Field | Type | Notes |
|---|---|---|
modules |
[]string |
Each module@version, or bare module / module@latest (resolved to a concrete version via go list -m -json) |
resolve_deps |
bool | When true, expands the transitive module graph (go mod download -json all) |
go_mod |
string | A project's own go.mod content; when set, modules and resolve_deps are ignored |
go_sum |
string | Optional, paired with go_mod |
auth |
*object |
optional one-time login for a private module host, injected into the go/git subprocesses: {"host","username","password"}. host is required unless every listed module shares one host. Never stored, rejected inside watch specs — standing credentials go in ARTIGATE_GO_AUTH (details) |
POST /admin/go/collect HTTP/1.1
Content-Type: application/json
{"modules":["rsc.io/quote@v1.5.2"],"resolve_deps":true}
Python — POST /admin/python/collect¶
PythonCollectRequest. Body limit 1 MiB. See Python (PyPI).
{
"requirements": ["requests==2.31.0", "flask>=3"],
"sdists": ["some-source-only-pkg", "another==1.2.3"],
"target": {
"python_version": "3.11",
"implementation": "cp",
"abi": "cp311",
"platforms": ["manylinux2014_x86_64"],
"only_binary": true
}
}
| Field | Type | Notes |
|---|---|---|
requirements |
[]string |
Passed to python -m pip download (wheels only). At least one of requirements/sdists must be non-empty — else error "no python requirements provided" |
sdists |
[]string |
omitempty; per-package sdist opt-in: name or name==1.2.3. Resolved via the index JSON API (--pypi-json), never through pip; requires an API-declared SHA-256. Failures are skipped into skipped_modules |
target |
*PythonTarget |
Optional cross-target selector |
target.python_version |
string | omitempty |
target.implementation |
string | omitempty, e.g. cp |
target.abi |
string | omitempty, e.g. cp311 |
target.platforms |
[]string |
e.g. manylinux2014_x86_64 |
target.only_binary |
bool | Compatibility field: omit it or set true; false is rejected because wheels-only pip collection is mandatory |
pip runs wheels-only, always
Every pip invocation adds --only-binary=:all:, with or without a target selector — pip never invokes a source-package build backend in the signing process. Opted-in sdists bypass pip entirely (plain verified download), so no build hook runs for them either; clients build those at install time.
Maven — POST /admin/maven/collect¶
MavenCollectRequest. Body limit 8 MiB. See Java (Maven).
| Field | Type | Notes |
|---|---|---|
coordinates |
[]string |
Each groupId:artifactId:version |
pom_xml |
string | A full pom.xml; when set, coordinates is ignored. Only its dependency information (parent, properties, dependencies, dependencyManagement) is extracted into a sanitized project for mvn -B dependency:go-offline; <build>, <profiles>, <repositories>, and unknown elements are rejected |
Both empty → error "no maven coordinates or pom_xml provided". SNAPSHOT and dynamic/range versions are rejected.
NPM — POST /admin/npm/collect¶
NpmCollectRequest. Body limit 8 MiB. See NPM.
{
"packages": ["lodash@4.17.21", "react@^18.2", "@scope/pkg@latest"],
"package_json": "",
"package_lock": ""
}
| Field | Type | Notes |
|---|---|---|
packages |
[]string |
npm install specs; the full dependency graph is resolved and bundled |
package_json |
string | A project's own package.json; when set, packages is ignored |
package_lock |
string | Optional; requires package_json (else error "package_lock requires package_json"); pins the exact resolved graph |
Both JSON blobs must be valid JSON. Packages resolving outside the registry (e.g. git URLs) or whose tarball fails are skipped/reported.
APT — POST /admin/apt/collect¶
AptCollectRequest. Body limit 1 MiB. See APT (Debian/Ubuntu).
{
"name": "debian",
"uri": "http://deb.debian.org/debian",
"suites": ["bookworm", "bookworm-updates"],
"components": ["main", "contrib"],
"architectures": ["amd64"],
"signed_by": "/etc/apt/keyrings/debian.gpg",
"source_list": "",
"newest_only": true
}
| Field | Type | Notes |
|---|---|---|
name |
string | Mirror name (URL segment on the high side) |
uri |
string | Archive base URI |
suites |
[]string |
One or more, e.g. ["bookworm","bookworm-updates"]; all share the mirror's pool |
components |
[]string |
e.g. ["main","contrib"]; applies to every suite |
architectures |
[]string |
e.g. ["amd64"]; applies to every suite |
signed_by |
string | Local keyring path used to verify each suite's Release |
source_list |
string | A deb822 stanza; an alternative to the explicit fields above |
auth |
*object |
optional one-time HTTP Basic login for a private mirror: {"host","username","password"}; host may be omitted when every source shares one host. Never stored, rejected inside watch specs — standing credentials go in ARTIGATE_UPSTREAM_AUTH |
newest_only |
*bool |
Defaults true when absent; false mirrors every version in the index |
RPM — POST /admin/rpm/collect¶
RpmCollectRequest. Body limit 1 MiB. See RPM (RHEL/Fedora).
{
"name": "baseos",
"base_url": "https://packages.microsoft.com/rhel/9/prod/",
"gpg_key": "/etc/pki/rpm-gpg/RPM-GPG-KEY",
"repo_file": "",
"newest_only": true,
"architectures": ["x86_64", "noarch"]
}
| Field | Type | Notes |
|---|---|---|
name |
string | Repo name (URL segment on the high side); defaults to a slug of base_url. Only honored with the explicit fields — repo_file mirrors are always named by their baseurl slug (section headers are structural only) |
base_url |
string | Repository base URL |
gpg_key |
string | Local keyring path for gpgv (optional) |
repo_file |
string | A full .repo file (one or more [sections]); an alternative to name+base_url |
auth |
*object |
optional one-time HTTP Basic login for a private repo: {"host","username","password"}; host may be omitted when every section shares one host. Never stored, rejected inside watch specs — standing credentials go in ARTIGATE_UPSTREAM_AUTH |
newest_only |
*bool |
Defaults true when absent; keeps only the highest EVR per package |
architectures |
[]string |
Defaults to ["x86_64","noarch"] when absent; only packages of these architectures are mirrored (applies to every repo in the collect) |
Containers — POST /admin/containers/collect¶
ContainerCollectRequest. Body limit 1 MiB. See Container images (OCI).
| Field | Type | Notes |
|---|---|---|
images |
[]string |
docker-style refs (tag or @sha256: digest) |
auth |
*object |
optional one-time login for a private registry: {"registry","username","password"}; registry may be omitted when the pull uses a single registry. Never stored, and rejected inside watch specs — standing credentials go in ARTIGATE_CONTAINER_AUTH (details) |
linux/amd64 only
Only the linux/amd64 platform is mirrored. Unfetchable images are skipped and reported in skipped_modules.
AI models — POST /admin/hf/collect¶
HFCollectRequest. Body limit 1 MiB. See AI models (Hugging Face).
{
"models": ["hf.co/unsloth/gpt-oss-20b-GGUF:Q4_0"],
"repos": ["openai/gpt-oss-20b", "org/model@main"],
"repo_exclude": ["original", "metal"]
}
| Field | Type | Notes |
|---|---|---|
models |
[]string |
GGUF variant refs; the tag is a quantization resolved by the Hub (latest = the repo's default); the hf.co/ prefix is optional |
repos |
[]string |
full repository snapshots, pinned to a commit at collect time; @branch / @commit optional (default main) |
repo_exclude |
[]string |
skip repository paths: a bare directory name excludes the subtree, else path.Match against the full path |
At least one of models/repos is required. Gated models need ARTIGATE_HF_TOKEN on the low side; unfetchable references are skipped and reported in skipped_modules.
Rust crates — POST /admin/crates/collect¶
CratesCollectRequest. Body limit 1 MiB. See Rust crates.
| Field | Type | Notes |
|---|---|---|
crates |
[]string |
Crate specs: bare serde for the newest release, serde@1.0.203 to pin |
resolve_deps |
*bool |
Defaults true when absent — the transitive graph (normal + build dependencies, never dev) is resolved against the sparse index and bundled too; false mirrors only the listed crates |
include_optional |
bool | Additionally follow optional dependencies |
Every .crate is verified against the sparse-index cksum; unresolvable/unfetchable crates are skipped and reported in skipped_modules.
Terraform / OpenTofu — POST /admin/terraform/collect¶
TerraformCollectRequest. Body limit 1 MiB. See Terraform / OpenTofu.
{
"providers": ["hashicorp/aws@5.50.0"],
"modules": ["terraform-aws-modules/vpc/aws@5.8.1"],
"platforms": ["linux_amd64"],
"registry": ""
}
| Field | Type | Notes |
|---|---|---|
providers |
[]string |
namespace/type, optionally @version (bare = newest release) |
modules |
[]string |
namespace/name/system, optionally @version |
platforms |
[]string |
os_arch names the provider zips are mirrored for; defaults to ["linux_amd64"] |
registry |
string | Upstream registry override for this collect (e.g. https://registry.opentofu.org); empty uses --terraform-registry or https://registry.terraform.io |
At least one provider or module is required. Provider zips are verified against the registry shasum and mirrored with the upstream SHA256SUMS/.sig/signing keys; failed items are skipped and reported in skipped_modules.
Helm — POST /admin/helm/collect¶
HelmCollectRequest. Body limit 1 MiB. See Helm charts.
{
"name": "bitnami",
"url": "https://charts.bitnami.com/bitnami",
"charts": ["nginx@21.1.0", "redis"]
}
| Field | Type | Notes |
|---|---|---|
name |
string | Optional mirror name (/helm/<name> on the high side); defaults to a slug of the URL |
url |
string | Required — the upstream chart repository (the URL helm repo add would use) |
charts |
[]string |
Chart specs: bare nginx for the newest version, nginx@21.1.0 to pin |
Archives are verified against the upstream index digest when one is declared; failed charts are skipped and reported in skipped_modules.
NuGet — POST /admin/nuget/collect¶
NugetCollectRequest. Body limit 1 MiB. See NuGet.
| Field | Type | Notes |
|---|---|---|
packages |
[]string |
Package specs: bare Serilog for the newest stable release, Newtonsoft.Json@13.0.3 to pin |
resolve_deps |
*bool |
Defaults true when absent — nuspec dependencies are resolved to the lowest applicable version per range (NuGet restore behavior) and bundled too |
The flat container publishes no digests, so downloads are TLS-trusted and validated against each package's embedded nuspec; failed packages are skipped and reported in skipped_modules.
Alpine (apk) — POST /admin/apk/collect¶
ApkCollectRequest. Body limit 1 MiB. See Alpine (apk).
{
"name": "alpine",
"uri": "https://dl-cdn.alpinelinux.org/alpine",
"branches": ["v3.22"],
"repositories": ["main", "community"],
"architectures": ["x86_64"],
"repositories_file": "",
"newest_only": true
}
| Field | Type | Notes |
|---|---|---|
name |
string | Mirror name (/apk/<name> on the high side); defaults to a slug of the URI |
uri |
string | Mirror base URL (the part before <branch>/<repo>) |
branches |
[]string |
Branches to mirror, e.g. ["v3.22"]; required with uri |
repositories |
[]string |
Defaults to ["main"] |
architectures |
[]string |
Defaults to ["x86_64"] |
repositories_file |
string | A pasted /etc/apk/repositories file — an alternative to uri+branches+repositories; every line must name the same mirror base |
auth |
*object |
optional one-time HTTP Basic login for a private mirror: {"username","password"} (a collect uses a single mirror, so no host is needed). Never stored, rejected inside watch specs — standing credentials go in ARTIGATE_UPSTREAM_AUTH |
newest_only |
*bool |
Defaults true when absent; keeps only each package's highest version |
Each .apk is verified against the APKINDEX-declared size and Q1 control checksum; per-package failures are skipped and reported in skipped_modules.
Conda — POST /admin/conda/collect¶
CondaCollectRequest. Body limit 1 MiB. See Conda channels.
{
"channel": "conda-forge",
"name": "",
"subdirs": ["linux-64"],
"packages": ["numpy", "scipy==1.13.1", "pandas>=2.0,<3"],
"no_deps": false
}
| Field | Type | Notes |
|---|---|---|
channel |
string | Required. A bare channel name resolved under the channel base (--conda-channel-base, default https://conda.anaconda.org) or a full http(s) channel URL |
name |
string | Mirror name (/conda/<name> on the high side); defaults to the bare channel name, else a slug of the URL |
subdirs |
[]string |
Platform subdirs; noarch is always searched too. Empty means just noarch |
packages |
[]string |
Required. Specs: numpy, numpy==1.26.4, numpy=1.26, numpy=1.26.*, pandas>=2.0,<3 — a version always attaches to a package name |
no_deps |
bool | Skip the depends closure |
auth |
*object |
optional one-time HTTP Basic login for a private channel: {"username","password"}. Never stored, rejected inside watch specs — standing credentials go in ARTIGATE_UPSTREAM_AUTH |
Each package file is verified against its repodata-declared SHA-256 (an entry without one is refused); per-package failures are skipped and reported in skipped_modules.
RubyGems — POST /admin/rubygems/collect¶
RubyGemsCollectRequest. Body limit 1 MiB. See RubyGems.
| Field | Type | Notes |
|---|---|---|
gems |
[]string |
Required. name for the newest release, name@1.2.3 to pin |
platforms |
[]string |
Optional platform variants fetched beside the pure-Ruby gem when upstream publishes them |
no_deps |
bool | Skip the runtime dependency closure |
Every .gem is verified against its compact-index-declared SHA-256; the verbatim /info lines travel in the manifest. Per-gem failures are reported in skipped_modules.
Composer — POST /admin/composer/collect¶
ComposerCollectRequest. Body limit 1 MiB. See PHP Composer.
| Field | Type | Notes |
|---|---|---|
packages |
[]string |
Required. vendor/project for the newest stable release, vendor/project:2.0.2 to pin |
no_deps |
bool | Skip the require closure |
Dist zips are TLS-trusted (Composer metadata declares no usable digest) and hash-locked into the bundle; each release's version object travels with dist/source stripped. Per-package failures (including unsupported constraint forms) are reported in skipped_modules.
VS Code extensions — POST /admin/vsx/collect¶
VSXCollectRequest. Body limit 1 MiB. See VS Code extensions.
| Field | Type | Notes |
|---|---|---|
extensions |
[]string |
Required. publisher.name for the newest version, publisher.name@1.14.0 to pin |
no_deps |
bool | Skip dependencies and extension packs (which otherwise ride along at newest) |
The .vsix is verified against the registry-published SHA-256 when one exists, else TLS-trusted; per-extension failures are reported in skipped_modules.
Ansible Galaxy — POST /admin/galaxy/collect¶
GalaxyCollectRequest. Body limit 1 MiB. See Ansible Galaxy.
| Field | Type | Notes |
|---|---|---|
collections |
[]string |
Required. namespace.name for the newest version, namespace.name@8.5.0 to pin (full three-part semver) |
no_deps |
bool | Skip collection dependencies |
Each artifact is verified against the v3-API-declared SHA-256 and size; per-collection failures are reported in skipped_modules.
CRAN — POST /admin/cran/collect¶
CRANCollectRequest. Body limit 1 MiB. See R packages (CRAN).
| Field | Type | Notes |
|---|---|---|
packages |
[]string |
Required. name for the mirror's current version, name@1.15.4 to pin (superseded pins fetch from Archive/) |
Tarballs are verified against the index-declared MD5 when present (Archive downloads are TLS-trusted); dependency resolution follows Depends/Imports/LinkingTo at current versions, skipping base packages. Unresolvable items are reported in skipped_modules.
Snap packages — POST /admin/snap/collect¶
SnapCollectRequest. Body limit 1 MiB. See Snap packages.
| Field | Type | Notes |
|---|---|---|
snaps |
[]string |
Required. name for the stable channel, name@channel to pick another (hello@edge, blender@4.1/stable) |
architecture |
string | Store architecture (default amd64; one per collect) |
no_bases |
bool | Skip the base snaps the listed snaps declare (which otherwise ride along from stable) |
Each .snap is verified during download against the store-declared SHA3-384 and size, and mirrored together with its store assertion chain (.assert), cross-checked against the channel entry before staging. Per-snap failures are reported in skipped_modules.
Git — POST /admin/git/collect¶
GitCollectRequest. Body limit 1 MiB. See Git repositories.
| Field | Type | Notes |
|---|---|---|
url |
string | Upstream clone URL (http(s) smart protocol). A URL embedding user:pass@ is rejected — use auth or ARTIGATE_UPSTREAM_AUTH |
name |
string | Mirror name (git/<name>.git on the high side); defaults to a slug of the URL |
refs |
[]string |
Optional full ref names to mirror; default is every branch and tag |
auth |
*object |
optional one-time HTTP Basic login for a private repository (e.g. a GitHub/GitLab PAT as the password): {"username","password"}. Never stored, rejected inside watch specs — standing credentials go in ARTIGATE_UPSTREAM_AUTH |
OSV — POST /admin/osv/collect¶
OsvCollectRequest. Body limit 1 MiB. See OSV advisories.
| Field | Type | Notes |
|---|---|---|
ecosystems |
[]string |
Required. OSV ecosystem names exactly as osv.dev spells them (npm, PyPI, crates.io, Debian:12, …); each name's current all.zip database is fetched |
The OSV bucket publishes no digests, so downloads are TLS-trusted, then checked to be readable advisory archives before signing; failed ecosystems are skipped and reported in skipped_modules. An unchanged database dedups to a no-op export.
Uploads — POST /admin/uploads/collect¶
multipart/form-data, not JSON — the one collect endpoint that differs. See Uploads.
curl -fsS -X POST -F "folder=tools" -F "file=@installer.run" \
http://low:8080/admin/uploads/collect
| Form field | Notes |
|---|---|
folder |
Required. Target folder (one path segment, ≤ 128 chars, no leading ., no separators or control chars). Field value capped at 4 KiB |
file |
One or more file parts (up to 10,000). Names are reduced to their base name and validated like folder; a duplicate name in one upload is rejected. Parts stream to disk while being hashed — no staging cap, but each file must fit one bundle under the per-bundle transport limit (64 GiB by default) |
The shared force field does not apply: uploads always ship in full (the export-dedup index is deliberately never consulted), so prior_files is always 0. ?dry_run=1 works; ?stream=1 is recommended for multi-gigabyte files.
Streaming variant — ?stream=1¶
Append ?stream=1 to any /admin/{eco}/collect to receive live progress as NDJSON (one JSON object per line). This is what the dashboard's "Collect & export" modal uses.
- Response headers:
Content-Type: application/x-ndjson,Cache-Control: no-store,X-Content-Type-Options: nosniff. HTTP 200 is sent immediately, then lines are flushed as they occur. - The request body is buffered up to 16 MiB before headers go out (the collect goroutine re-reads it), a cap that sits above each handler's own body limit.
- Exactly one terminal
doneorerrorevent follows zero or moreloganddlevents. - Aborting the request (the dashboard's Stop button) cancels the running collect server-side: downloads, spawned tools, and bundle packing all stop, and no sequence number is burned.
Event shapes:
{"type":"dl","name":"model-00001-of-00002.safetensors","done":5242880,"total":12582912000,"bps":44040192}
{"type":"done","result":{"stream":"go","sequence":42,"exported_modules":12,"bundle_id":"go-bundle-000042"}}
dl events sample an in-flight file transfer at most every 500 ms — direct HTTP downloads (containers, AI models, APT, RPM), bundle packing ("name":"packing hf-bundle-000001.tar.gz", measured on the uncompressed input side), and uploads to the HTTP diode endpoint. total is 0 when the size is unknown; transfers finishing inside the first interval emit nothing. They are ephemeral: when the client reads slowly, samples are dropped rather than queued (log lines are never dropped). The dashboard renders them as the per-file progress bar with rate and ETA.
Progress lines are human-readable, e.g. Resolving the Go module graph…, Resolved 12 module(s); fetching…, → [3/12] rsc.io/quote@v1.5.2, ✗ example.com/x@v0.1.0: not found, Packing 40 file(s) into a signed bundle…, Running mvn dependency:go-offline…, Resolving 2 image reference(s) (linux/amd64)….
Consume it with curl:
curl -N -X POST 'http://localhost:8080/admin/go/collect?stream=1' \
-H 'Content-Type: application/json' \
-d '{"modules":["rsc.io/quote@v1.5.2"],"resolve_deps":true}'
Note
If the ResponseWriter cannot flush (exotic wrappers only), the server falls back to a single buffered ExportResult.
Re-export — POST /admin/reexport¶
Re-transmits already-produced bundles by replaying the exact archived signed bytes from <root>/bundles back into the export dir — no re-collect, no re-sign. Works for any ecosystem. Errors return 400.
The spec can be given three ways (stream defaults to "go" when unspecified):
# 1. Query string
curl -X POST 'http://localhost:8080/admin/reexport?stream=go&sequences=42,45-47'
# 2. JSON body (ReexportHTTPBody)
curl -X POST http://localhost:8080/admin/reexport \
-H 'Content-Type: application/json' \
-d '{"stream":"go","sequences":"42,45-47"}'
# 3. Raw text body
curl -X POST http://localhost:8080/admin/reexport --data-binary '42,45-47'
sequences is a comma list of single numbers and inclusive start-end ranges; expansion is capped at 10000 sequences per request. A missing spec errors with "missing sequence range; use ?stream=go&sequences=42,45-47 or JSON ...".
Response — ReexportResult:
{
"stream": "go",
"requested_ranges": ["42", "45-47"],
"sequences": [42, 45, 46, 47],
"reexported": [
{ "stream": "go", "sequence": 42, "exported_modules": 12,
"bundle_id": "go-bundle-000042", "message": "re-exported from archive" }
],
"failed": ["43: no archived bundle for go-bundle-000043"]
}
| Field | Type | Notes |
|---|---|---|
stream |
string | |
requested_ranges |
[]string |
The raw tokens, e.g. ["42","45-47"] |
sequences |
[]int64 |
The expanded list |
reexported |
[]ExportResult |
Successful replays (message:"re-exported from archive") |
failed |
[]string |
omitempty; "<seq>: <error>" — a sequence with no archived bundle fails with "no archived bundle for <bundleID>" |
Retention pruning is not yet built
Every produced bundle is currently retained under <root>/bundles, so re-export always works. A bundle whose archive copy is gone cannot be re-exported.
Watches — /admin/watches*¶
SQLite-backed recurring collects (<root>/watches.db). The scheduler tick is --watch-interval (default 60s; 0 disables it), and the minimum interval floor is 1 minute. See Scheduling (watches).
GET /admin/watches — list¶
Returns WatchListResponse:
{ "watches": [
{
"id": 1,
"stream": "python",
"label": "requests hourly",
"spec": "{\"requirements\":[\"requests\"]}",
"interval_seconds": 3600,
"enabled": true,
"created_at": "2026-07-05T09:00:00Z",
"last_run_at": "2026-07-05T10:00:00Z",
"last_status": "ok",
"last_message": "bundle python-bundle-000007: 12 unit(s)",
"next_run_at": "2026-07-05T11:00:00Z"
}
] }
Watch fields: id, stream, label, spec (the collect payload as a JSON string), interval_seconds, enabled, created_at, last_run_at (omitempty), last_status (omitempty, "ok"/"error"), last_message (omitempty, e.g. "no new content since last export; skipped"), next_run_at.
POST /admin/watches — create¶
Body createWatchRequest. Body limit 8 MiB. Validation errors → 400.
{
"stream": "python",
"label": "requests hourly",
"spec": { "requirements": ["requests"] },
"interval_seconds": 3600
}
| Field | Type | Notes |
|---|---|---|
stream |
string | Must be a known stream |
label |
string | Defaults to stream if empty |
spec |
JSON | The raw collect payload for that stream; must be valid JSON |
interval_seconds |
int64 | Must be ≥ 60 |
Returns the created Watch.
POST /admin/watches/update — edit¶
Body updateWatchRequest. Body limit 8 MiB. This is the Edit button in the UI: it changes a watch's label, interval, and/or stored spec in place, keeping its id, stream, enabled state, and run history.
{
"id": 1,
"label": "requests every 2h",
"interval_seconds": 7200,
"spec": { "requirements": ["requests==2.32.4"] }
}
| Field | Type | Notes |
|---|---|---|
id |
int64 | Required; unknown id → 404 |
label |
string | Optional; blank keeps the current label |
spec |
JSON | Optional; omitted or null keeps the current spec |
interval_seconds |
int64 | Optional; 0 keeps the current interval, otherwise must be ≥ 60 |
The merged result is validated like a create (interval floor, valid-JSON spec) → 400. The stream cannot be changed — delete and recreate instead. Returns the updated Watch.
Two scheduling effects to know about:
- If the watch has run before,
next_run_atis re-spaced tolast_run_at + interval_seconds, so shortening the interval can make it due immediately; a never-run watch keeps itsnext_run_at. - A run already queued or running keeps the spec it was enqueued with; the edit applies from the next run.
Act on a watch by id¶
POST /admin/watches/run, .../enable, .../disable, .../delete all take a watchIDRequest body (limit 64 KiB):
| Route | Response | Effect |
|---|---|---|
/admin/watches/run |
{"status":"started"} |
Runs it now in the background (guarded against the scheduler) |
/admin/watches/enable |
{"status":"ok"} |
Enables and makes it due promptly |
/admin/watches/disable |
{"status":"ok"} |
Disables the schedule |
/admin/watches/delete |
{"status":"ok"} |
Removes the watch |
Bundle status — GET /admin/bundles and GET /ui/api/status¶
Both return the identical LowBundleStatus payload.
{
"streams": [
{
"stream": "go",
"next_sequence": 43,
"exported_sequences": [
{
"sequence": 42,
"bundle_id": "go-bundle-000042",
"in_archive": true,
"in_outbound": false,
"size_bytes": 1048576
}
]
}
]
}
| Field | Type | Notes |
|---|---|---|
streams[].stream |
string | Union of known streams, streams with state, and streams with bundle files on disk (sorted) |
streams[].next_sequence |
int64 | The next-to-allocate counter (floored at 1) |
exported_sequences[].sequence |
int64 | |
exported_sequences[].bundle_id |
string | |
exported_sequences[].in_archive |
bool | A retained copy exists in <root>/bundles (re-transmittable) |
exported_sequences[].in_outbound |
bool | Still staged in the export dir; goes false once forwarded across the diode — the normal "sent" state, not an error |
exported_sequences[].size_bytes |
int64 | Sum of the archive + manifest + signature |
Health & dashboard¶
GET /healthz→ bodyok\n, no JSON. Pure liveness: it answers as long as the process serves.GET /readyz→ 200ok\nwhen the low side can do its job, 503 with a[-] check: reasonline per failing check when it cannot. Checks: the schedule store answers (watch-store), the export spool directory exists (export-spool), and no bundle's last diode transfer failed while its files still wait in the outbound spool (diode-transfer). Append?verboseto list every check on success too. GET/HEAD only; always open, even with auth enabled.GET /metrics→ Prometheus text-exposition telemetry (see the README's monitoring section). GET/HEAD only; always open.GET /,/ui,/ui/→ the self-contained HTML dashboard (tabs: Overview / Go / Python / Maven / NPM / APT / RPM / Containers / AI Models / Crates / Terraform / Helm / NuGet / Alpine / Status). Non-read methods → 405.
HIGH side¶
HighServer.ServeHTTP tries, in order: serveHighAdmin, serveDiode, serveGo, servePython, serveMaven, serveApt, serveRpm, serveHF, serveContainers, serveNpm, serveCrates, serveTerraform, serveHelm, serveNuget, serveApk, serveUploads, serveUI; unclaimed → 404. Every ecosystem handler is read-only (GET/HEAD; others → 405, or a registry error for containers). The one write surface is the opt-in diode ingest (/diode/, below), which only lands files in the import pipeline. The high side never fetches upstream and never invokes toolchains — it serves imported bundle contents from disk. See High side.
Admin & health¶
| Route | Method | Returns |
|---|---|---|
/healthz |
any | ok\n (pure liveness) |
/readyz |
GET/HEAD | 200 ok when the import pipeline is healthy; 503 listing the failing checks when a stream is blocked on a missing bundle (stream-gaps), landed bundles sit undrained (import-backlog), import passes stopped completing or the last one failed (import-pipeline), import status is uncomputable (import-status), or the unverified-transport quota is exhausted (transport-quota). ?verbose lists every check on success too |
/metrics |
GET/HEAD | Prometheus telemetry |
/admin/import |
POST | ImportResult JSON (imports the next in-order bundle), or 500 |
/admin/status |
GET | ImportStatus JSON |
/admin/missing |
GET | ImportStatus JSON (an alias of /admin/status) |
/admin/uploads |
GET | JSON listing of the uploads tree: folders with each file's name, size, and modification time |
/admin/uploads/delete |
POST | Delete one uploaded file — JSON body {"folder":"…","name":"…"} → {"status":"ok"}, 404 if absent |
The mutating admin routes are loopback-gated
POST /admin/import and POST /admin/uploads/delete answer only loopback callers unless ARTIGATE_HIGH_ALLOW_REMOTE_ADMIN=on — other callers get 403. The read-only routes are open like everything else on the high side.
ImportResult:
ImportStatus — also the body of /ui/api/overview's status:
{
"streams": [
{
"stream": "go",
"last_imported_sequence": 41,
"next_expected_sequence": 42,
"highest_seen_sequence": 47,
"blocking_missing_sequence": 42,
"missing_ranges": ["42-44"],
"quarantined_sequences": [46, 47],
"ready_to_import": false
}
]
}
| Field | Type | Notes |
|---|---|---|
stream |
string | |
last_imported_sequence |
int64 | |
next_expected_sequence |
int64 | last_imported_sequence + 1 |
highest_seen_sequence |
int64 | Highest complete bundle seen in landing/quarantine |
blocking_missing_sequence |
int64 | omitempty; set only when a later bundle arrived but the immediate next is absent (a real gap) |
missing_ranges |
[]string |
Gaps rendered as "42" / "45-47" |
quarantined_sequences |
[]int64 |
Bundles that arrived out of order, held |
ready_to_import |
bool | The very next bundle is on disk and complete |
Status has a side effect
/admin/status, /admin/missing, and /ui/api/overview first sort stray landing bundles into quarantine/duplicates before reporting.
Diode ingest — PUT|POST /diode/<bundle-file>¶
The HTTP diode transport's receiving end, off by default — enabled with ARTIGATE_DIODE_INGEST=on. Enabling it requires a whitespace-free ARTIGATE_DIODE_TOKEN of at least 32 bytes (Authorization: Bearer …, constant-time compare). The body streams atomically into the landing directory; a completed bundle triggers an immediate import.
| Situation | Status |
|---|---|
| Stored | 200, {"stored":"<name>","size":<bytes>} |
| Ingest disabled | 403 diode ingest is disabled; set ARTIGATE_DIODE_INGEST=on |
| Missing/wrong token | 401 |
| Method not PUT/POST | 405 |
Name is not one of the three bundle-file shapes (<stream>-bundle-<seq>{.tar.gz,.manifest.json,.manifest.json.sig}) |
400 |
| Body exceeds the 64 GiB per-file limit | 413 |
The body is capped at 64 GiB before it enters the normal verification pipeline. Oversized or interrupted uploads are removed without replacing an existing landing file. The transport carries no trust — an uploaded bundle is verified exactly like a diode-carried file (signature, sequencing, hashes).
Serving endpoints¶
Each ecosystem owns a URL prefix. Point clients at the high-side base URL; see the per-ecosystem pages for full client configuration.
Go (GOPROXY) — prefix /go¶
Client: GOPROXY=<base>/go,off (GOSUMDB stays on — the checksum database is served below). Standard GOPROXY protocol. See Go modules.
| URL | Returns |
|---|---|
/go/<module>/@v/list |
Newline list of complete, non-pseudo versions (text/plain) |
/go/<module>/@v/<version>.info |
{"Version":"...","Time":"..."} JSON |
/go/<module>/@latest |
Latest ModuleInfo JSON |
/go/<module>/@v/<version>.mod |
The go.mod |
/go/<module>/@v/<version>.zip |
The module zip |
/go/<module>/@v/<version>.ziphash |
The zip hash |
/go/sumdb/<name>/supported |
200 when checksum-database data for <name> is mirrored, else 404 (the client then treats the proxy as not proxying that database) |
/go/sumdb/<name>/latest |
The database's latest mirrored signed tree head |
/go/sumdb/<name>/lookup/<module>@<version> |
The signed lookup record captured for that module |
/go/sumdb/<name>/tile/… |
Merkle-tree tiles (hash proofs) |
Only these shapes are served; anything else → 404.
Python (PEP 503 simple index) — prefixes /simple, /packages/¶
Client: pip install --index-url <base>/simple <pkg>. See Python (PyPI).
| URL | Returns |
|---|---|
/simple or /simple/ |
HTML anchor list of normalized project names |
/simple/<project>/ |
HTML "Links for <project>" with <a href="/packages/<file>#sha256=<hash>"> per wheel |
/packages/<filename> |
The wheel file (no slashes allowed in <filename>) |
Maven — prefix /maven¶
Client: use <base>/maven/ as a repository URL. See Java (Maven).
Serves the Maven-2 layout directly: /maven/<group/as/path>/<artifact>/<version>/<file>. maven-metadata.xml (and its .sha1/.md5) is computed on the fly for the enclosing group/artifact directory.
APT — prefix /apt¶
Client sources.list URI: <base>/apt/<mirror-name>. See APT (Debian/Ubuntu).
Static serving of the mirrored dists/, pool/, Release, InRelease, Packages*, etc.
RPM — prefix /rpm¶
Client baseurl=<base>/rpm/<repo-name>. See RPM (RHEL/Fedora).
Static serving of repodata/ plus the RPMs.
Containers (OCI / Docker Registry v2) — prefix /v2¶
Client: docker pull <high-side-host>/<repo>:<tag>. See Container images (OCI).
| URL | Returns |
|---|---|
/v2/ |
Sets Docker-Distribution-API-Version: registry/2.0, body {} (version probe) |
/v2/_catalog |
{"repositories":["...","..."]} |
/v2/<name>/tags/list |
Tags list JSON (<name> may contain slashes) |
/v2/<name>/manifests/<ref> |
Image manifest (<ref> = tag or sha256:...) |
/v2/<name>/blobs/<digest> |
Blob (config/layer) by sha256:... digest |
Non-read methods reply with a registry-style error UNSUPPORTED "read-only registry"; invalid names → NAME_INVALID.
NPM — prefix /npm¶
Client: npm config set registry <base>/npm/. See NPM.
| URL | Returns |
|---|---|
/npm/<name> or /npm/@scope/pkg |
Packument (full package metadata document; dist-tags are the mirrored upstream tags filtered to versions present, with latest regenerated when the upstream tag is absent or unmirrored) |
/npm/<name>/<version> or /npm/@scope/pkg/<version> |
Single version manifest — <version> may also be a served dist-tag (latest, beta, …) |
/npm/<name>/-/<file> or /npm/@scope/pkg/-/<file> |
Tarball (<file> must contain no slash) |
POST /npm/-/npm/v1/security/advisories/bulk |
npm audit bulk advisories, answered from the mirrored OSV npm database (gzip request bodies supported); 404 until that database is imported, so npm reports audit unavailable rather than a false all-clear |
AI models — prefixes /v2, /hf, /api/models, /<org>/<name>/resolve¶
Clients: ollama pull <high-host>/<org>/<name>:<tag>, or HF_ENDPOINT=<base> for vLLM/transformers/hf. See AI models (Hugging Face).
| URL | Returns |
|---|---|
GET\|HEAD /v2/<org>/<name>/manifests/<tag-or-digest> |
variant manifest (also under /v2/hf.co/<org>/<name>/…); tags and names match case-insensitively |
GET\|HEAD /v2/<org>/<name>/blobs/<digest> |
blob, Range supported |
GET /v2/<org>/<name>/tags/list |
{"name":"<org>/<name>","tags":[…]} |
GET /hf/<org>/<name>/<tag>.gguf |
the variant's raw model file, Content-Disposition filename <name>-<tag>.gguf |
GET /api/models/<org>/<name>[/revision/<rev>] |
snapshot info: pinned commit (sha) + siblings file list |
GET /api/models/<org>/<name>/tree/<rev>[/<path>] |
file listing (?recursive=true supported) — what modern huggingface_hub clients enumerate before downloading; always a single page |
GET\|HEAD /<org>/<name>/resolve/<rev>/<path> |
snapshot file with ETag (sha256) and X-Repo-Commit; Range supported |
Hub-API misses carry X-Error-Code (RepoNotFound, RevisionNotFound, EntryNotFound) so huggingface_hub raises its typed errors. The /v2 space is shared with containers without ambiguity — a container name's first segment is a dotted registry host, which can never parse as a Hugging Face organization.
Rust crates (cargo sparse registry) — prefix /crates¶
Client: ~/.cargo/config.toml source replacement with registry = "sparse+<base>/crates/index/". See Rust crates.
| URL | Returns |
|---|---|
/crates/index/config.json |
{"dl": "<base>/crates/dl"} — cargo appends /{crate}/{version}/download itself |
/crates/index/<index-path> |
Regenerated sparse-index file for one crate (e.g. /crates/index/se/rd/serde) |
/crates/dl/<name>/<version>/download |
The .crate archive |
Terraform / OpenTofu — prefixes /.well-known/terraform.json, /terraform¶
Client: ~/.terraformrc network_mirror (https://<host>/terraform/v1/providers/, HTTPS required), or host-prefixed source addresses (<host>/hashicorp/aws). See Terraform / OpenTofu.
| URL | Returns |
|---|---|
/.well-known/terraform.json |
Service discovery: providers.v1 → /terraform/v1/providers/, modules.v1 → /terraform/v1/modules/ |
/terraform/v1/providers/<ns>/<type>/versions |
Mirrored provider versions with per-platform availability |
/terraform/v1/providers/<ns>/<type>/<ver>/download/<os>/<arch> |
Download descriptor: download_url, shasum, shasums_url, shasums_signature_url, mirrored signing_keys |
/terraform/v1/modules/<ns>/<name>/<system>/versions |
Mirrored module versions |
/terraform/v1/modules/<ns>/<name>/<system>/<ver>/download |
204 + X-Terraform-Get naming the archive |
/terraform/providers/…, /terraform/modules/… |
Artifact files: provider zips, …_SHA256SUMS, …_SHA256SUMS.sig, module.tar.gz |
Helm — prefix /helm¶
Client: helm repo add artigate <base>/helm/<mirror>. See Helm charts.
| URL | Returns |
|---|---|
/helm/<mirror>/index.yaml |
The regenerated repository index (application/yaml) |
/helm/<mirror>/charts/<name>-<version>.tgz |
The chart archive |
NuGet (v3 feed) — prefix /nuget¶
Client: nuget.config source <base>/nuget/v3/index.json with <clear />. See NuGet.
| URL | Returns |
|---|---|
/nuget/v3/index.json |
Service index (PackageBaseAddress/3.0.0, RegistrationsBaseUrl, SearchQueryService) |
/nuget/v3-flatcontainer/<id>/index.json |
{"versions": […]} (lowercase, ascending) |
/nuget/v3-flatcontainer/<id>/<ver>/<id>.<ver>.nupkg |
The package archive |
/nuget/v3-flatcontainer/<id>/<ver>/<id>.nuspec |
The verbatim embedded nuspec |
/nuget/v3/registration/<id>/index.json |
Registration index (single inlined page with catalog entries and dependency groups) |
/nuget/v3/registration/<id>/<version>.json |
Registration leaf for one version (inlined catalog entry, listed, index backlink) |
/nuget/v3/search?q=<text> |
Minimal search (case-insensitive substring on the id) |
Alpine (apk) — prefix /apk¶
Client /etc/apk/repositories line: <base>/apk/<mirror>/<branch>/<repo>. See Alpine (apk).
| URL | Returns |
|---|---|
/apk/<mirror>/<branch>/<repo>/<arch>/APKINDEX.tar.gz |
The regenerated index — RSA-signed when --apk-rsa-key is set |
/apk/<mirror>/<branch>/<repo>/<arch>/<pkg>-<ver>.apk |
The package |
/apk/keys/<key-name> |
The PEM public key matching --apk-rsa-key (only when signing is configured) |
Conda — prefix /conda¶
Client: conda/mamba/micromamba with --override-channels -c <base>/conda/<mirror>. See Conda channels.
| URL | Returns |
|---|---|
/conda/<mirror>/<subdir>/repodata.json |
The regenerated subdir index (application/json; noarch always exists, even empty) |
/conda/<mirror>/<subdir>/<file>.conda / ….tar.bz2 |
The package file |
RubyGems (compact index) — prefix /rubygems¶
Client Gemfile: source "<base>/rubygems". See RubyGems.
| URL | Returns |
|---|---|
/rubygems/versions |
created_at header + one <name> <versions> <md5> line per gem |
/rubygems/info/<gem> |
The verbatim upstream info lines whose .gem is present |
/rubygems/names |
One gem name per line |
/rubygems/gems/<name>-<version>[-<platform>].gem |
The gem file |
The legacy Marshal endpoints (specs.4.8.gz, quick/, api/v1/dependencies) are deliberately not served.
Composer (p2) — prefix /composer¶
Client composer.json: repository {"type":"composer","url":"<base>/composer"} plus "packagist.org": false. See PHP Composer.
| URL | Returns |
|---|---|
/composer/packages.json |
{"metadata-url": "/composer/p2/%package%.json", "available-packages": […]} |
/composer/p2/<vendor>/<project>.json |
Full version objects, newest first, dist re-pointed at the mirror |
/composer/p2/<vendor>/<project>~dev.json |
Always an empty list for known packages (no dev versions) |
/composer/dist/<vendor>/<project>/<version_normalized>.zip |
The dist zip |
VS Code extensions — prefix /vsx¶
Client: VSCODE_GALLERY_SERVICE_URL=<base>/vsx/gallery. See VS Code extensions.
| URL | Returns |
|---|---|
POST /vsx/gallery/extensionquery |
The VS Code gallery query API (exact-id and free-text filters, paginated) |
/vsx/assets/<publisher>/<name>/<version>/<assetType> |
Gallery assets: the .vsix (…Services.VSIXPackage) and the manifest (…Code.Manifest) |
/vsx/files/<publisher>/<name>/<publisher>.<name>-<version>.vsix |
Direct .vsix download |
Ansible Galaxy (v3) — prefix /galaxy¶
Client: ansible-galaxy collection install ns.name -s <base>/galaxy/. See Ansible Galaxy.
| URL | Returns |
|---|---|
/galaxy/api/ |
Discovery: {"available_versions": {"v3": "v3/"}} |
/galaxy/api/v3/collections/<ns>/<name>/ |
Collection page with highest_version |
/galaxy/api/v3/collections/<ns>/<name>/versions/ |
Version list, newest first (single page) |
/galaxy/api/v3/collections/<ns>/<name>/versions/<version>/ |
Version detail: artifact{filename,sha256,size}, absolute download_url, metadata.dependencies |
/galaxy/download/<ns>-<name>-<version>.tar.gz |
The collection artifact |
CRAN — prefix /cran¶
Client: install.packages("pkg", repos = "<base>/cran"). See R packages (CRAN).
| URL | Returns |
|---|---|
/cran/src/contrib/PACKAGES and PACKAGES.gz |
The regenerated index (newest present release per package, recomputed MD5sum) |
/cran/src/contrib/<name>_<version>.tar.gz |
The source package |
/cran/src/contrib/Archive/<name>/<name>_<version>.tar.gz |
Superseded releases |
Snap packages — prefix /snap¶
Client: download the pair, then snap ack <name>_<rev>.assert && snap install <name>_<rev>.snap. See Snap packages.
| URL | Returns |
|---|---|
/snap/files/<name>/<name>_<rev>.snap |
The squashfs archive |
/snap/files/<name>/<name>_<rev>.assert |
The store assertion chain (application/x.ubuntu.assertion) |
/snap/info/<name> |
JSON revision index (newest first): version, channel, architecture, digest, and both file URLs per revision |
Git (dumb HTTP) — prefix /git¶
Client: git clone <base>/git/<mirror>.git. See Git repositories.
| URL | Returns |
|---|---|
/git/<mirror>[.git]/HEAD |
ref: refs/heads/<default> |
/git/<mirror>[.git]/info/refs |
The ref list (<sha>\t<refname> lines; the smart probe ?service=git-upload-pack gets the same plain text, pushing git onto the dumb protocol) |
/git/<mirror>[.git]/objects/info/packs |
P pack-<sha1>.pack — only the newest pack is advertised |
/git/<mirror>[.git]/objects/pack/pack-<sha1>.{pack,idx} |
The verified pack and its regenerated index |
OSV advisories — prefix /osv¶
The upstream bucket's own layout, for offline scanners. Ecosystem names may be given verbatim (URL-encoded where needed) or as their storage slug (lowercased, non-[a-z0-9._-] characters → -: Alpine:v3.20 ↔ alpine-v3.20). See OSV advisories.
| URL | Returns |
|---|---|
/osv/ecosystems.txt |
Mirrored ecosystem names, one per line (404 while nothing is mirrored) |
/osv/<ecosystem>/all.zip |
The ecosystem's database snapshot (application/zip) |
/osv/<ecosystem>/<ID>.json |
One advisory (GHSA-…, CVE-…, MAL-…), streamed straight out of the verified zip |
Uploads — prefix /uploads¶
Plain file serving; the listing and delete admin routes are under Admin & health. See Uploads.
| URL | Returns |
|---|---|
/uploads/<folder>/<name> |
The file bytes (range and conditional requests supported) |
Dashboard JSON — /ui/api/*¶
serveUI handles /, /ui, /ui/ (dashboard HTML), /ui/app.js, and the four JSON endpoints below. All are GET/HEAD only (else 405).
GET /ui/api/overview → UIOverview¶
Just the import status; the package trees are fetched lazily.
GET /ui/api/tree?eco=<eco>&path=<path>¶
eco ∈ go (default), python, maven, apt, rpm, hf, containers, npm, crates, terraform, helm, nuget, apk, conda, rubygems, composer, vsx, galaxy, cran, snap, git, osv, uploads. path is the parent node path (empty for root); children are returned one level at a time.
{ "nodes": [
{ "label": "github.com", "path": "github.com", "kind": "dir", "expandable": true, "count": 12 }
] }
UITreeNode fields: label, path, kind (dir | module | version | project | file), expandable (bool), count (omitempty).
- Go / Maven / APT / RPM / containers / AI models / Terraform / Helm / apk / conda / Galaxy use a slash-segment tree: root yields first path segments; an exact module's children are its
versionleaves (path=module@version). - Python uses a two-level tree: root →
projectnodes; a project expands tofile(distribution filename) leaves. Uploads use the same shape: folder → file leaves. - NPM, crates, NuGet, RubyGems, Composer, VS Code, CRAN, git, and OSV use a flat name → versions tree (an npm scope is part of the name; a git repo's "versions" are its short refs).
Inventory is memoized for 3 seconds, so freshly imported content appears within that window.
GET /ui/api/detail?eco=<eco>&path=<path> → UIDetail¶
path = module@version for Go, a wheel filename for Python, a coordinate/ref per ecosystem. Not found → 404.
{
"title": "golang.org/x/text",
"subtitle": "v0.14.0",
"fields": [ { "label": "Module", "value": "golang.org/x/text", "mono": true } ],
"go_mod": "module golang.org/x/text\n\ngo 1.18\n",
"copy_ref": "",
"clone_url": "",
"downloads": [ { "label": "v0.14.0.zip", "url": "/go/golang.org/x/text/@v/v0.14.0.zip" } ],
"layers": [ { "command": "RUN ...", "size": "5.0 MiB", "digest": "sha256:...", "empty": false } ]
}
| Field | Type | Notes |
|---|---|---|
title |
string | |
subtitle |
string | omitempty |
fields |
[]UIDetailField |
{label, value, mono} — Go exposes Module, Version, Published, Zip size, Zip SHA-256, and a Proxy path (/go/<esc>/@v/<verEsc>.zip); Python exposes Filename, Version, Size, Download (/packages/<file>), SHA-256 |
go_mod |
string | omitempty; the full go.mod |
copy_ref |
string | omitempty; a host-relative container pull ref the client prepends its host to |
clone_url |
string | omitempty; a git mirror's host-relative repository path (git/<name>.git) the client prepends this server's origin to, rendering a copyable full git clone command |
downloads |
[]UIDownload |
omitempty; direct-download links {label, url} — the artifact's files (module zip, wheel, jar/pom, per-arch .deb/.rpm, npm tarball, raw GGUF, uploaded file) as host-relative URLs, rendered as download buttons. Empty for leaves that are not plain files (container images, HF repo snapshots) |
layers |
[]UIImageLayer |
omitempty; container build history — {command, size, digest, empty} |
GET /ui/api/repos?eco=<eco> → UIReposResponse¶
Valid only for eco ∈ apt | rpm | containers | hf | apk; anything else → 400 "repos are only available for apt, rpm, hf, containers, and apk". For hf, entries with "kind":"repo" are full repository snapshots (consumed via HF_ENDPOINT); the rest are GGUF models with their variant tags. For apk, each mirror lists its branch/repository/architecture selections as suites, with "kind":"apk" and signed reporting whether --apk-rsa-key is configured.
{ "repos": [
{ "name": "debian",
"suites": [
{ "name": "bookworm", "components": ["main"], "architectures": ["amd64"] },
{ "name": "bookworm-updates", "components": ["main"], "architectures": ["amd64"] }
],
"tags": ["3.20"], "signed": true }
] }
UIRepo fields: name, suites (omitempty, APT only — each suite with its own components/architectures, so the "Set me up" release picker can build exact stanzas), tags (omitempty, containers only), signed (bool — true when the high side republishes with its own GPG signature; for APT, when every suite's InRelease is present). APT fields are empty for RPM.
See also¶
- Low side and High side — operating each role.
- Scheduling (watches) — the recurring-pull model behind
/admin/watches*. - Configuration reference — every flag and environment variable.
- Ecosystems — the per-ecosystem pages linked above.
- Troubleshooting & limitations — error codes and known edges.