Skip to content

Experimental HTTP transport, revision 1

This is the first post-milestone-1 transport binding. It changes no canonical object schemas, IDs, or local repository format. The reference server is a single-repository, loopback-only development service, not the hosted AIPHub.

Scope and authorization

The base path is /aip/v1/repositories/{name}. A name contains 1..64 ASCII letters, digits, hyphens or underscores. It is an administrator-selected routing label, not a filesystem path or canonical ObjectID. Each service instance exposes one explicitly configured repository. Objects never resolve across repositories.

Private mode requires a bearer token for every request, including existence and inventory queries. Public mode permits anonymous GET and HEAD only. PUT always requires the configured token. A token grants read/write access to this one repository; account login, per-user roles and multiple credentials are deferred. Authorization runs before object lookup and returns the same 401 response for missing or incorrect credentials, irrespective of object existence. Credentials are accepted only in the Authorization header, never query parameters or cookies. Responses use Cache-Control: no-store; CORS is not enabled. Requests with an Origin header are rejected in this server-to-server prototype.

The CLI is aip serve --name NAME [--listen 127.0.0.1:7777] [--visibility private|public]. It requires AIP_SERVE_TOKEN in its environment: at least 32 printable ASCII characters without whitespace. Operators must supply a randomly generated secret through their credential tooling. AIP does not print, persist, or put the token in command arguments. Private is the default. The listener must be a literal loopback IP and a port. Plain HTTP is permitted only for this local profile. Any future remote deployment MUST use HTTPS and separately define credential issuance, rotation, quotas and hosted authorization.

serve runs until interrupted. On graceful shutdown it returns the normal CLI success envelope with repository name and listen address; startup failures use the existing CLI error envelope. There is no stream of JSON events on stdout. The server neither executes uploaded code nor changes HEAD or working files.

Objects and discovery

Request relative to base Response
GET /objects?after=ID&limit=N JSON inventory page, ascending ObjectIDs.
HEAD /objects/{id} 200 if a verified object exists, 404 if absent; no body.
GET /objects/{id} 200 with exact canonical bytes, Content-Type: application/cbor.
PUT /objects/{id} Canonical bytes with Content-Type: application/cbor; JSON acknowledgment.

IDs must be full lowercase SHA-256 hex. PUT checks the URL hash, canonical encoding, schema, all referenced objects and graph relationships before storing anything. Dependencies MUST arrive first. Missing dependencies or invalid graphs return 422 and do not poison the visible CAS. Unknown versions are rejected. An identical PUT succeeds without replacing existing bytes. An existing corrupt object is an error, never overwritten. Received objects are immediately visible to inventory and reverse queries, even without registry references. PUT does not implicitly create an intent/state registry reference.

Inventory includes every object type, including evidence and decisions, so reverse associations are discoverable. Success data is {"ids":[ID,...],"next":ID|null}; next is the final returned ID when more entries exist. The default limit is 256; accepted limits are 1..1000. Omit after for the first page. All query parameters are single-valued; unknown parameters fail. Inventory is not a transaction across pages. With concurrent uploads, clients must reconcile again from the beginning to discover IDs inserted before their cursor. No deletion or garbage collection is offered by this binding. Readers must independently validate downloaded bytes and graphs.

Object reads verify the reachable graph before success. Inventory checks bytes, encoding and schema for returned IDs; it is discovery, not a graph-validation certificate. The implementation enumerates loose object filenames for every page and is intended for small repositories.

Registry references

Only states/{id} and intents/{id} are supported. HEAD is local workspace selection and is never exposed or updated through this binding. There are no named branch tips, remote workspace selection or arbitrary reference paths.

GET /refs/{kind}/{id} returns data {"id":ID} or 404 if absent. It verifies the reference’s content, target type and reachable graph.

PUT /refs/{kind}/{id} accepts application/json with exactly {"expected":null|ID,"next":ID}. Both fields are required, with no duplicate or unknown fields. Next MUST equal the ID in the reference’s name. Expected is null for creation, or that same ID for reaffirmation. Under the repository write lock, the server compares the observed reference with expected, verifies the target graph/type, then durably publishes the reference. Mismatched expected returns 409 without changing the reference. A retry after a lost response should resolve the reference first. This is compare-and-swap on one reference, not a transaction over several references or uploaded objects.

Responses and limits

JSON responses use {"ok":true,"data":...} or {"ok":false,"error":{"code":CODE,"message":TEXT}}. Messages are diagnostic; clients use status and code. Object PUT success data is {"id":ID}. Successful JSON requests return 200, including duplicate object uploads and new references.

HTTP status Error code Meaning
400 invalid_request Invalid query, hash mismatch or reference body.
401 unauthorized Missing or incorrect bearer credential.
403 origin_forbidden Browser-origin requests are outside this profile.
404 not_found Unknown/malformed authorized route or missing object/reference.
405 method_not_allowed Unsupported method; Allow names supported methods.
409 conflict Reference precondition failed.
413 too_large Body exceeds the configured protocol limit.
415 unsupported_media_type Wrong content type or encoded request body.
422 invalid_object Object schema, references or relationships are invalid.
500 storage_error Corruption, write contention or storage failure.
503 busy Another request is being processed; retry later.

HEAD never returns a response body, including on errors. Object bodies are limited to 67,108,864 bytes, reference bodies to 1024 bytes. Compressed request bodies are not accepted. Unknown JSON fields, duplicate keys, trailing values and missing fields are rejected. The reference handler permits one active request at a time and rejects excess concurrent requests with 503 to bound memory use. It sets header/read/write/idle timeouts. Graph limits remain those in encoding.md; no constant-time graph-validation or large-repository performance promise is made.

Transfer procedure and future hosting

Discover object IDs, compare with the destination inventory, download missing objects, traverse their references, and upload in dependency-first order. Include evidence and decisions discovered in inventory, not just a state’s forward closure. Verify destination graphs, then create/reaffirm the selected state and intent registry references. Transfers can be retried because object IDs are immutable; complete objects from interrupted transfers remain discoverable.

The initial server implements this wire boundary; a user-facing synchronization client is subsequent work. R2 storage, D1 metadata, Astro 7/Starlight distribution pages, multi-user authentication and hosted public/private repositories belong to AIPHub adapters. They must preserve the byte/hash and authorization boundaries above. Neither Cloudflare nor AIPHub becomes a local AIP dependency.