id: BB-PLAN-4H7KQ2ZP
type: plan
title: Toad Deployment API
spec: BB-SPEC-5TQ2M8HD
status: draft
Toad Deployment API
Outcome
toad runs on klops as a rootless Go service that owns release activation for enrolled Quadlet workloads.
A Luci job in an application repository can publish an image and deploy it by immutable digest, with probation, promotion, rollback, and durable status.
Decisions
Language: Go, matching Luci and one service toolchain.
Identity: dedicated rootless toad user, own Podman socket, own systemd user scope, storage under /data/toad.
Enrolled workloads run inside the toad user scope, so no root, sudo, polkit, or foreign session bus is needed.
Durable state is file-backed with atomic rename, following Luci's storage approach; no database dependency.
Toad runs containerized; it drives the machine through the Podman HTTP API and the systemd D-Bus interface over mounted sockets, so the image needs no podman or systemctl binary.
The container uses UserNS=keep-id, so its process identity and bind-mounted files retain the dedicated host user's natural ownership; no User=0 override.
The API listens on a Unix socket for local administration and on loopback TCP for token-authenticated consumers.
Credentials are runtime state owned by toad: toad token issue|list|revoke, hashed at rest, shown once, never in OpenTofu, state, or Git.
Operator interface is the toad client in the same binary, used over existing klops SSH; no dedicated public SSH endpoint.
The read-only web surface uses existing public Caddy Basic Auth, a shared Unix socket, a bundled stock-Caddy gateway, and the private Toad network; it introduces no host TCP listener.
Deployments are operator-triggered through the CLI first; CI-driven deployment is a later decision.
Application image building is out of scope here: Luci job containers have no Podman socket and no capabilities, so they cannot build images today.
Slices
1. Promote path
Submit a digest for one enrolled test workload and reach a promoted revision.
Enrollment read from an operator-owned file; requests carry only service, digest, expected generation, idempotency key.
Durable operation and service records with atomic writes.
Rollout: pull and verify digest, stop and verify termination, activate unit, observe readiness, promote.
Generation CAS, idempotency replay, single active rollout per service.
Status and operation log endpoints.
2. Failure path
Rollout deadline, continuous-ready stability window, restart budget across the whole attempt.
Readiness attribution to expected unit invocation, container, and digest.
Automatic rollback to the retained promoted revision, retained locally.
Terminal, non-looping failed rollback.
Post-promotion failures alert without rollback.
3. Credentials and transports
Credential store with hashed tokens, <id>.<secret> format, and immediate revocation.
Unix socket listener with permission-based local administration; loopback TCP for service tokens.
Token administration refused over TCP.
Enrollment drops all credential material.
4. Operator client
toad status|deploy|rollback|log against the API with TOAD_ENDPOINT and a service token.
deploy defaults to digest-and-generation idempotency, waits for a terminal phase, prints the operation log, and exits non-zero unless promoted.
rollback aborts the active probation and waits for the restored revision.
5. Recovery
Reconciliation of persisted intent against host state at start.
Single-writer lock across processes and restarts.
Deadlines that do not reset on restart; monitoring gaps that do not accumulate healthy time.
Boot ordering that prevents an unreconciled candidate from running.
Fault injection at each mutation boundary plus host reboot during probation.
6. Host layer on Podman and systemd APIs
Replace binary execution with the Podman HTTP API for pull, image inspect, and container inspect.
Replace systemctl with systemd D-Bus calls for reload, start, stop, and restart count.
Keep the existing rollout.Host seam so rollout tests stay unchanged.
7. Host deployment
OpenTofu: toad user setup mirroring setup-ci-user.sh, image build, Quadlet with the four mounts, enrollment rendering, host wrapper script.
Fence existing automation: rsync --delete, group restarts, weekly GC, and AutoUpdate must not touch enrolled units or retained images.
just check, just tofu plan, human-approved just apply.
8. CI handoff, deferred
Blocked on two prior decisions, and not required for the plan's outcome:
where application images are built, since Luci cannot build them today;
whether Luci gains one generic deploy adapter plus a service-scoped token, or deployment stays an operator action.
If taken, Luci speaks plain HTTP to the API, learns no application semantics, and surfaces only sanitized, operation-scoped output.
9. Enroll applications
Enroll the first application, then a second one, to prove enrollment carries no per-application code.
Each enrollment moves its workload into the toad scope with stable volumes and secret references.
Record the backward-compatible migration rule every enrolled application accepts for the retained rollback window.
Prove publish, deploy, probation, promotion, and rollback from each application's own repository.
10. Read-only web surface and ingress
Responsive service cards and selectable detail for deployment state, live readiness, sanitized configuration, container state, systemd user-unit properties, recent operations, and highlighted operation logs.
Nugu styling, bundled Nerd Font symbols, copy controls, and no mutation controls.
Never expose raw Quadlets, environment values, host journal entries, generic host metrics, token material, or host paths.
Bundled stock-Caddy gateway binds only the shared Unix socket, accepts only the configured hostname, routes Toad over the private network, and returns 404 otherwise.
Existing public Caddy mounts only the shared socket directory read-only, applies a dedicated Basic Auth credential, and adds no relay port beyond existing HTTPS.
Verification
Each slice ends with the complete services/toad Go test suite plus the behavior it introduces, exercised against a real rootless Podman where the behavior depends on it.
Slices 7 to 10 additionally require just check, a reviewed just tofu plan, and live deployment evidence before any claim of deployment.
---
id: BB-PLAN-4H7KQ2ZP
type: plan
title: Toad Deployment API
spec: BB-SPEC-5TQ2M8HD
status: draft
---
# Toad Deployment API
## Outcome
`toad` runs on klops as a rootless Go service that owns release activation for enrolled Quadlet workloads.
A Luci job in an application repository can publish an image and deploy it by immutable digest, with probation, promotion, rollback, and durable status.
## Decisions
- Language: Go, matching Luci and one service toolchain.
- Identity: dedicated rootless `toad` user, own Podman socket, own systemd user scope, storage under `/data/toad`.
- Enrolled workloads run inside the `toad` user scope, so no root, sudo, polkit, or foreign session bus is needed.
- Durable state is file-backed with atomic rename, following Luci's storage approach; no database dependency.
- Toad runs containerized; it drives the machine through the Podman HTTP API and the systemd D-Bus interface over mounted sockets, so the image needs no `podman` or `systemctl` binary.
- The container uses `UserNS=keep-id`, so its process identity and bind-mounted files retain the dedicated host user's natural ownership; no `User=0` override.
- The API listens on a Unix socket for local administration and on loopback TCP for token-authenticated consumers.
- Credentials are runtime state owned by toad: `toad token issue|list|revoke`, hashed at rest, shown once, never in OpenTofu, state, or Git.
- Operator interface is the `toad` client in the same binary, used over existing klops SSH; no dedicated public SSH endpoint.
- The read-only web surface uses existing public Caddy Basic Auth, a shared Unix socket, a bundled stock-Caddy gateway, and the private Toad network; it introduces no host TCP listener.
- Deployments are operator-triggered through the CLI first; CI-driven deployment is a later decision.
- Application image building is out of scope here: Luci job containers have no Podman socket and no capabilities, so they cannot build images today.
## Slices
### 1. Promote path
Submit a digest for one enrolled test workload and reach a promoted revision.
- `services/toad` module, `toad serve`, loopback HTTP API, per-service token.
- Enrollment read from an operator-owned file; requests carry only service, digest, expected generation, idempotency key.
- Durable operation and service records with atomic writes.
- Rollout: pull and verify digest, stop and verify termination, activate unit, observe readiness, promote.
- Generation CAS, idempotency replay, single active rollout per service.
- Status and operation log endpoints.
### 2. Failure path
- Rollout deadline, continuous-ready stability window, restart budget across the whole attempt.
- Readiness attribution to expected unit invocation, container, and digest.
- Automatic rollback to the retained promoted revision, retained locally.
- Terminal, non-looping failed rollback.
- Post-promotion failures alert without rollback.
### 3. Credentials and transports
- Credential store with hashed tokens, `<id>.<secret>` format, and immediate revocation.
- Unix socket listener with permission-based local administration; loopback TCP for service tokens.
- Token administration refused over TCP.
- Enrollment drops all credential material.
### 4. Operator client
- `toad status|deploy|rollback|log` against the API with `TOAD_ENDPOINT` and a service token.
- `deploy` defaults to digest-and-generation idempotency, waits for a terminal phase, prints the operation log, and exits non-zero unless promoted.
- `rollback` aborts the active probation and waits for the restored revision.
### 5. Recovery
- Reconciliation of persisted intent against host state at start.
- Single-writer lock across processes and restarts.
- Deadlines that do not reset on restart; monitoring gaps that do not accumulate healthy time.
- Boot ordering that prevents an unreconciled candidate from running.
- Fault injection at each mutation boundary plus host reboot during probation.
### 6. Host layer on Podman and systemd APIs
- Replace binary execution with the Podman HTTP API for pull, image inspect, and container inspect.
- Replace `systemctl` with systemd D-Bus calls for reload, start, stop, and restart count.
- Keep the existing `rollout.Host` seam so rollout tests stay unchanged.
### 7. Host deployment
- OpenTofu: `toad` user setup mirroring `setup-ci-user.sh`, image build, Quadlet with the four mounts, enrollment rendering, host wrapper script.
- Fence existing automation: `rsync --delete`, group restarts, weekly GC, and `AutoUpdate` must not touch enrolled units or retained images.
- `just check`, `just tofu plan`, human-approved `just apply`.
### 8. CI handoff, deferred
Blocked on two prior decisions, and not required for the plan's outcome:
- where application images are built, since Luci cannot build them today;
- whether Luci gains one generic deploy adapter plus a service-scoped token, or deployment stays an operator action.
If taken, Luci speaks plain HTTP to the API, learns no application semantics, and surfaces only sanitized, operation-scoped output.
### 9. Enroll applications
- Enroll the first application, then a second one, to prove enrollment carries no per-application code.
- Each enrollment moves its workload into the `toad` scope with stable volumes and secret references.
- Record the backward-compatible migration rule every enrolled application accepts for the retained rollback window.
- Prove publish, deploy, probation, promotion, and rollback from each application's own repository.
### 10. Read-only web surface and ingress
- Responsive service cards and selectable detail for deployment state, live readiness, sanitized configuration, container state, systemd user-unit properties, recent operations, and highlighted operation logs.
- Nugu styling, bundled Nerd Font symbols, copy controls, and no mutation controls.
- Never expose raw Quadlets, environment values, host journal entries, generic host metrics, token material, or host paths.
- Bundled stock-Caddy gateway binds only the shared Unix socket, accepts only the configured hostname, routes Toad over the private network, and returns 404 otherwise.
- Existing public Caddy mounts only the shared socket directory read-only, applies a dedicated Basic Auth credential, and adds no relay port beyond existing HTTPS.
## Verification
Each slice ends with the complete `services/toad` Go test suite plus the behavior it introduces, exercised against a real rootless Podman where the behavior depends on it.
Slices 7 to 10 additionally require `just check`, a reviewed `just tofu plan`, and live deployment evidence before any claim of deployment.