# Plan: Universe Deployment Ship the enterprise-grade deployment for demarkus. The deliverable *is* the deployment: a customer's ops team installs a Helm chart, runs a broker, runs an agent, and has a federated demarkus universe their org can use. Any company evaluating demarkus (their internal "POC") installs the same product an established customer runs in production. There is no separate "POC slice" — the slice mentality is rejected. We build it once, right, and customers trial the real thing. ## Goal Deliver a complete, supportable, production-grade Kubernetes deployment package for demarkus, including: 1. A Helm chart for `demarkus-server` (one world). 2. A Helm chart for `demarkus-broker` (OIDC token issuance + revocation). 3. A Helm chart for `demarkus-agent` (hub aggregator, crawl-and-index). 4. Reference topology examples (Argo CD `ApplicationSet`, Kustomize overlay). 5. Backend-agnostic observability — structured slog emission + reference configs for Datadog, OTel Collector, Vector, Fluent Bit, Grafana Alloy. 6. Customer-facing documentation — installation, security/threat model, operations, upgrade path, per-provider OIDC setup, observability recipes. 7. A release pipeline producing images and chart releases consumable from GHCR. The same artifacts power first-customer trial and steady-state operations. ## Non-Goals (Phase 7+ territory) - Multi-replica worlds with shared storage (RWX / object backend). - Cross-cluster universe federation. - Operator with a `World` CRD. - Hosted / managed SaaS. - Non-markdown content. ## Constraints - **No core protocol/server changes.** Period. Per prior precedent (Claude Code plugin, Obsidian plugin, `feedback_plugin_scope.md`). Observability is achieved by log derivation in a collector, not by adding `/metrics` or OTel SDK calls to the server. Note: in Slice A we promoted `HashToken` and the token-mint library to `protocol/` (under `protocol/auth.go` and `protocol/token/`). These are additive helper relocations consumed by server + CLI + future broker; no wire-protocol or server behavior changed. - Capability-based auth model is non-negotiable. The server never learns identity — only labels. - Markdown-only scope is non-negotiable. ## Decisions (resolved during planning) - **Health probes use `exec`, not HTTP.** Both charts ship liveness/readiness `exec` probes that invoke the `demarkus` CLI fetching `/.well-known/agent-manifest.md` (always public per `/architecture.md`). No core change. Same pattern as `redis-cli ping` / `pg_isready`. - **UDP port is a values knob.** Default `6309`, override via `server.udpPort`. Documented option: switch to `443` for VPN/middlebox-hostile networks (Cloudflare Warp Zero Trust, corp firewalls that filter non-standard UDP). Protocol default stays `6309`. - **Wildcard TLS via cert-manager DNS-01.** Single `*.` cert covers all worlds + broker hostnames. HTTP-01 cannot work for worlds (no HTTP); DNS-01 is the standard path for QUIC services. - **DNS topology.** One A record per world + one for the broker, all under the same wildcard zone. Each world exposed by `Service: LoadBalancer` `protocol: UDP`; broker by standard Ingress (HTTPS). - **Hub aggregator is the existing `demarkus-agent`** at `client/cmd/demarkus-agent/`. Already implements `crawl` + `daemon` subcommands with TOML config, multi-worker crawl, per-host token auth, per-server + aggregated index publishing. Verified end-to-end on 2026-05-11. **Phase 5 is no longer a blocking prerequisite.** - **`demarkus-agent` lives in `client/cmd/`, not `tools/`.** It's a protocol client (uses `fedcrawl`, `fetch`, `tokens`, `links`), not a utility. Memory rule sharpened: `client/cmd/` = protocol clients (CLI, TUI, MCP, agent); `tools/` = utilities (broker, future sync/ops binaries). - **Token-mint library lives at `protocol/token/`, not `tools/internal/token/`** (revised mid-Slice-A after CodeRabbit review). `protocol/` is reachable by both `server/` (where `demarkus-token` CLI lives) and `tools/` (where the broker will live), which `tools/internal/` was not. `protocol/` already owns the `HashToken` byte-shape contract, so it's the natural home for the on-disk token primitives that must round-trip identically across CLI mints and broker mints. - **`demarkus-token` CLI stays at `server/cmd/demarkus-token/`** for now (rewired to import `protocol/token`). Moving the binary to `tools/` was deferred to a follow-up that splits the release pipeline (server release currently ships demarkus-token; tools has no release cadence yet). Same applies to `demarkus-publish` — still at `server/cmd/demarkus-publish/`, deferred because hoisting it requires also hoisting `server/internal/store/` out of internal-package scope. - **Multi-OIDC.** Broker speaks generic OIDC, not Google-specific. Provider behind a `Verifier` interface. Google validated first; Okta, Entra ID, Auth0 follow with config only. - **Workload Identity (GKE) as a broker option.** Values flag annotates the broker ServiceAccount with the GSA mapping. Off by default; on for GKE customers wanting no static SA keys. Not applicable to world servers. - **Broker is HA.** Multi-replica with `resourceVersion` optimistic concurrency on Secret writes, retry on conflict. - **Observability is log-derived, backend-agnostic.** demarkus already emits structured slog. Charts ship Datadog autodiscovery annotations + reference configs for OTel Collector, Vector, Fluent Bit, Grafana Alloy. Customer's SRE picks the agent/backend. Latency histograms deferred — depends on what current slog includes per request; revisit during trials if needed. - **Backup/DR is documented, not built.** The chart deliberately does not run backup CronJobs. Operations doc covers Velero, VolumeSnapshot, and `demarkus-agent sync` as DR options. - **Image hosting:** `ghcr.io/latebit-io/{demarkus-server,demarkus-broker,demarkus-agent}`. - **Chart registry:** OCI charts in GHCR. - **Cosign signing:** deferred to backlog. Half-day CI add when wanted. ## Open Questions 1. **First customer trial.** Nesto (`*.library.nesto.ca`) is path-B — trial waits for product. Trial runbook lands at `/trials/nesto.md` when scoping starts. 2. **CLI relocation to `tools/`.** Follow-up to Slice A. Requires `tools/.goreleaser.yml`, a `release-tools` job in the workflow, an `install.sh` block fetching the tools archive, and updates to plugin scripts + helm image build. Acceptable to defer until the release-pipeline work has its own dedicated PR. ## Repository Layout Reflects state as of Slice B merge (2026-05-11): ``` deploy/ helm/ demarkus-server/ # one-world chart (Phase 6.1) — shipped PR #107 demarkus-broker/ # OIDC token broker chart (Phase 6.3) demarkus-agent/ # crawl/index agent chart (Phase 6.0) — shipped PR #106 k8s/ examples/ applicationset.yaml # Argo CD ApplicationSet over a worlds: list kustomize-overlay/ # Kustomize alternative observability/ datadog/ # autodiscovery annotations + dashboard JSON otel-collector/ # collector config recipes vector/ # vector config recipes fluent-bit/ # fluent-bit parser + filter recipes scripts/ # operator helpers (cert pre-check, MTU probe, etc.) protocol/ auth.go # HashToken — sha256- contract (Slice A) token/ # Generate, ReadFile, AppendEntry, WriteFile, # FormatEntry, flock helpers (Slice A) + # AppendBytes, RemoveBytes in-memory helpers # (Slice B) for callers that round-trip TOML # through k8s Secrets instead of disk. # Shared by CLI and broker; on-disk path is # atomic temp+rename, fsync data + parent dir # for durability, advisory flock(2) for # cross-process serialization, quoted-key # handling for non-bare TOML labels. server/cmd/ demarkus-server/ # the server binary (existing) demarkus-token/ # token mint CLI — uses protocol/token # (Slice A: rewired, not moved; tools/ # relocation deferred — see Open Questions) demarkus-publish/ # publish CLI (existing; deferred move) client/cmd/ demarkus-agent/ # protocol client — federation crawler (existing) tools/ demarkus-broker/ # broker binary (Slice B, PR #109) — imports # protocol/token for byte-identical hashes # (Phase 6.2). main.go + internal/broker/ # { config, session, oidc, issuer, server, # labels }. Single-world OIDC mint flow # merged via PR #109; Slice C splits the # remaining work into C.1 authz + owner-test, # C.2 sweeper + leader-election + RBAC test, # C.3 rotate, C.4 rate limit. SCIM webhook # stays in backlog. ``` ## Sub-Phases ### 6.0 — `demarkus-agent` verified + chart ✓ binary, ✓ chart (merged PR #106) **Binary status (2026-05-11):** existing `demarkus-agent` verified end-to-end against a 3-server smoke test (2 team worlds + 1 hub). Crawls, builds aggregated and per-server indexes, publishes to hub. Two bugs fixed during verification: - `fedcrawl/crawl.go publishIndex` accepted only `ok` status; first publish returns `created`, surfaced as a misleading warning despite the publish succeeding. Now accepts both. - `publishIndex` always used `expected_version=0` (create-only), causing every re-publish in daemon mode to fail with `conflict`. Now uses `-1` (no check) for idempotent hub re-publish; server's no-op-on-duplicate-content prevents version churn. - `Makefile` did not build `demarkus-agent`; the agent had to be hand-built. Now builds with `make client`. Tests in `client/internal/fedcrawl/crawl_test.go` cover all three: create + re-publish + status acceptance + per-server / aggregated modes. `bash pre-commit.sh` clean. **Agent Helm chart shipped in PR #106** at `deploy/helm/demarkus-agent/`: - `Deployment` (not StatefulSet — agent is stateless modulo state file; recoverable from next crawl). - ConfigMap holding the TOML agent config (`seeds`, `hubs`, `crawl`, `politeness`, `schedule`). - Secret holding per-host tokens for publishing to hub(s). - ServiceAccount, no special RBAC needed (no k8s API calls). - Exec liveness probe (`demarkus-agent version` returns 0). - No `Service` (agent is outbound-only). - Pod annotations for Datadog autodiscovery; log fields documented for `mint`/`crawl` events. ### 6.1 — `demarkus-server` Helm chart ✓ (merged PR #107) `deploy/helm/demarkus-server/`. Production-grade. Workload: - `StatefulSet`, 1 replica (multi-replica is Phase 7). - `volumeClaimTemplates` — each world owns its PVC. Never a shared PVC. - Container image bundles `demarkus-server` + `demarkus` CLI (for exec probes). - Exec liveness + readiness probes against `/.well-known/agent-manifest.md`. - Resource requests/limits with sane defaults, overridable. - `Service` type `LoadBalancer`, `protocol: UDP`, port from `server.udpPort` (default 6309). Annotations for cloud-specific LB type (NLB on AWS, etc.). Secrets (world namespace): - `-tokens` — TOML of SHA-256 hashes. Server-mounted. - **Persistence across `helm upgrade` is non-negotiable.** Rendered with `helm.sh/resource-policy: keep` + `helm.sh/hook: pre-install` and empty `data: {}` in the template body. Helm creates the Secret on first install and never overwrites it on subsequent upgrades; broker writes via k8s API are preserved. - Bootstrap Job (below) seeds the initial `admin` token on first install only. The broker manages all subsequent entries at runtime via the k8s API. - **Without this pattern, every `helm upgrade` would silently wipe every broker-minted token.** This is the single most important Helm correctness property in the chart and must be unit-tested. - kubelet propagation: Secret data changes propagate to the mounted file with a delay (default ~60s). Server re-reads via SIGHUP (preferred) or fsnotify; verify which during chart implementation. - Broker state (raw tokens are never persisted; email→label mappings and issuance metadata) lives in the **broker namespace**, not the world namespace. See §6.2 for the broker's `issuances` Secret. The world's `tokens` Secret holds only hashes and is the sole source of truth the server reads. Persistence boundary recap: pod restarts, node failures, control-plane restarts → fine. `helm upgrade` → fine *iff* `resource-policy: keep`. `kubectl delete namespace` → everything gone (operations doc covers Velero / VolumeSnapshot recovery). `helm uninstall` without `keep` annotation → tokens Secret gone; with `keep`, retained. Auth + TLS: - TLS Secret mounted via `volumeMounts`; cert/key paths via flags. - Optional `cert-manager` `Certificate` resource (behind a flag) requesting `*.` from a configured `ClusterIssuer`. Bootstrap Job: - Mints initial `admin` token on first install via `protocol/token.Generate` + `protocol/token.AppendEntry` (Slice A). - `helm.sh/hook: pre-install` only — does not run on upgrade. - Idempotent guard: reads the tokens Secret; skips if `admin` label already exists. - SIGHUPs pod after writing (no-op on first install before pod is up). Observability: - Pod annotations for Datadog autodiscovery. - Reference configs for OTel Collector, Vector, Fluent Bit in `deploy/observability/`. - slog output already structured; no chart-side instrumentation needed. RBAC: - Bootstrap Job SA with `get/update` on the named Secret, `get/list/create` on `pods/exec` for SIGHUP. Namespace-scoped `Role`, not `ClusterRole`. Tests: - `helm-unittest` for templates, including a test that asserts `resource-policy: keep` is present on the tokens Secret and that the rendered Secret has empty `data: {}`. - Kind-based integration test in CI: install chart → exec into pod → verify health → publish via CLI → verify version increments → `helm upgrade` with a changed value → re-verify token-based auth still works (regression guard against the upgrade-wipe footgun). ### 6.2 — `demarkus-broker` binary (`tools/demarkus-broker/`) Slice B (single-world OIDC mint flow) **merged as PR #109** (commit `2a6aae6`, 2026-05-11). Slice C (groups-claim authorization, expiry sweeper, leader election, rotate, rate limit, SCIM webhook) is open and split into four landed PRs (C.1–C.4) — see §Sequencing for the ordering and §Status for the agreed sub-slicing. #### Role & topology - **Issuance authority, not a request proxy.** Broker sits on the `demarkus login` path; clients then talk to world servers directly carrying the raw token. World servers stay identity-blind; broker never sees `mark://` requests. Capability model preserved. - **One broker per universe (cluster) by default.** Single OIDC client registration, single `worlds:` list in values, single issuance state Secret. Multiple brokers only when (a) multiple OIDC providers must coexist, (b) hard tenant isolation between orgs sharing a cluster, or (c) Phase-7 geo split. N>1 is supported but not the common case. - Broker SA holds a namespace-scoped `Role` + `RoleBinding` in **each world's namespace**, with `get/patch` limited to **that world's `tokens` Secret only**. No `ClusterRole`. Blast radius bounded to the token Secrets it's explicitly granted. #### State Two distinct Kubernetes Secrets, never merged: | Secret | Lives in | Contents | Reader | |---|---|---|---| | `-tokens` | each world's namespace | TOML: `[tokens.