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 #
- Verify vine identity, mesh, free memory/disk, and status.wisp.place HTTPS 200.
Preserve
/opt/wisp-statusand existing Caddy routes. Its daily local backup includes status sqlite/config and Caddy, not bot secrets or an off-host backup. - 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, streamdocker savethrough SSH topodman loadon vine, thenpodman push --tls-verify=falseto the registry; do not change global daemon security settings for this one deployment. The quadlet uses the exact digest andPull=never, so no host-wide registry configuration is required. - Install the populated quadlet as
/etc/containers/systemd/preview-bot.container;systemctl daemon-reload, thensystemctl 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. - Verify loopback behavior below before exposing a route. Copy Caddyfile with
metadata to a timestamped rollback file outside the daily rotating backup.
Append
Caddyfile.fragmentto/opt/caddy/Caddyfile, preserving its inode: Caddy bind-mounts this individual file, so atomic replacement can leave the container seeing the previous inode. Validate usingpodman exec caddy caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile. Only on success, reload with the same command replacingvalidatewithreload. Do not restart Caddy or gatus. DNS must point at vine before normal ACME issuance/public verification; DNS is managed separately. - 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.