Skip to content

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. The go stream keeps the legacy single-stream numbering.
  • Error codes: collect and re-export errors are 400; watch validation 400, watch store failures 500; high-side import/status failures 500; UI detail not-found 404; repos for a wrong ecosystem 400. Non-read methods on serving/UI routes return 405.
  • 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).

{ "coordinates": ["com.google.guava:guava:33.0.0-jre"], "pom_xml": "" }
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).

{ "images": ["alpine:3.20", "ghcr.io/org/app@sha256:abc..."] }
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.

{
  "crates": ["serde@1.0.203", "tokio"],
  "resolve_deps": true,
  "include_optional": false
}
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.

{
  "packages": ["Newtonsoft.Json@13.0.3", "Serilog"],
  "resolve_deps": true
}
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.

{
  "gems": ["rake@13.2.1", "rails"],
  "platforms": ["x86_64-linux"],
  "no_deps": false
}
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.

{
  "packages": ["monolog/monolog", "psr/container:2.0.2"],
  "no_deps": false
}
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.

{
  "extensions": ["golang.Go", "redhat.vscode-yaml@1.14.0"],
  "no_deps": false
}
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.

{
  "collections": ["ansible.posix", "community.general@8.5.0"],
  "no_deps": false
}
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).

{
  "packages": ["jsonlite", "data.table@1.15.4"]
}
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.

{
  "snaps": ["hello", "firefox@latest/candidate"],
  "architecture": "amd64"
}
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.

{
  "url": "https://github.com/org/repo.git",
  "name": "repo",
  "refs": ["refs/heads/main"]
}
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.

{
  "ecosystems": ["npm", "PyPI", "Go", "Alpine:v3.22"]
}
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 done or error event follows zero or more log and dl events.
  • 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":"log","message":"→ [3/12] rsc.io/quote@v1.5.2"}
{"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"}}
{"type":"error","error":"no modules could be fetched: ..."}

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_at is re-spaced to last_run_at + interval_seconds, so shortening the interval can make it due immediately; a never-run watch keeps its next_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):

{ "id": 1 }
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 → body ok\n, no JSON. Pure liveness: it answers as long as the process serves.
  • GET /readyz200 ok\n when the low side can do its job, 503 with a [-] check: reason line 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 ?verbose to 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:

{ "imported": true, "imported_bundles": ["go-bundle-000042"], "message": "all streams up to date" }

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.20alpine-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/overviewUIOverview

{ "status": { "streams": [ /* ...ImportStatus... */ ] } }

Just the import status; the package trees are fetched lazily.

GET /ui/api/tree?eco=<eco>&path=<path>

ecogo (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 version leaves (path = module@version).
  • Python uses a two-level tree: root → project nodes; a project expands to file (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 ecoapt | 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