Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Artifact contract

This release stores bounded reports and logs as authenticated artifacts and also records external HTTPS links. Source checkpoints continue to travel through Git remotes. The service never fetches an external artifact URL and never treats a client filename as a filesystem path.

Limits and storage

  • An uploaded artifact is at most 16 MiB. Request bodies are streamed to disk and SHA-256 checked; they are not encoded as JSON or buffered in memory.
  • Live and reserved uploads share a configurable service-wide quota (10 GiB by default). A reservation counts until it expires, is deleted, or becomes finalized and later expires.
  • The service preserves configurable free disk space (256 MiB by default). It checks available space at reservation, before upload, and while writing each chunk. Four uploads may run concurrently. Waiting for an upload slot is bounded to five seconds and an upload must complete within two minutes.
  • The store is a directory named after and next to its database, for example coordinator.sqlite3.artifacts. Each database therefore has an isolated store. Blob, staging, and lock names use server-generated UUIDs. On Unix, directories are mode 0700 and staged files are mode 0600.
  • Upload reservations last one hour. Retention defaults to 90 days and can be set from 1 to 3650 days or pinned with no expiry. Expiry is evaluated using the service clock. Metadata remains after expiry or deletion.

The service stages a body under its generated key, enforces its exact reserved size while receiving it, hashes it incrementally, calls fsync, and atomically renames it into the blob store. Only then does it take the SQLite writer lock, recheck the current credential/session, reservation author, expiry, size, and digest, and commit finalized metadata, its mutation receipt, and its event in one transaction. No transaction is held while receiving a body or doing filesystem work. A crash after rename and before commit leaves a recoverable orphan bound to the same reservation; retrying the same saved bytes and idempotency key verifies that blob and finalizes it without replacement or duplication.

Each cleanup pass is bounded to 100 database records, 100 files from one rotating blob prefix, and 100 staging entries. Before removing a known expired/deleted blob, cleanup obtains the artifact lock and rechecks its current database state. Unknown blob orphans must be older than five minutes, then receive the same lock and a fresh absence check. This prevents cleanup racing upload finalization or a retention change. Live reservations remain intact, including a durable blob awaiting a retried database finalization. Run reconciliation after opening the state at service startup; reservation creation also invokes a bounded pass.

HTTP API

Every route requires normal human or agent authentication. Every mutation also requires a persisted Idempotency-Key. All JSON success bodies use the normal data envelope.

POST /api/v1/projects/{project}/artifacts records an external link:

{
  "display_name": "CI report",
  "media_type": "text/html",
  "external_url": "https://artifacts.example/report/123",
  "task_id": null,
  "job_id": null,
  "size_bytes": null,
  "sha256": null,
  "retention_days": 90,
  "pinned": false
}

The URL must be HTTPS, have a host, and contain no embedded credentials. The service records it without making a network request. Optional task and job IDs must belong to the same project; when both are supplied, the job must belong to that task.

POST /api/v1/projects/{project}/artifacts/uploads reserves an upload:

{
  "filename": "check-report.txt",
  "media_type": "text/plain",
  "size_bytes": 1234,
  "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "task_id": null,
  "job_id": null,
  "retention_days": 90,
  "pinned": false
}

The response contains artifact metadata and an authenticated upload_path. Send the exact raw bytes to that path with PUT, the same normal authentication, and a separately persisted idempotency key. Content-Length, when supplied, must match the reservation. The reservation author is the only principal allowed to upload its content. A retry must reuse its saved bytes, digest, and key.

GET /api/v1/projects/{project}/artifacts returns a bounded cursor page plus authenticated service-wide upload usage, quota, disk capacity/reserve, maximum artifact size, and default retention diagnostics in data.storage. limit defaults to 50 and is capped at 200. GET /api/v1/projects/{project}/artifacts/{artifact} returns one metadata record.

GET /api/v1/projects/{project}/artifacts/{artifact}/content streams finalized, live upload bytes. Downloads always use Content-Disposition: attachment, a generated safe ASCII filename, the recorded content type, Content-Length, and X-Content-Type-Options: nosniff. External-link content is not proxied.

POST /api/v1/projects/{project}/artifacts/{artifact}/retention accepts:

{"pinned": false, "retention_days": 180}

When pinned is true, omit retention_days. The author or a human operator may change retention. POST /api/v1/projects/{project}/artifacts/{artifact}/delete accepts a nonempty reason; the author or a human operator may create the deletion tombstone. The database change commits before physical cleanup. If cleanup is interrupted, retrying the same key or startup reconciliation removes the inaccessible bytes.

Metadata reports database state as reserved, finalized, or deleted, plus a current availability value:

AvailabilityMeaning
pendingA live upload reservation has not finalized.
availableA live external link or service blob is accessible.
expiredIts reservation or retention deadline passed.
deletedAn attributed deletion tombstone exists.
unavailableFinalized upload metadata exists but its blob is missing or unsafe.

Task submission integration must call validate_submission_artifacts(connection, project, ids, now) under its writer transaction before inserting the submission, then call link_submission_artifacts(connection, project, submission, ids, now) in that same transaction. References are unique, limited to 100, same-project, finalized, and live. submission_artifacts is immutable; later expiry or deletion changes availability without erasing the historical reference.

Native transfers and configuration

After reserving an upload, use artifacts upload --id ID --file PATH with the same local harness session. The CLI snapshots exact bytes into its protected journal before sending them. Retrying that command reuses the snapshot, digest, and idempotency key, even if the original file changed. Completed upload retries reauthenticate and return current artifact metadata, including expiry, deletion, or unavailability; they never upload again. Use artifacts download --id ID --output PATH to save bounded bytes into a new file. Its size and SHA-256 must match authenticated metadata before the new file is published. An existing destination is never overwritten.

Server settings COORDINATOR_ARTIFACT_QUOTA_BYTES and COORDINATOR_ARTIFACT_DISK_RESERVE_BYTES configure admission limits; the latter must retain at least 16 MiB. COORDINATOR_JSON_BODY_LIMIT_BYTES defaults to 1 MiB and can be lowered to 1024 bytes. Binary streaming retains its separate 16 MiB hard limit. Startup reconciles artifact metadata and storage before accepting requests, preserving explicit unavailable/tombstoned evidence.