# artifacts
An **artifact** is a web page the agent made: one HTML file, a few files, or a whole built app. It opens in a window beside the chat that made it (web), or in the browser from a row in the transcript (terminal), on its `latest/` address, which follows updates. Every version is kept, and artifacts outlive their session: deleting a session leaves its artifacts in the gallery, which is the only place an artifact is deleted.
The `artifacts` extension (`src/albedo/harness/extensions/artifacts/`, enabled by default) owns the storage, the Python module, the HTTP service and the page bridge. Clients draw it: `web/` has the window and the gallery, `cli/internal/tui` the row, and `albedo web` (`cli/internal/webui`) serves the pages.
## the model's side
The Python module `artifacts`:
- `artifacts.create(title, html=None, *, files=None, directory=None, entry="index.html")` makes an artifact at version 1. Give exactly one source: `html` (the entry file's text), `files` (a dict of relative path to `str` or `bytes`), or `directory` (a folder to publish whole, such as a build's `dist`).
- `artifacts.update(id, html=None, *, files=None, directory=None, entry=None, title=None)` writes the next version. `html` replaces the entry file and keeps the rest; `files` changes only the paths it names (a value of `None` removes that path); `directory` replaces the whole tree; with no source it only renames.
- `artifacts.list(limit=20, session=None)` lists artifacts, newest change first; `session="current"` keeps this session's.
- `artifacts.get(id, version=None)` describes one version: `id`, `title`, `version`, `versions`, `entry`, `files` (path to size), `session`, `created_at`, `updated_at`.
- `artifacts.read(id, path=None, version=None)` returns one file (the entry by default), `str` for text, `bytes` otherwise.
- `artifacts.state(id)` is what the page saved through `localStorage`; `artifacts.errors(id)` is what failed in the page (newest last, at most 20).
- `artifacts.url(id, version=None)` is the page's address on the running `albedo web`, for checking it with the browser plugin. Without a version it is the `latest/` address, which follows updates (below).
Each `create` and `update` adds an `artifact` activity to the running cell's trace: `{"kind": "artifact", "target":
, "artifact": {"id": , "version": }}`. That is how both clients learn of it, from history they already read; the trace holds no capability key.
Uploads are content addressed: the module hashes each file (SHA-256), asks the daemon which it lacks (`artifacts.missing`), sends only those (`artifacts.upload`, one file per call, base64), then saves the version as a manifest of path, hash and size (`artifacts.save`). Publishing an app again sends only the files that changed.
Limits: 16 MiB per file, 64 MiB and 2,000 files per version. A path is relative, uses `/`, has no empty, `.` or `..` segment, does not start with `.albedo/`, and is at most 512 bytes. Titles are 1 to 200 characters.
## storage
| table | holds |
| --- | --- |
| `artifacts` | `id`, `key` (the capability, 32 hex characters), `session`, `workspace`, `title`, `version` (latest), `created_at`, `updated_at` (ms) |
| `artifact_versions` | `artifact`, `version`, `entry`, `manifest` (JSON: path to `{hash, size}`), `cell`, `created_at` |
| `artifact_files` | `hash` (SHA-256 hex), `size`, `data` |
| `artifact_state` | `artifact`, `state` (JSON object of string to string), `updated_at` |
| `artifact_errors` | `artifact`, `version`, `at`, `message`, `source`, `line`, `column`, `stack` |
Nothing is deleted with its session. Deleting an artifact removes its rows and every file no remaining version names.
## HTTP
Under `/extensions/artifacts/`, all with the daemon token:
- `GET items?limit=&next=&session=` — the gallery, newest change first: `{items: [Artifact], next}`. An `Artifact` is `{id, key, title, version, versions, entry, session_id, session_name, session_deleted, workspace, created_at, updated_at, bytes}`.
- `GET items/{id}` — one artifact with `history`: `[{version, entry, created_at, cell_id, files: [{path, size}], bytes}]`, newest first.
- `DELETE items/{id}` — `204`.
- `GET frame/{id}.{key}/{version}/{path}` — one file of one version, with its content type. An HTML file gets the bridge (below) injected first thing in its ``. A wrong key is `404`, as is a missing path; a path ending in `/` serves its `index.html`, and an empty one the version's entry. A bare `{version}` without the slash redirects (`302`, relative) to `{version}/`, and an empty path whose entry sits in a folder to that entry, so the page's relative paths resolve inside the version. `{version}` may be `latest`: the newest version, whatever it is when asked, so the page's relative files follow too. A numbered version's files are `immutable`; under `latest` they are `no-cache` with the file's hash as `ETag` (`If-None-Match` answers `304`). HTML is always `no-store`, since it carries the saved storage.
- `GET frame/{id}.{key}/{version}/.albedo/version?after=` — `{version}`, the artifact's latest, `no-store`. With `after`, the daemon holds the answer until the latest passes it or 25 s pass (the waiting request registers in an ETS table, `albedo_artifacts:listen`, and saving or deleting the artifact wakes it, so nothing polls); without, it answers at once. `{version}` only has to exist: a following page asks under the version it shows.
- `POST frame/{id}.{key}/{version}/.albedo/state` — the page's `localStorage`, a JSON object of string to string, at most 1 MiB; `204`.
- `POST frame/{id}.{key}/{version}/.albedo/errors` — one failure, `{message, source, line, column, stack}`, at most 16 KiB; `204`.
The frame routes are what `albedo web` forwards; a browser never reaches the daemon itself.
## albedo web
`/frame/...` forwards to the daemon's `/extensions/artifacts/frame/...` with the token and without the cookie, `Origin` or `Referer`. It needs no sign-in: the key in the path is the capability. Only `GET` and `HEAD` (files, `latest/` and the `.albedo/version` wait, whose cache headers it passes through untouched, with no timeout of its own), and `POST` to the two `.albedo/` paths, pass. Every answer carries
```
Content-Security-Policy: sandbox allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-modals allow-downloads allow-pointer-lock
Access-Control-Allow-Origin: *
Cross-Origin-Resource-Policy: cross-origin
Referrer-Policy: no-referrer
X-Content-Type-Options: nosniff
```
so a page opened in its own tab is sandboxed exactly as one in a window.
While it runs, `albedo web` keeps `$ALBEDO_HOME/web.json` with its `pid`, bound `port`, loopback `url` (`http://127.0.0.1:4317`), `names`, `addresses`, `allow`, `local`, `public`, and `tailnet_https`. It removes the file when it stops.
## the boundary
A page has the network: it can load libraries from a CDN, call APIs and open websockets, as the kernel already can. What it must never reach is albedo: the daemon's API, the web client's cookie or page, the browser storage of albedo's origin. Both the window's `