continuum HTTP API
The wire contract between the qube CLI and the
continuum registry server. Read endpoints are
anonymous; write endpoints require a bearer token.
Status: draft (v0). Endpoints, request/response shapes, and auth flow are subject to change while the language is pre-1.0.
Conventions
- Base URL: the production registry. Default in
qubeishttps://qubes.q64.dev. The companion user-facing UI is athttps://continuum.q64.dev. - API prefix:
/v1. A breaking change moves the prefix to/v2and the previous version is kept available for one major-release grace period. - Content type: request and response bodies are JSON unless
otherwise noted. Archives are zip (
application/zip,.zip). - Auth:
Authorization: Bearer <token>on write endpoints. Tokens are scoped (publish-only, owner-only, admin) and rate-limited. - Errors: every non-2xx response body uses the same
diagnostic envelope
qubealready parses for compiler errors. HTTP status code complements but does not replace the envelope’scodefield. - Pagination:
?page=<n>&limit=<n>(1-indexed). Default limit 50, max 200. Responses includenext/prevURL hints. - Caching: read endpoints set
ETagand respond toIf-None-Match. Archive downloads also setCache-Control: immutable, max-age=31536000because archives are content-addressed and never replaced. - Rate limits: signalled via
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Resetheaders.
Endpoints
Read — qube metadata
GET /v1/qubes/{name}Returns the qube’s manifest digest, latest stable version, version list, owners, and rendered README.
Path parameter {name} is the qube’s dotted name verbatim
(dev.q64.webmcp_client). Dots are URL-safe, so the name is always a
single, unescaped path segment — no percent-encoding, no scope slash.
{ "name": "audio-filters", "latest": "0.4.2", "owners": ["alice"], "description": "Real-time audio filters.", "repository": "https://github.com/...", "license": "MIT OR Apache-2.0", "downloads": 12034, "versions": [ { "version": "0.4.2", "published_at": "2026-04-12T14:22:01Z", "yanked": false, "effects": ["@io"] }, { "version": "0.4.1", "published_at": "...", "yanked": false, "effects": ["@io"] } ]}GET /v1/qubes/{name}/{version}Returns the full manifest for a specific version plus its effect index, declared targets, and dependency graph.
GET /v1/qubes/{name}/{version}/archiveReturns the .zip (immutable, content-addressed). The archive
contains the qube source tree rooted at the included qube.json5.
Read — index (sparse, Cargo-style)
GET /v1/index/{prefix}/{name}Returns a newline-delimited JSON list of every published version for
{name}, each line being a small record (version, dependencies,
yanked, effects digest). qube’s resolver fetches these efficiently
without paginating through versions.
Prefix sharding follows Cargo’s: qu/be/qube-name for names ≥ 4
chars; shorter names use abbreviated shards (1/x, 2/xx, 3/x/xxx).
Read — search and discovery
GET /v1/search?q=<query>&category=<cat>&effect=<marker>Full-text and faceted search over name, description, keywords, categories, and declared effects.
GET /v1/categoriesGET /v1/popular?period=<24h|7d|30d|all>GET /v1/recentWrite — publish
POST /v1/qubes/{name}Authorization: Bearer <token>Content-Type: multipart/form-data
manifest=<qube.json5 body> archive=<binary zip>Server validates, in order:
- Manifest matches
qube.json5.schema.json. - Version is not already published; name is available or owned by the token’s subject.
- Archive checksum. The server computes the SHA-256 of the
uploaded archive and stores it as the version’s canonical
identifier. The manifest does not carry a checksum field; the
archive is content-addressed by the server’s computation, which
is then returned to the client and recorded in
qube.lock(per “Resolver protocol” below). - Effect indexing. The server runs the effect analyser (the
same code path
qube audituses locally — seeqube-cli.md§“Publishing flow”) on the uploaded archive, producing the qube’s transitive effect set. The detected set is stored alongside the manifest and surfaced viaGET /v1/qubes/{name}/{version}/effects. - Effect cross-check. The manifest’s
effects.declaredmust be a superset (after closure under implications) of the detected set. Drift returns422 Unprocessable Entitywith a diagnostic envelope (EFF130) explaining which detected effect was missing from the declaration. - Capability cross-check. The manifest’s
capabilitiesmust match the capability set derived from the effect index (perenv.md§“Capability disclosure”). Drift returns422withENV040.
On success: 201 Created with the new version’s metadata, including
the indexed effect set and the computed SHA-256.
Write — yank / unyank
DELETE /v1/qubes/{name}/{version} # yankPOST /v1/qubes/{name}/{version}/unyank # restoreYanked versions are still resolvable for existing lockfiles but new
qube add calls skip them. Yanking is not deletion; the archive
remains downloadable for reproducibility.
Write — owner management
POST /v1/qubes/{name}/owners { add: ["bob"] }DELETE /v1/qubes/{name}/owners { remove: ["bob"] }GET /v1/qubes/{name}/ownersThe original publisher cannot be removed; ownership transfer is a separate confirmed flow (see admin docs).
Auth
POST /v1/auth/token{ "scopes": ["publish"], "description": "CI for example/voice-agent", "expires_in": 7776000}→ 201{ "token": "qube_pat_…", "id": "tok_abc123", "scopes": ["publish"], "expires_at": "..."}Tokens are PATs (personal access tokens); the registry never returns
the raw token again after creation. Token rotation and revocation
follow the same endpoint shape (DELETE /v1/auth/token/{id}).
Resolver protocol
The expected dance for qube install:
- For each direct dep in
qube.json5, fetchGET /v1/index/{shard}/{name}. - Run resolution locally (pubgrub-style) producing a flat plan.
- For each resolved
(name, version):- Fetch
GET /v1/qubes/{name}/{version}for the full manifest. - Fetch
GET /v1/qubes/{name}/{version}/archiveand extract to~/.qube/cache/sha256/<digest>/.
- Fetch
- Write
qube.lockwith name, version, source URL, sha256, and the effect index per dep.
qube install --offline skips steps 1, 3 and depends on qube.lock
- a populated cache.
Capability disclosure surface
qube audit calls:
GET /v1/qubes/{name}/{version}/effectsResponse:
{ "declared": ["@io", "@network"], "detected": ["@io", "@network"], "transitive": ["@io", "@network"], "by_dependency": { "url-parser@1.2.0": ["@pure"], "http-client@0.4.0": ["@io", "@network"] }}Field semantics:
| Field | Source |
|---|---|
declared | The qube’s manifest effects.declared (per qube.json5.md §Effects). |
detected | The compiler-derived effect set from static analysis on this qube’s pub surface. Matches declared after the publish-time cross-check (EFF130). |
transitive | The closure of detected over every dependency’s detected set, per the implication graph in effects.md. |
by_dependency | The contribution of each direct dependency to transitive. Map of <name>@<version> → effect list. |
The manifest’s effects.deny field is not surfaced here —
denial is enforced locally at resolve time, not part of the
published metadata. (A dependency carrying a denied effect is
EFF131, raised by the resolver before any HTTP call.)
This is the data behind “what does this qube ultimately touch” —
shown in the registry UI on every qube detail page and in
qube audit output.
RPC endpoint resolution
A qube that serves an RPC world (rpc.export: true, per
qube.json5.md §RPC) registers a served world and an
endpoint address alongside its capability/effect metadata. A consumer’s
rpc.import may name a qube instead of a literal wrpc://… URL; the
resolver maps the name to its endpoint here, the same way it resolves a
dependency version:
GET /v1/qubes/{name}/{version}/worldreturns the synthesized WIT world the qube serves and its endpoint
address(es). Because importing a remote qube adds @wire to the consumer’s
effect set (per effects.md and rpc.md), the
remote dependency is disclosed in qube audit like any capability — the
@wire reach and the served world appear at qube add time. The endpoint
itself may be a qubepods deployment (per env.md), whose
wasi:http endpoint doubles as the wRPC server.
Schema validation endpoint (optional)
POST /v1/validateContent-Type: application/json5
<qube.json5 body>Server validates against qube.json5.schema.json
and returns either { "ok": true } or a diagnostic envelope describing
the schema violations. Useful for editors that don’t want to bundle the
schema themselves.
Archive format
- Container: a zip archive (
.zip,application/zip); entries are stored with DEFLATE. Chosen over.tar.gzfor first-class support in the toolchain languages (Zigstd.zip/ a vendored MITminizfor thequbeCLI;fflateon the Workers registry) and native double-click / drag-and-drop extraction on Windows. DEFLATE and the zip format are unencumbered (RFC 1951; no patents), like gzip. - Layout: a single root directory named
<qube-name>-<version>— e.g.dev.q64.webmcp_clientat0.1.0packs underdev.q64.webmcp_client-0.1.0/. Dotted names are filesystem-safe, so the name appears verbatim (no flattening). The directory holdsqube.json5at its root plus the file set described below. The directory name is presentational only: a qube’s canonical identity is its manifestname/versionplus the archive’s SHA-256 (below). - Default file set (when
includeis absent in the manifest):qube.json5, every file undersrc/, every file undertests/, every file underexamples/(and a top-levelexample/), the README (whatever pathreadmenames, orREADME.mdif unset), and everyLICENSE-*file at the project root. Examples ship by default so an installed qube carries runnable usage guidance for humans and coding agents; a qube with heavy example assets trims them withexclude. - When
includeis present, the file set is exactly the union of the default set above and theincludeglobs.includeadds to the default; it never replaces it. (Manifests that need to remove a default-included file useexclude.) excludeis applied last and may drop any file the previous steps would have included, default or not.- Maximum unpacked size: TBD (likely 50 MB for v0; raise via owner request).
- SHA-256 of the archive is the canonical identifier; the registry
computes it at publish time (per “Write — publish” step 3) and
qube.lockrecords it.
Address space and compiled artifacts
The Continuum stores source archives, not compiled wasm: a published
.zip is the qube’s source tree (per “Archive format”), content-addressed by
its SHA-256. The address space (wasm32 / wasm64, see
memory.md §“The platform”) is therefore not a registry
concern — it is chosen by the consumer at build time, and one source archive
yields either variant. The published qube.json5 only declares which address
spaces a qube’s targets can build (per qube.json5.md §Targets),
not a compiled artifact.
Storing both compiled builds of a qube — so a runtime can hand the
WebKit-compatible wasm32 artifact to iPad/Safari and wasm64 to capable
engines, falling back to wasm32 when in doubt — is a deploy-host
responsibility, not the Continuum’s. On qubepods that is the artifact store
plus the QubePod manifest’s component.variants map; the glue code probes the
client engine and requests the matching build (see
memory.md §“Address-space negotiation”).
If the ecosystem later wants the Continuum itself to serve prebuilt, content-addressed
wasm32/wasm64artifacts alongside source (the way some npm packages ship prebuilt binaries next to source), that is an additive/v1/qubes/{name}/{version}/artifact?addr=wasm32|wasm64endpoint — it does not change the source-archive contract above. Tracked as future work.
Notes on hosting
The reference deployment runs on Cloudflare Workers (handlers), R2
(archive storage), D1 (metadata, ownership), and KV (hot
indexes/search caches). The HTTP API surface above is the contract;
the storage layout is implementation-defined. Re-implementations can
ship on any stack as long as they honor this spec and the
qube.json5.schema.json it links to.