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:
- A Helm chart for
demarkus-server(one world). - A Helm chart for
demarkus-broker(OIDC token issuance + revocation). - A Helm chart for
demarkus-agent(hub aggregator, crawl-and-index). - Reference topology examples (Argo CD
ApplicationSet, Kustomize overlay). - Backend-agnostic observability — structured slog emission + reference configs for Datadog, OTel Collector, Vector, Fluent Bit, Grafana Alloy.
- Customer-facing documentation — installation, security/threat model, operations, upgrade path, per-provider OIDC setup, observability recipes.
- 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
WorldCRD. - 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/metricsor OTel SDK calls to the server. Note: liftingHashTokenfromserver/internal/auth/toprotocol/(Slice A) is an additive helper relocation, not a behavioral change, and is considered within the spirit of this rule. - 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/readinessexecprobes that invoke thedemarkusCLI fetching/.well-known/agent-manifest.md(always public per/architecture.md). No core change. Same pattern asredis-cli ping/pg_isready. - UDP port is a values knob. Default
6309, override viaserver.udpPort. Documented option: switch to443for VPN/middlebox-hostile networks (Cloudflare Warp Zero Trust, corp firewalls that filter non-standard UDP). Protocol default stays6309. - Wildcard TLS via cert-manager DNS-01. Single
*.<root>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: LoadBalancerprotocol: UDP; broker by standard Ingress (HTTPS). - Hub aggregator is the existing
demarkus-agentatclient/cmd/demarkus-agent/. Already implementscrawl+daemonsubcommands 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-agentlives inclient/cmd/, nottools/. It's a protocol client (usesfedcrawl,fetch,tokens,links), not a utility. Memory rule sharpened:client/cmd/= protocol clients (CLI, TUI, MCP, agent);tools/= utilities (token, publish, sync, broker).- Multi-OIDC. Broker speaks generic OIDC, not Google-specific. Provider behind a
Verifierinterface. 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
resourceVersionoptimistic 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 syncas 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
- First customer trial. Nesto (
*.library.nesto.ca) is path-B — trial waits for product. Trial runbook lands at/trials/nesto.mdwhen scoping starts.
Repository Layout
deploy/
helm/
demarkus-server/ # one-world chart
demarkus-broker/ # OIDC token broker chart
demarkus-agent/ # crawl/index agent chart
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.)
client/cmd/
demarkus-agent/ # protocol client — federation crawler (existing)
tools/
demarkus-token/ # token mint CLI (moved from server/cmd/ in Slice A)
demarkus-publish/ # publish CLI (moved from server/cmd/ in Slice A)
demarkus-broker/ # broker binary (monorepo until API stabilizes)
internal/token/ # shared mint library, extracted from demarkus-token
protocol/
auth.go # HashToken (promoted from server/internal/auth/ in Slice A)
Sub-Phases
6.0 — demarkus-agent verified + chart ✓ binary, ⏳ chart
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 publishIndexaccepted onlyokstatus; first publish returnscreated, surfaced as a misleading warning despite the publish succeeding. Now accepts both.publishIndexalways usedexpected_version=0(create-only), causing every re-publish in daemon mode to fail withconflict. Now uses-1(no check) for idempotent hub re-publish; server's no-op-on-duplicate-content prevents version churn.Makefiledid not builddemarkus-agent; the agent had to be hand-built. Now builds withmake 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.
Remaining 6.0 work — agent Helm chart 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 versionreturns 0). - No
Service(agent is outbound-only). - Pod annotations for Datadog autodiscovery; log fields documented for
mint/crawlevents.
Acceptance: helm install agent ./deploy/helm/demarkus-agent --set config.seeds[0]=mark://team-a:6309 ... produces a pod that crawls on schedule and publishes to the configured hub. Pod restart resumes from state PVC (optional; can also be ephemeral).
6.1 — demarkus-server Helm chart
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+demarkusCLI (for exec probes). - Exec liveness + readiness probes against
/.well-known/agent-manifest.md. - Resource requests/limits with sane defaults, overridable.
ServicetypeLoadBalancer,protocol: UDP, port fromserver.udpPort(default 6309). Annotations for cloud-specific LB type (NLB on AWS, etc.).
Secrets (world namespace):
<release>-tokens— TOML of SHA-256 hashes. Server-mounted.- Persistence across
helm upgradeis non-negotiable. Rendered withhelm.sh/resource-policy: keep+helm.sh/hook: pre-installand emptydata: {}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
admintoken on first install only. The broker manages all subsequent entries at runtime via the k8s API. - Without this pattern, every
helm upgradewould 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.
- Persistence across
- 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
issuancesSecret. The world'stokensSecret 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-managerCertificateresource (behind a flag) requesting*.<root>from a configuredClusterIssuer.
Bootstrap Job:
- Mints initial
admintoken on first install via the shared mint library (tools/internal/token, Slice A). helm.sh/hook: pre-installonly — does not run on upgrade.- Idempotent guard: reads the tokens Secret; skips if
adminlabel 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/updateon the named Secret,get/list/createonpods/execfor SIGHUP. Namespace-scopedRole, notClusterRole.
Tests:
helm-unittestfor templates, including a test that assertsresource-policy: keepis present on the tokens Secret and that the rendered Secret has emptydata: {}.- Kind-based integration test in CI: install chart → exec into pod → verify health → publish via CLI → verify version increments →
helm upgradewith 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/)
Full feature set, no skip-DELETE corner-cuts.
Role & topology
- Issuance authority, not a request proxy. Broker sits on the
demarkus loginpath; clients then talk to world servers directly carrying the raw token. World servers stay identity-blind; broker never seesmark://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+RoleBindingin each world's namespace, withget/patchlimited to that world'stokensSecret only. NoClusterRole. Blast radius bounded to the token Secrets it's explicitly granted.
State
Two distinct Kubernetes Secrets, never merged:
| Secret | Lives in | Contents | Reader |
|---|---|---|---|
<world>-tokens |
each world's namespace | TOML: [tokens.<label>] hash=, paths=, operations=, expires= |
demarkus-server |
<broker>-issuances |
broker namespace | JSON: label → {email, world, paths, operations, issued_at, expires} + secondary index email → [labels] |
broker only |
World servers see only hashes. Email and identity live in broker state alone. Both Secrets follow the §6.1 resource-policy: keep pattern — broker state must also survive helm upgrade of the broker chart.
Labels
Opaque IDs (e.g., usr_<random8>). Not email-derived. Reasons:
- Sanitizing emails (dots, plus signs, IDN) into TOML keys is fragile.
- Token rotation yields a new label each time; opaque IDs reflect that naturally.
- "Revoke everything for
fredrick@x" becomes a broker-state index lookup, not a label string scan.
Per-world authorization (values schema)
worlds:
- name: team-a
namespace: team-a
tokensSecret: team-a-tokens
allow:
domains: ["nesto.ca"]
groups: ["engineering"] # OIDC `groups` claim
defaultToken:
paths: ["/team-a/*"]
operations: ["read", "publish"]
expiresAfter: 24h
On demarkus login, broker evaluates the OIDC identity against every world in its config and mints one token per world the user qualifies for. Client may request narrower scope; never broader.
HashToken provenance
Promoted to protocol/ in Slice A. Broker imports protocol.HashToken to ensure byte-identical hashes to what world servers read from tokens.toml. The hash format sha256-<hex> is treated as a protocol contract, single source of truth.
Revocation
Three triggers, one cleanup index (the issuances Secret):
-
User-initiated (
demarkus token revoke <label>orrotate):DELETE /tokens/:labelcarrying OIDC ID token.- Broker confirms
entry.email == claims.email(owner check — users can only revoke their own). - Patches the world's
tokensSecret to drop the label. - Drops the entry from issuances Secret.
- SIGHUPs world server pod(s) so
tokens.tomlis re-read. (Or relies on server fsnotify if present — verify during implementation.)
-
Expiry sweeper (broker in-process ticker, default every 5 min, leader-elected via
Leaseso only one replica sweeps):- Queries issuances where
expires < now. - For each: patch the world's
tokensSecret + drop from issuances + SIGHUP.
- Queries issuances where
-
Identity lifecycle (user leaves the org):
- Default: short-lived tokens.
defaultToken.expiresAfterof 24h means stale tokens age out within a day. Nextdemarkus loginreruns OIDC; if the user's IdP account is disabled, login fails. No broker→IdP coupling needed. Same approach as AWS STS /gcloud auth login. - Backlog: SCIM lifecycle webhook (
POST /scim/v2/Users/:id) for enterprise IdPs that push deprovisioning events (Okta, Entra). Optional add when a customer asks. - Periodic IdP re-validation rejected — worse trade-off than either above (couples broker to IdP rate limits, slower than SCIM, less simple than short-lived).
- Default: short-lived tokens.
Cleanup edge cases
- Orphan in
tokens.toml(admin-minted via legacydemarkus-tokenCLI): broker never claims it. Admins manage via CLI. Broker API only operates on labels it minted. - Orphan in issuances Secret (admin hand-deleted from world tokens Secret): expiry sweeper detects drift on each pass and prunes broker-state entries whose label no longer exists in the world's TOML.
- In-flight request after revoke: a raw token already authenticated on a live QUIC connection completes its current request. Property of the capability + connection-reuse model. Documented; not fixable without core server changes.
Routes (HTTP, behind Ingress)
POST /auth/login— OIDC redirect entry point.GET /auth/callback— OIDC callback; mints tokens for every qualifying world; returns JSON{world → raw_token}. One-time response.GET /tokens— list caller's tokens from broker state (labels + metadata only, never raw tokens).DELETE /tokens/:label— revoke caller-owned token.POST /tokens/:label/rotate— revoke + mint with same scope; returns new raw token.GET /healthz,GET /readyz.
OIDC providers
Provider behind a Verifier interface. Initial: Google (via coreos/go-oidc or google.golang.org/api/idtoken). Follow-ons: Okta, Entra ID, Auth0 — config only. Group-claim availability varies per provider; doc page per provider explains required claim configuration (e.g., Entra needs the groups optional claim enabled; some providers require a userinfo call to fetch groups not in the ID token).
HA
Multi-replica. Issuance writes to k8s Secrets use resourceVersion optimistic concurrency with retry-on-conflict. Sweeper uses a coordination.k8s.io/Lease for leader election so only one replica runs the periodic loop.
Tests
- Mint flow: table-driven, mock OIDC verifier, fake k8s clientset, assert
tokensSecret + issuances Secret both updated atomically (best-effort — k8s API is two calls). - Sweeper: fake clock + fake clientset.
- RBAC permission-denied path explicitly covered (broker SA without
patchon a world's Secret → mint fails cleanly, not partial state). - Drift pruning: orphan-in-issuances case covered by sweeper test.
- Owner check on
DELETE— different OIDC subject than the entry → 403.
6.3 — demarkus-broker Helm chart
Multi-replica HA. As previously specified.
6.4 — Universe topology examples
ApplicationSet + Kustomize overlay. As previously specified.
6.5 — Observability recipes
Per-backend configs in deploy/observability/. As previously specified.
6.6 — Documentation suite
/deployment/*.md + per-chart READMEs. As previously specified.
6.7 — Release pipeline
GHCR images + OCI charts. Cosign deferred to backlog.
Sequencing
- Slice A (in flight) — token-mint library refactor;
HashTokentoprotocol/;demarkus-token+demarkus-publishtotools/;make allandpre-commit.shextended to includetools. - 6.0 chart — quick win since binary is done.
- 6.1 server chart — main piece.
- 6.2 broker binary — parallel with 6.1, design pinned (see above).
- 6.3 broker chart — after 6.2 testable.
- 6.4 topology examples.
- 6.5 observability recipes.
- 6.6 docs — incremental throughout.
- 6.7 release pipeline — final.
Rough effort: 2–4 weeks of focused work (down from 4–8 in v3 — agent binary work eliminated).
Backlog (deferred, easy to add later)
- Cosign signing of images + chart releases. Half-day CI add when wanted.
- Latency log-enrichment (
duration_msfield on request slog lines). Tiny additive change. Defer until trials show it's needed. - SCIM lifecycle webhook on the broker (
POST /scim/v2/Users/:id) for enterprise IdPs that push deprovisioning events. Adds responsiveness beyond what short-lived tokens give.
Risks
- Observability via logs ceiling. If customers want signals not derivable from current slog (latency, internal state like version counts or PVC fullness), we hit a wall. Mitigation: log enrichment is a small additive change; internal-state metrics derivable by a cluster-side sidecar that calls
LISTperiodically. - Broker secret-write blast radius. Holds k8s API creds across world namespaces. Mitigated by namespace-scoped
Roles (one per world), audit log, optionalNetworkPolicy. Per-worldRoleis non-negotiable — neverClusterRole. - OIDC provider coupling. First impl is Google; structure so second provider is a one-day add.
- Token revocation in-flight latency. SIGHUP reloads
tokens.toml, but a request already authenticated on a live QUIC connection completes. Property of model. - Helm upgrade wiping tokens. Classic templating footgun: rendering a Secret on every upgrade overwrites broker-minted content. Mitigated by
helm.sh/resource-policy: keep+pre-installhook on the world tokens Secret (and the broker issuances Secret). Regression-tested in CI via the kind integration test. - Chart proliferation. Three charts + examples + dashboards. Mitigate with shared common-labels templates.
- Trial scope creep. First customer is path-B. If feedback pulls scope back to "demo slice" mid-build, decide explicitly.
Status
Plan v6, 2026-05-11. §6.1 secrets section expanded to make the helm upgrade persistence pattern explicit (resource-policy: keep + pre-install hook + empty data: {} + regression-tested in CI). The stale <release>-token-values Secret line removed — broker state lives in the broker namespace per §6.2, never co-located with the world's tokens Secret. §6.2 broker design pinned during Slice A planning (topology = one broker per universe; state split; opaque labels; per-world authorization; revocation via DELETE + expiry sweeper + short-lived tokens; SCIM in backlog). Slice A in flight: token-mint library refactor, HashToken promotion to protocol/, demarkus-token/demarkus-publish move to tools/.