Skip to content

Spec 026 — Hosts + agent tokens scaffolding#79

Merged
Copxer merged 3 commits into
mainfrom
spec/026-hosts-and-agent-tokens
May 1, 2026
Merged

Spec 026 — Hosts + agent tokens scaffolding#79
Copxer merged 3 commits into
mainfrom
spec/026-hosts-and-agent-tokens

Conversation

@Copxer
Copy link
Copy Markdown
Owner

@Copxer Copxer commented May 1, 2026

Closes #78

Spec: specs/phase-6-docker-hosts/026-hosts-and-agent-tokens.md

Summary

  • Lays the data + auth foundation for Phase 6 (Docker Host Agent MVP): five new tables (hosts, agent_tokens, containers, host_metric_snapshots, container_metric_snapshots), models, factories, and HostStatus / HostConnectionType enums.
  • Adds the Docker domain actions (Create/Update/Archive Host, Issue/Rotate/Revoke AgentToken) plus a small AgentTokenIssueResult DTO. Plaintext bearer tokens are generated by IssueAgentTokenAction, returned exactly once via the DTO, flashed to the session for one-shot reveal in the UI, and never persisted, never logged. The DB only ever sees sha256(plaintext).
  • Wires HostPolicy + AgentTokenPolicy (project-owner gates mirroring WebsitePolicy), Inertia controllers + routes under /monitoring/hosts/*, and the matching Vue pages (Index / Create / Edit / Show) plus an AgentTokenPanel component that handles the one-shot plaintext reveal + copy + rotate / revoke confirmations.

Test plan

  • All five migrations apply cleanly on a fresh DB and roll back cleanly (migrate:fresh + migrate:rollback --step=5).
  • Models + factories work end-to-end via php artisan tinker.
  • Settings → Monitoring → Hosts page lists hosts under the current user's projects (sibling-user data does not leak).
  • Creating a host then minting a token displays the plaintext once (via flash.agentTokenPlaintext); subsequent navigation drops it.
  • Rotating a token replaces the active row; the old plaintext's hash now belongs to a revoked row.
  • Project detail page shows a Hosts tab with an empty state (existing phase-pending placeholder; spec 028 wires the real list).
  • vendor/bin/pint --dirty clean.
  • php artisan test — 434 passing (+30 vs main).
  • npm run build succeeds.

Self-review notes

Self-review pass via superpowers:code-reviewer flagged four should-fix items, all addressed in commit f4ee30a:

  • ArchiveHostAction is now idempotent on archived_at — re-archive leaves the original timestamp pinned (with a feature test).
  • Token name length validates via a new StoreAgentTokenRequest form request (max:80) instead of being silently mb_substr'd in the controller.
  • Added the rotate mismatched-pair 404 test to mirror the existing destroy case (both endpoints share ensureBelongs).
  • Added 4 sibling-isolation feature tests covering show / edit / update / destroy. The policy is unit-tested, but a controller-level regression (e.g., dropping $this->authorize(...)) would have slipped past those — these lock the contract end-to-end.

Spec deviation: resources/js/lib/hostStyles.ts (mirrors websiteStyles.ts) replaced the planned HostStatusBadge.vue. The spec's "Files touched" list was updated to reflect the actual approach.

Two nice-to-haves deferred (in scope for a future hardening pass, not blocking):

  • Plaintext briefly lives in sessions.payload (encrypted with APP_KEY) for the redirect target's read. Known characteristic of any Laravel redirect()->with(...). A future polish could return the plaintext via useForm's onSuccess callback to skip the session round-trip.
  • Inertia caches page props in window.history.state, so a back-nav after issuing a token will re-render the plaintext from history cache. Not a cross-user leak (same browser session, same authenticated user), but a tighter UX could router.replace({ preserveState: false }) after the first paint.

Copxer added 3 commits May 1, 2026 11:32
Lay the data + auth foundation for Phase 6 (Docker Host Agent MVP).

- Migrations: hosts, agent_tokens, containers, host_metric_snapshots,
  container_metric_snapshots.
- Models, factories, HostStatus + HostConnectionType enums.
- Docker domain actions: Create/Update/Archive Host, Issue/Rotate/Revoke
  AgentToken. Plaintext token returned once via AgentTokenIssueResult and
  flashed to the session — never persisted, never logged.
- HostPolicy + AgentTokenPolicy mirror WebsitePolicy (project-owner gates).
- Routes + Inertia controllers under /monitoring/hosts/* (sibling to
  /monitoring/websites/*).
- Vue pages Index / Create / Edit / Show + AgentTokenPanel component
  surfaces the one-time plaintext via Inertia flash.
- Tests: 20 new feature tests covering CRUD, policy gating, token
  issuance / rotation / revocation, and mismatched host/token pairs.

Telemetry ingestion lands in spec 027; metric rendering in 028.
- ArchiveHostAction is now idempotent: re-archive leaves archived_at
  pinned to the original timestamp instead of overwriting.
- Replace ad-hoc mb_substr name truncation with StoreAgentTokenRequest
  form-request validation (name nullable, max:80) so overlong labels
  surface a real validation error.
- Add rotate-mismatch 404 test (mirrors the existing destroy-mismatch
  case) and assert no token state mutation on either side.
- Add 4 sibling-isolation feature tests covering show/edit/update/
  destroy (the policy is unit-tested, but a controller-level regression
  would slip past those — these lock the contract end-to-end).
- Add archive-idempotency feature test asserting archived_at is pinned
  across a second destroy call.
- Update spec Files-touched list to reflect the lib/hostStyles.ts +
  StoreAgentTokenRequest additions and the dropped HostStatusBadge.vue.

Tests: 27 → 30 in this file family, full suite 434 green. Pint clean,
build green.
@Copxer Copxer merged commit ee3dcdd into main May 1, 2026
1 check passed
@Copxer Copxer deleted the spec/026-hosts-and-agent-tokens branch May 1, 2026 19:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Spec 026 — Hosts + agent tokens scaffolding

1 participant