Master/agent phase 1 (inventory): per-machine agents (updater-agent) dial the master (updater-master) over WebSocket with pinned-key TLS (MASTER_KEY) and per-agent tokens (pua_…, only the hash is stored). They send hello, a full snapshot, then diffs after Docker events settle, plus a full re-inventory every 60 s.
The master stores inventory in Postgres (embedded migrations), tracks online agents in memory, and serves an htmx UI with live SSE updates (Overview, Agents with add/revoke). The UI sits behind Authentik OIDC (PKCE) with an ALLOWED_GROUP check, Postgres-backed sessions and CSRF.
Today's cmd/server and its packages are untouched, so the existing updater keeps running. Nothing can be updated from the new UI yet (phase 2+).
The UI follows the plan's minimal templates, not the Ledger design. The redesign is tracked in #18.
Notable decisions
Plan deviations are listed in docs/superpowers/plans/2026-10-12-master-agent-phase-1-inventory.md, section "Deliberate deviations". One is a known limitation: Swarm rows are filed under the cluster ID the agent reports. A leaked agent token can therefore rewrite another cluster's overview rows. This is consistent with the spec ("feed false inventory, not command machines"). Phase 2 must not route Swarm jobs on a claimed cluster ID alone.
Hardening added during review:
Revocation is re-checked after the connection registers.
A duplicate hello is rejected.
Swarm messages are accepted only from managers that report a cluster ID.
Inventory writes lock the agent row and refuse revoked agents, so revoke cleanup cannot be undone by writes already in flight.
Empty OIDC state is rejected, and the CSRF token is read from the form body only.
The agent dial has a 30 s timeout.
The token page sends Cache-Control: no-store.
Migration 0001 has a partial unique index on agents(name) WHERE revoked_at IS NULL, so a revoked agent's name can be reused. Revoking also removes the agent's Compose and container rows.
The master does OIDC discovery at startup and exits if the issuer is unreachable; Swarm restarts it.
Test plan
go test ./... passes with no Docker and no Postgres
go test -tags integration ./internal/master/store/ passes against Postgres 17 (17 tests)
go test -race passes for internal/agent/..., internal/master/... and internal/e2e
go vet ./... is clean, and go.mod still says go 1.26.3
docker build produces package-updater, updater-master and updater-agent
Manual smoke test from the plan against the real Authentik, with agents on swarm-mgr and the machine running slusk:
Login works with a user in ALLOWED_GROUP. A user outside the group gets 403.
Add agent shows a snippet. The agent connects and its services show up live.
Swarm services appear once, even with two manager agents.
Stopping and starting a container updates the overview within a few seconds.
Revoke cuts the agent off immediately, and its services leave the overview.
An expired session redirects htmx and SSE requests to login instead of injecting HTML.
## Summary
- Master/agent phase 1 (inventory): per-machine agents (`updater-agent`) dial the master (`updater-master`) over WebSocket with pinned-key TLS (`MASTER_KEY`) and per-agent tokens (`pua_…`, only the hash is stored). They send hello, a full snapshot, then diffs after Docker events settle, plus a full re-inventory every 60 s.
- The master stores inventory in Postgres (embedded migrations), tracks online agents in memory, and serves an htmx UI with live SSE updates (Overview, Agents with add/revoke). The UI sits behind Authentik OIDC (PKCE) with an `ALLOWED_GROUP` check, Postgres-backed sessions and CSRF.
- Today's `cmd/server` and its packages are untouched, so the existing updater keeps running. Nothing can be updated from the new UI yet (phase 2+).
Closes #6
The UI follows the plan's minimal templates, not the Ledger design. The redesign is tracked in #18.
## Notable decisions
- Plan deviations are listed in `docs/superpowers/plans/2026-10-12-master-agent-phase-1-inventory.md`, section "Deliberate deviations". One is a known limitation: Swarm rows are filed under the cluster ID the agent reports. A leaked agent token can therefore rewrite another cluster's overview rows. This is consistent with the spec ("feed false inventory, not command machines"). Phase 2 must not route Swarm jobs on a claimed cluster ID alone.
- Hardening added during review:
- Revocation is re-checked after the connection registers.
- A duplicate hello is rejected.
- Swarm messages are accepted only from managers that report a cluster ID.
- Inventory writes lock the agent row and refuse revoked agents, so revoke cleanup cannot be undone by writes already in flight.
- Empty OIDC state is rejected, and the CSRF token is read from the form body only.
- The agent dial has a 30 s timeout.
- The token page sends `Cache-Control: no-store`.
- Migration 0001 has a partial unique index on `agents(name) WHERE revoked_at IS NULL`, so a revoked agent's name can be reused. Revoking also removes the agent's Compose and container rows.
- The master does OIDC discovery at startup and exits if the issuer is unreachable; Swarm restarts it.
## Test plan
- [x] `go test ./...` passes with no Docker and no Postgres
- [x] `go test -tags integration ./internal/master/store/` passes against Postgres 17 (17 tests)
- [x] `go test -race` passes for `internal/agent/...`, `internal/master/...` and `internal/e2e`
- [x] `go vet ./...` is clean, and `go.mod` still says `go 1.26.3`
- [x] `docker build` produces `package-updater`, `updater-master` and `updater-agent`
- [ ] Manual smoke test from the plan against the real Authentik, with agents on `swarm-mgr` and the machine running slusk:
- [ ] Login works with a user in `ALLOWED_GROUP`. A user outside the group gets 403.
- [ ] Add agent shows a snippet. The agent connects and its services show up live.
- [ ] Swarm services appear once, even with two manager agents.
- [ ] Stopping and starting a container updates the overview within a few seconds.
- [ ] Revoke cuts the agent off immediately, and its services leave the overview.
- [ ] An expired session redirects htmx and SSE requests to login instead of injecting HTML.
- inventory: resolve the image reference via ContainerInspect when a moved
tag makes ContainerList report the image ID
- store: allow reusing a revoked agent's name (partial unique index) and
mark the revoked agent's compose/container services removed
- web: publish a services event on revoke; no-store on the one-time token page
- webauth: reject empty OIDC state; accept the CSRF form token from the body only
- agent conn: bound the WebSocket handshake to 30s
- hub: log agent_replaced when a connection replaces an older one
ApplySnapshot and UpsertService now take a FOR SHARE lock on the agent's
row and refuse with model.ErrRevoked when it is revoked. RevokeAgent's
UPDATE waits on that lock, so an in-flight snapshot or upsert can no
longer bring a revoked agent's services back after the revoke cleanup.
shcizo
merged commit 3090b6d4f0 into main2026-10-12 13:50:13 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
updater-agent) dial the master (updater-master) over WebSocket with pinned-key TLS (MASTER_KEY) and per-agent tokens (pua_…, only the hash is stored). They send hello, a full snapshot, then diffs after Docker events settle, plus a full re-inventory every 60 s.ALLOWED_GROUPcheck, Postgres-backed sessions and CSRF.cmd/serverand its packages are untouched, so the existing updater keeps running. Nothing can be updated from the new UI yet (phase 2+).Closes #6
The UI follows the plan's minimal templates, not the Ledger design. The redesign is tracked in #18.
Notable decisions
docs/superpowers/plans/2026-10-12-master-agent-phase-1-inventory.md, section "Deliberate deviations". One is a known limitation: Swarm rows are filed under the cluster ID the agent reports. A leaked agent token can therefore rewrite another cluster's overview rows. This is consistent with the spec ("feed false inventory, not command machines"). Phase 2 must not route Swarm jobs on a claimed cluster ID alone.Cache-Control: no-store.agents(name) WHERE revoked_at IS NULL, so a revoked agent's name can be reused. Revoking also removes the agent's Compose and container rows.Test plan
go test ./...passes with no Docker and no Postgresgo test -tags integration ./internal/master/store/passes against Postgres 17 (17 tests)go test -racepasses forinternal/agent/...,internal/master/...andinternal/e2ego vet ./...is clean, andgo.modstill saysgo 1.26.3docker buildproducespackage-updater,updater-masterandupdater-agentswarm-mgrand the machine running slusk:ALLOWED_GROUP. A user outside the group gets 403.