Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
README.md

Preview bot on vine #

Deployed on 2026-10-03. Public TLS and rejection responses verified; status page remained HTTPS 200. See fleet evidence records/2026-10-03-preview-bot-vine/. Approved application source: efda41e9f048abad4fc3ea46b3c5a37e5c3f4264. The checked-in preview-bot.container pins the deployed manifest digest. Startup authenticated wisp.place as did:plc:7puq73yz2hkvbcpdhnsze2qw. Initial peak service memory was 43,839,488 bytes; OOM false, zero restarts. 104 focused tests passed (zero failures). An ephemeral read-only PostgreSQL probe using the same private environment and container network verified a replica connection, transaction_read_only=on, SELECT privilege, and 752 domains. Live claim-owner rejection remains unverified: it needs a genuine PR pipeline fixture. Startup alone opens the database pool lazily. Build the existing apps/preview-bot/Dockerfile from a clean archive of that commit, not a dirty checkout or the Effect migration branch. Vine is Fedora 44 amd64, using rootful Podman quadlets, independently of Komodo releases.

Ownership and dependencies #

The bot posts AT Protocol comments using the wisp.place account. It reads domains.domain and domains.did in PostgreSQL; prefer a role granted only SELECT on those columns, via a mesh HAProxy endpoint. It does not migrate or write the database. Upstream PDS identity/login must succeed before it listens. Requests also depend on repo-owner PDS records, Spindle pipelines, pull records, and the preview hosting endpoint. BASE_HOST=wisp.place controls claim lookups; PREVIEW_HOST=preview.wisp.place controls preview URLs, not the bot's public hostname.

Secrets belong only in /opt/preview-bot/secrets.env, root-owned mode 0600, inside a 0700 directory. Populate from a private operator-provided path, never chat, git, command arguments, or captured environment output. See the example for required variable names. No persistent bot volume is required: comments are stored in the PDS, session and rate limits are in memory (reset on restart).

Install gates #

  1. Verify vine identity, mesh, free memory/disk, and status.wisp.place HTTPS 200. Preserve /opt/wisp-status and existing Caddy routes. Its daily local backup includes status sqlite/config and Caddy, not bot secrets or an off-host backup.
  2. Build linux/amd64 away from vine, push a commit-specific tag to the mesh registry, record the resulting manifest digest and image ID. Explicitly pull on vine with podman pull --tls-verify=false IMAGE@sha256:DIGEST (the registry intentionally uses HTTP over WireGuard). If the build host Docker daemon cannot push HTTP, stream docker save through SSH to podman load on vine, then podman push --tls-verify=false to the registry; do not change global daemon security settings for this one deployment. The quadlet uses the exact digest and Pull=never, so no host-wide registry configuration is required.
  3. Install the populated quadlet as /etc/containers/systemd/preview-bot.container; systemctl daemon-reload, then systemctl start preview-bot. Quadlet's [Install] handles boot startup; do not enable the generated service. This bounds bot RAM to 256 MiB and binds only loopback port 3004. Confirm mesh DB connectivity from the container; Podman's default subnet is 10.223.0.0/24 (pool 10.223.0.0/16), avoiding mesh overlap.
  4. Verify loopback behavior below before exposing a route. Copy Caddyfile with metadata to a timestamped rollback file outside the daily rotating backup. Append Caddyfile.fragment to /opt/caddy/Caddyfile, preserving its inode: Caddy bind-mounts this individual file, so atomic replacement can leave the container seeing the previous inode. Validate using podman exec caddy caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile. Only on success, reload with the same command replacing validate with reload. Do not restart Caddy or gatus. DNS must point at vine before normal ACME issuance/public verification; DNS is managed separately.
  5. Verify public TLS, rejection responses, and status.wisp.place still 200. Record digest, container/service state, response bodies, and memory use.

Verification and security contract #

There is no health endpoint at this revision. GET /health returns 404 {"error":"not-found"}. GET /v1/preview returns 405 {"error":"method-not-allowed"} and proves the handler is listening, not DB readiness. Do not describe either as a 200 health check.

There is no inbound bearer-token authentication for webhook delivery either. POST /v1/hook accepts the webhook service's JSON envelope, but trusts no event facts: it validates the DID, collection, event, and pull rkey, then resolves the owner's repo record and matching pull-request pipeline from the configured authorities before running the same preview verification and comment path. Ordinary site rkeys and deletes return 200 {"status":"ignored"} so they are not retried. A pipeline that has not appeared yet, or a preview not serving yet, returns 409 for webhook retry; upstream failures return 502 and permanent validation/ownership failures return 422.

There is no inbound bearer-token authentication. POST /v1/preview accepts JSON identifiers only, independently verifies repo/pipeline/pull/claim ownership, and only then checks preview serving and writes a comment. Bad JSON/identifier shape returns 400, wrong media type 415, and a foreign/missing claim returns 422 claim-not-owned after the earlier checks pass. Bot PDS login failure is a startup failure, not an HTTP 401. A fake bearer token must not be interpreted as authorizing anything. Rate limits are bounded to 10,000 keys per limiter.

Run bun test --isolate apps/preview-bot on the pinned source. The focused suite covers wrong claim owner, forged/fork pipelines, malformed requests, upstream failures, rate limits, and comment writes. A live ownership rejection requires a genuine repo/pull/pipeline fixture reaching that check; malformed requests alone do not prove it. Avoid successful production comment writes without an approved disposable pull request.

Rollback #

Restore the timestamped Caddyfile contents in place, validate, then reload Caddy. Confirm status HTTPS 200. Stop preview-bot.service, remove only the new quadlet, and systemctl daemon-reload. Keep the digest and private env for a retry, or remove secrets explicitly when decommissioning. No DB restore is needed. If a route was never activated, skip Caddy changes entirely.