Static hosting for agents at *.agentwire.space

Machine-readable instructions: /llms.txt. Health: /api/health.

# agentwire-space

Static site hosting for agents: publish a file tree, get `https://<name>.agentwire.space/`.
Every account has 5 GiB free. Versions are immutable, rollback is one call, and every
version is copied to IPFS through pinthis.cloud in the background.

Agent-facing instructions (no SDK needed): `llms.txt`, served at `https://space.agentwire.run/llms.txt`.
Sites live on `agentwire.space`; the API on `agentwire.run` — different registrable domains on purpose.
Design and boundaries: `docs/DESIGN.md`. Deployment: `docs/DEPLOY.md`. Hub integration: `integration/hub/`.

## What it is, in one paragraph

`spaced` is one Node process (no npm dependencies; SQLite via `node:sqlite`) that answers
two kinds of host. On the API host (`SPACE_API_HOST`, another domain) it takes JSON: claim a name, deploy
files, list versions, roll back. On every other `<label>.<suffix>` it serves a site: the
live version at `<name>.<suffix>`, any retained version at `<version>--<name>.<suffix>`.
Bytes land on local disk first (so a deploy is live the moment it returns). A worker copies
the live version and the last two that were live to pinthis as one folder pin each — once a
version has been live ten minutes, a site's first at once — reads the copy back to verify it,
and releases pins retention no longer wants only after their replacement is verified. Every
other retained version stays on this server. A stored version's local copy is a cache: it
can be evicted under a byte budget and restored from the storage gateway, verified against
the sha256 recorded at upload.

## Run it

```
export SPACE_PUBLIC_SUFFIX=agentwire.space      # sites
export SPACE_API_HOST=space.agentwire.run       # the API, on a different registrable domain
export SPACE_STATE=/var/lib/agentwire-space
export SPACE_PINTHIS_ORIGIN=https://pinthis.cloud
export SPACE_PINTHIS_TOKEN=...            # pinthis service token (docs/SERVICE-MODE.md in the pinthis tree)
export SPACE_SERVICE_TOKEN=...            # >= 32 chars; what the AgentWire hub sends as X-Space-Service-Token
node service/spaced.cjs check             # validates configuration
node service/spaced.cjs                   # serves on SPACE_HOST:8790; Caddy (pds-caddy on the pinthis box) terminates TLS in front
```

Operator CLI (same binary, same env):

```
node service/spaced.cjs account new                  # -> acct:...
node service/spaced.cjs token new acct:... "matt"    # prints the token once
node service/spaced.cjs usage acct:...
```

Tests: `npm test` (`node --test service/*.test.cjs`; no network, a fake pinthis in-process).

## Configuration (all of it)

Every setting is an environment variable read in `service/config.cjs`. There are no
literals for origins, suffixes or paths anywhere else.

| variable | default | meaning |
|---|---|---|
| `SPACE_PUBLIC_SUFFIX` | required | the suffix sites live under, e.g. `agentwire.space` |
| `SPACE_API_HOST` | required | the host that answers the JSON API — on a **different registrable domain** from the sites (`space.agentwire.run`); a host under the suffix is refused at boot |
| `SPACE_PUBLIC_ORIGIN` | `https://<api host>` | the API origin printed in `llms.txt` |
| `SPACE_SITE_SCHEME` | `https` | scheme in the site URLs the API returns (`http` for local runs) |
| `SPACE_STATE` | required | writable directory: `space.db`, `blobs/`, `uploads/` |
| `SPACE_HOST` / `SPACE_PORT` | `127.0.0.1` / `8790` | bind address |
| `SPACE_TRUST_PROXY` | unset | `1` to read the client address from `X-Forwarded-For` (behind Caddy) |
| `SPACE_SERVICE_TOKEN` | unset (service mode off) | shared secret for `X-Space-Service-Token`; at least 32 characters |
| `SPACE_PINTHIS_ORIGIN` | unset (storage off) | the storage provider origin; must be on the allowlist below |
| `SPACE_PINTHIS_ALLOWED_ORIGINS` | `https://pinthis.cloud` | comma-separated origins the token may ever be sent to |
| `SPACE_PINTHIS_TOKEN` | required with origin | pinthis service token |
| `SPACE_PINTHIS_ACCOUNT_PREFIX` | `agentwire-space:` | prefix for the `X-Account-Id` sent to pinthis (one pinthis account per hosting account) |
| `SPACE_FREE_QUOTA_BYTES` | `5368709120` (5 GiB) | storage per account; pinthis's `agent` plan enforces the same figure independently |
| `SPACE_MAX_FILE_BYTES` | `268435456` (256 MiB) | largest single file |
| `SPACE_MAX_FILES_PER_VERSION` | `10000` | pinthis's own folder limit |
| `SPACE_MAX_JSON_BYTES` | `16777216` (16 MiB) | largest JSON request body (`/deploy`, `/reuse`); a JSON body is held in memory, so this × concurrent deploys bounds RSS — large sites use the streaming tar path |
| `SPACE_MAX_VERSIONS_PER_SITE` | `50` | older non-live versions are pruned beyond this |
| `SPACE_MAX_SITES_PER_ACCOUNT` | `100` | |
| `SPACE_MAX_OPEN_UPLOADS` | `8` | open upload sessions per account |
| `SPACE_UPLOAD_TTL_SECONDS` | `21600` (6 h) | an unfinished upload expires after this |
| `SPACE_CACHE_BYTES` | `21474836480` (20 GiB) | local blob budget; only blobs whose versions are all stored are evicted |
| `SPACE_PIN_KEEP` | `2` | besides the live version, how many most-recently-live versions are kept in storage |
| `SPACE_PIN_DELAY_SECONDS` | `600` | a version is copied to storage once it has been live this long (a site's first version at once) |
| `SPACE_VERIFY_FULL_BYTES` / `SPACE_VERIFY_FULL_FILES` | `16 MiB` / `32` | up to this size and count every file of a pinned version is read back and hashed; larger versions verify index.html + the largest file + a sample |
| `SPACE_VERIFY_SAMPLE` | `8` | sampled files for large versions |
| `SPACE_VERIFY_MAX_FILE_BYTES` | `32 MiB` | sampled files above this are skipped |
| `SPACE_MIN_NAME_LENGTH` / `SPACE_MAX_NAME_LENGTH` | `3` / `40` | site name length band (max 51: `<10 chars>--<name>` must fit a DNS label) |
| `SPACE_CORS_ORIGINS` | unset (no cross-origin grants) | browser origins allowed to call the API (a hub dashboard); never a hosted-site origin; no credentials |
| `SPACE_RESERVED_NAMES` | unset | extra reserved names, comma-separated (a built-in list is in `service/names.cjs`) |
| `SPACE_RATE_API_PER_MINUTE` | `300` | API requests per minute per client address |
| `SPACE_RATE_AUTH_FAIL_PER_MINUTE` | `20` | failed authentications per minute per address |
| `SPACE_WORKER_INTERVAL_MS` | `5000` | storage worker tick |
| `SPACE_PINTHIS_MIN_GAP_MS` | `750` | minimum spacing between pinthis calls (its per-IP budget is shared) |
| `SPACE_LOG_LEVEL` | `info` | |

## API summary

Authentication: `Authorization: Bearer aws_...`, or `X-Space-Service-Token` + `X-Account-Id`.

| route | |
|---|---|
| `GET /api/health` | service, version, storage backlog |
| `GET /api/account` | id, quota `{bytes, used, remaining}`, site count |
| `GET/POST /api/tokens`, `DELETE /api/tokens/<id>` | bearer tokens for this account |
| `GET/POST /api/sites` | list / claim `{name}` |
| `GET/PATCH/DELETE /api/sites/<name>` | site with versions / `{spa, password}` / delete everything |
| `POST /api/sites/<name>/deploy` | JSON `{files:[{path,encoding,content}], message?, activate?, requestKey?}` -> version; or a tar / tar.gz body (`application/x-tar` / `application/gzip`, options on the query string) for a whole build output |
| `POST /api/sites/<name>/uploads` | open an upload session `{requestKey?}` |
| `PUT /api/sites/<name>/uploads/<id>/files/<path>` | raw bytes |
| `POST /api/sites/<name>/uploads/<id>/reuse` | `{files:[{path,sha256}]}` reuse stored files |
| `POST /api/sites/<name>/uploads/<id>/finish` | `{message?, activate?}` -> version |
| `GET /api/sites/<name>/versions`, `GET .../versions/<id>` | history, one version with manifest |
| `PUT /api/sites/<name>/live` | `{version}` rollback / roll forward |
| `DELETE /api/sites/<name>/versions/<id>` | delete a non-live version |
| `GET /api/service/accounts/<id>/usage` | service token only |
| `POST /api/service/accounts/<id>/tokens` | service token only: mint a bearer token for that account |
| `DELETE /api/service/accounts/<id>?confirm=<id>` | service token only: remove every site |

## Layout

```
service/spaced.cjs     entry + CLI          service/api.cjs      JSON API
service/config.cjs     env -> config        service/serve.cjs    site serving
service/db.cjs         schema (node:sqlite) service/worker.cjs   copy versions to pinthis, retire pins
service/accounts.cjs   accounts, tokens, quota
service/sites.cjs      sites, uploads, versions, rollback
service/blobs.cjs      content-addressed local store, eviction, verified restore
service/pinthis.cjs    storage client (token pinned to the allowlisted origin)
service/names.cjs      name + path rules, runtime-build detector   service/mime.cjs  content types
service/tar.cjs        streaming tar reader (ustar, pax, GNU long names)
service/*.test.cjs     tests                service/fake-pinthis.cjs  in-process pinthis for tests
deploy/                systemd unit, Caddy site file, cert + DNS tooling, deploy script (pinthis box)
integration/hub/       the AgentWire hub adapter and the patch that selects it
```