Journal — 2026-05-11
2026-05-11 — Universe deployment planning + agent verification
Worked through the production-deployment story for demarkus end-to-end with Fritz. Several reframes happened in sequence:
Reframe 1: "POC" means customer trial, not Claude-built slice. Initial framing treated the work as a one-week demo cut. Fritz pushed back: the deliverable is the productized k8s deployment itself (chart, broker, agent), what's "POC" is a customer (like nesto) trialing the solid product on their own infra. No corner-cuts. This collapsed the earlier /plans/poc-deployment.md into /plans/universe-deployment.md and the former was archived.
Reframe 2: No core changes, even for observability. Initially proposed a /metrics endpoint on server + broker for Prometheus. Fritz refused per the existing rule. Considered OTel SDK — same problem, still core instrumentation. Landed on log-derived metrics via collector sidecar/DaemonSet (Datadog Agent, OTel Collector, Vector, Fluent Bit, Grafana Alloy — customer picks one). Charts ship structured slog + autodiscovery annotations + reference configs. Backend-agnostic. Customer's SRE wires their backend. Captured in plan v3.
Reframe 3: Phase 5 agent was already built. Path A was "land Phase 5 agent first." On recon discovered client/cmd/demarkus-agent/ + client/internal/fedcrawl/ already implements crawl + daemon + per-host token auth + aggregated/per-server hub indexing. The roadmap saying Phase 5 was "IN PROGRESS" was misleading — the agent's been there. Memory note from 11 days ago said new utility binaries live in tools/, but the agent is at client/cmd/. Resolved by sharpening the rule: client/cmd/ = protocol clients (uses fedcrawl/fetch/tokens), tools/ = utilities (token mint, direct-store-write, sidecars). Agent stays where it is; future demarkus-broker and demarkus-sync go in tools/.
Smoke test surfaced two bugs in fedcrawl/crawl.go publishIndex:
- Status check only accepted
protocol.StatusOK. First publish returnscreated, which was emitted as a misleading warning even though the publish succeeded. Fix: accept bothokandcreated. - Always published with
expected_version=0(create-only), so every re-publish in daemon mode failed withconflict. Fix: use-1(no check). Hub indexes are regenerated whole-doc each crawl; server's no-op-on-duplicate-content prevents version churn. The TODO comment in the original code anticipated this.
Also discovered the Makefile didn't build demarkus-agent — only demarkus, demarkus-tui, demarkus-mcp. Added it to make client.
Tests added in client/internal/fedcrawl/crawl_test.go: TestPublishIndex covers status acceptance and the -1 expected-version invariant; TestPublishIndexRePublish covers the daemon-mode case (first call created, second call ok, both succeed); TestPublishToHubs covers aggregated and per-server modes. bash pre-commit.sh clean.
End-to-end smoke verified: 3-server local setup (team-a, team-b, hub), agent crawl publishes a hub index with all 4 docs, re-runs without conflict, only 1 version on the hub (no-op kicked in when content was identical between passes). Daemon mode is now safe to run on a schedule.
Net plan state at end of session: /plans/universe-deployment.md v4. Phase 5 prereq cleared. 6.0 reduced to the agent Helm chart (binary done). Next: 6.0 chart, then 6.1 server chart. Estimate down from 4-8 weeks to 2-4 weeks.
Bug-fix changes are staged on main (working tree), not yet committed — Fritz commits himself per the project rule.
2026-05-11 (late) — Phase 6 charts landed
Three PRs in sequence after the agent verification + bugfixes:
- #105 — fedcrawl publishIndex bugfixes (accept
created, idempotent re-publish viaexpected_version=-1), Makefile addsdemarkus-agenttomake client. Two rounds of CodeRabbit comments addressed. - #106 — Phase 6.0 demarkus-agent Helm chart. Production-grade: Deployment + ConfigMap + optional inline-tokens Secret + ServiceAccount with Workload Identity hook. Pod is non-root, read-only root FS, seccompProfile RuntimeDefault, all caps dropped. Probes intentionally omitted (outbound scheduled job). State PVC opt-in. helm-unittest suites included. Eight rounds of CodeRabbit comments addressed before merge, plus one follow-up for deterministic Secret key ordering (
sortAlpha). - #107 — Phase 6.1 demarkus-server Helm chart. StatefulSet +
volumeClaimTemplates, LoadBalancer Service on UDP/6309 (configurable port), three TLS modes (existingSecret / cert-manager Certificate / server-generated dev cert), two-Secret token bootstrap. Critical correctness pattern: both Secrets render in one file so a single$rawTokenvalue flows into both, keeping raw and hash consistent within one render pass.lookupagainst the existing token-values Secret preserves the admin token acrosshelm upgrade. Two CodeRabbit comments addressed (label-aware lookup, README markdown formatting).
Known caveat carried forward: the current server/Dockerfile ships only demarkus-server. The server chart's exec probes need /demarkus (CLI), and the future broker integration uses /demarkus-token. Multi-binary image is Phase 6.7 (release pipeline). Charts are YAML-correct; probes will fail against the current single-binary image until 6.7 lands.
State for next session: Phase 6.2 — the demarkus-broker Go binary — is the next thing. Recommended slicing into A/B/C/D PRs (library refactor → broker skeleton → install page + DELETE + rate limit + integration test → broker chart). See bootstrap notes for Slice A scope.
2026-05-11 — Slice A merged: token-mint library extracted
PR #108 (fix(server): Centralize token hashing and implement token generation) merged. First concrete delivery toward Phase 6.2 (broker).
What landed:
HashTokenpromoted fromserver/internal/auth/toprotocol/as the single source of truth for thesha256-<hex>token-hash contract. Server, CLI, and future broker all reference the same function.- New
protocol/token/package:Generate(label, paths, ops) Minted(pure, no I/O),ReadFile,AppendEntry,WriteFile,FormatEntry. Atomic temp+rename publishing,fsyncdata + parent dir for crash durability, advisoryflock(2)on a sidecar.lockfor cross-process serialization, defensiveslices.Cloneon capability slices, label quoting that handles dotted/whitespace/email-style labels correctly. server/cmd/demarkus-token/main.gorewritten to delegate toprotocol/token. ~80 lines of duplicated TOML/format/write logic deleted; the CLI inherits correctness (label quoting, atomicity, durability, locking) from the library.Makefile+pre-commit.shextended to cover thetools/module.
Architectural reversal worth flagging for future agents:
Mid-PR we chose "option C" — library at tools/internal/token/, CLI stays put with protocol.HashToken only. Plan §6.2 was updated to match. CodeRabbit then blocked approval on the grounds that the PR's stated goal ("centralize token hashing") wasn't actually being met for the CLI consumer — module-boundary rules made tools/internal/token unreachable from server/. We pivoted to "option B-prime": library at protocol/token/, CLI wired to it.
The plan at /plans/universe-deployment.md §6.2 still says the library lives at tools/internal/token/ and the repository-layout block reflects the same. That needs updating before agents picking up 6.2 (broker) get confused. Action: edit §6.2 + Repository Layout to point at protocol/token/. Broker's import path is github.com/latebit/demarkus/protocol/token from tools/demarkus-broker/.
Lessons:
- Option C was the right call for the CLI binary location (release pipeline stays unchanged). It was the wrong call for the library location. The two decisions were conflated; they should have been independent. Library placement should follow "where can all consumers reach it" first, "where does it feel architecturally clean" second.
protocol/is not just wire format — it's the shared contract layer. Anything that two modules need to agree on byte-for-byte (hash format, token TOML shape) belongs there.- CodeRabbit's "CLI doesn't use the centralized code" critique was correct. Naming a PR "centralize X" while not wiring the existing consumer creates a credibility gap.
Next per plan sequencing: 6.0 agent Helm chart (quick win — binary already verified 2026-05-11 earlier), then 6.1 server Helm chart with helm.sh/resource-policy: keep persistence pattern, then 6.2 broker binary (can import protocol/token directly).
2026-05-11 (late) — Slice B: broker scaffold + single-world mint flow
Picked up Phase 6.2 (broker binary). Plan v7 §6.0 status was stale (chart already shipped PR #106) — Fritz redirected to 6.2. Followed the A/B/C/D slicing from the prior journal entry; this is Slice B, the broker scaffold + single-world OIDC mint flow.
Decisions locked in before writing (Fritz approved):
k8s.io/client-godirect, not controller-runtime — the broker doesn't reconcile, it just CRUDs Secrets.coreos/go-oidc/v3+golang.org/x/oauth2for OIDC code flow.- Broker config in YAML (
KnownFields(true)catches typos at startup); world tokens stay TOML (protocol contract). - OIDC state cookie: signed HMAC-SHA256 over JSON
{nonce, expiresAt}payload; cookie is HttpOnly+Secure+SameSite=Lax on/auth/callbackpath only. 5-minute default TTL. - Code goes under
tools/demarkus-broker/, internal package atinternal/broker/.
Architectural reframe mid-slice: Originally planned a broker-issued session cookie for /tokens and DELETE /tokens/:label. Dropped it. Browser flow only uses the state cookie for CSRF during the OIDC dance; everything else is bearer-token authenticated using the user's OIDC ID token. Added VerifyIDToken(ctx, raw) (Claims, error) to the Verifier interface. The CLI is the primary consumer of /tokens / DELETE and already deals with token lifetimes; the browser only ever sees the one-time JSON callback response.
Touches to protocol/token/: Added two in-memory helpers AppendBytes(existing, label, *Entry) ([]byte, error) and RemoveBytes(existing, label) ([]byte, error) so the broker can do the same duplicate-check + format-or-rewrite logic as the CLI without touching disk. This is exactly the kind of additive helper the Slice A library promotion was designed for — both consumers (server/cmd/demarkus-token and tools/demarkus-broker/) read from protocol/token, byte-shape compatibility is automatic.
What landed (Slice B, ready for a PR):
tools/demarkus-broker/main.go— thin entry: config load, OIDC discovery, k8s client (in-cluster or kubeconfig), HTTP server, signal-driven graceful shutdown.tools/demarkus-broker/internal/broker/:labels.go—usr_<8 hex>opaque labels, 4 bytes of entropy + collision-retry in the issuer.config.go— YAML config with eager validation.KnownFields(true)rejects typos. Rejects zeroexpiresAfterbecause short-lived tokens are the identity-lifecycle mechanism.session.go—Signerfor the OIDC state cookie. HMAC-SHA256, constant-time signature compare. Rejects ≥16-byte keys, expired payloads, malformed envelopes.oidc.go—Verifierinterface (3 methods:AuthCodeURL,Exchange,VerifyIDToken). Production impl wrapscoreos/go-oidc+oauth2. Test double inoidc_test.gois the same interface, hard-codes claims, mocks discovery viahttptest.issuer.go—Issuer.Mint/List/Revokeagainst two Secrets: per-worldtokens.tomlSecret (tokens.tomlkey) + broker-namespaceissuances.jsonSecret. Optimistic concurrency viaresourceVersionwith retry-on-conflict up to 5. Label collisions also retry up to 5. Mint order: world Secret first (token works immediately), then issuance record (orphan-in-issuances is the documented partial-failure mode the future sweeper prunes).server.go—http.ServeMuxwith Go-1.22 method+path patterns. Routes:GET /healthz,GET /readyz,GET /auth/login,GET /auth/callback,GET /tokens,DELETE /tokens/{label}.- 87% line coverage.
helm-unittest-equivalent end-to-end tests viahttptest.NewServer+fake.Clientset+fakeVerifier.
Bugs I caught during development:
- First server-test run: every callback returned 401 "invalid state". Cause: in
newTestServer, I pinneds.clock = issuer.clockto a 2026-05-11 12:00 UTC fixed date. State cookie'sExpiresAt = clock() + 5min = 12:05 UTC. Real wall-clock during test run was hours later → cookie deemed expired byVerify. Decoupled the two clocks: server stays ontime.Now, issuer stays pinned for predictable token-expiry assertions.
Pre-commit run clean. Several gocritic + revive fixes along the way: hugeParam on Issuance, rangeValCopy on the entries slice, exported-type docstrings, http.NoBody over nil request bodies.
Carried-forward caveats for Slice C:
- No expiry sweeper, no leader-election Lease, no rotate, no rate limit, no SCIM webhook. All accepted backlog per plan §6.2.
- Authorization is domain-allowlist only. Groups claim → Slice C.
- Partial-mint behavior when multi-world iteration fails mid-loop: returns successful results + error to caller, leaves orphans in the Secrets the loop got to. Acceptable for Slice B since multi-world is mostly a Slice C concern (Slice B realistic config has one world).
client/go.modandserver/go.modpicked up cosmeticgo 1.26 → 1.26.0updates fromgo mod tidy;protocol/go.modgained 3 indirect deps (gopkg.in/check.v1,kr/pretty,rogpeppe/go-internal) surfaced by tidy. Benign — those were latent indirects, just now recorded.
Plan §6.2 staleness to fix: the section's "Token-mint library" subsection should be updated to reference protocol/token.AppendBytes and protocol/token.RemoveBytes as the in-memory primitives. The earlier list of Generate/AppendEntry/WriteFile/ReadFile/FormatEntry is still correct but doesn't mention the two new helpers the broker actually uses.
Bug-fix and code changes staged on main (working tree), not yet committed — Fritz commits himself.
2026-05-11 (very late) — Slice B merged as PR #109 (broker binary)
PR #109 (feat(broker): Implement Kubernetes-backed OIDC token broker) merged to main as 2a6aae6. The branch went through 7 rounds of CodeRabbit review (~20 comments) between the initial commit (fd18dce) and merge. Most comments produced material behavioral changes, not cosmetic fixes — worth recording the rationale so a future Slice C / hardening pass doesn't unwind them.
Security invariants baked into Issuer.Mint (do not remove without thought):
!claims.EmailVerified→ErrEmailUnverified. The productionVerifieralready rejects unverified IDs, butMintis reachable from test doubles and future Verifier impls. Belt-and-suspenders, not redundant.strings.TrimSpace(claims.Email) == ""→ErrNotAuthorized. A world with emptyAllowDomains(the "any verified user" knob) would otherwise authorize an empty/whitespace email becausedomainMatchesshort-circuits totrueon empty allow-list. Worse, that empty string would land as the owner in the issuances Secret, collapsing every future no-identity caller into a sharedList/Revokenamespace. The trim catches""," ","\t".
Correctness invariants in the mint/revoke flow:
- Global label uniqueness in
appendIssuance.removeIssuancefilters by label across the whole issuances Secret. A cross-world label collision (2^32 space is small once you accumulate worlds) would letRevoke(label)find the first matching entry, remove it from world A's tokens.toml, then drop both issuance entries — leaving world B's token live and untracked. Fix:appendIssuancescans for the label and returnstoken.ErrLabelExists;mintForWorldtreats that error as a collision, rolls back the world-Secret write, andcontinues the retry loop. Tested inTestMintCrossWorldLabelCollisionRetries. - Rollback context detaches from caller. When
appendIssuancefails afterappendToWorldSecretsucceeded, the world Secret has an active token with no ownership record. Rollback runs oncontext.WithoutCancel(ctx)+5s WithTimeoutso a client hangup mid-request doesn't skip the compensating delete and leave an orphan token live.defer cancel()was rejected by gocritic (defer-in-loop); replaced with explicitcancel()after the call. Revokeerrors on missing world. IflookupWorld(found.World)returns nil (operator removed the world between mint and revoke), silently dropping the issuance entry would leave the token live in the orphaned world's tokens.toml. Now returns an error naming the orphaned world; the issuance record stays so the operator can investigate. Tested inTestRevokeMissingWorldPreservesIssuance.
HTTP/API hygiene:
- Bearer parsing is case-insensitive per RFC 6750 §2.1 (
strings.Fields+strings.EqualFold). Clients in the wild sendBearer,bearer,BEARER. - Partial mint returns
200with{tokens: [...], partialFailure: "one_or_more_worlds_failed"}— stable code, noterr.Error()(which leaks Secret names + backend failure modes). Full err text goes to the structured log. The HTTP layer previously droppedresultsonerr != nilfromIssuer.Mint, throwing away tokens that had already been activated in their Secrets. - Log subjects are sha256-prefixed via
hashSubject(claims.Subject)rather than raw email/caller. The issuances Secret is the authoritative identity store; logs only need a stable correlation fingerprint. StateTTL < 0rejected at config load (negative durations parse fine in YAML).verifier := broker.NewVerifier(context.Background(), cfg.OIDC)inmain.go— coreos/go-oidc does not honor the context for JWKS refresh (it builds its own background context internally viaWithoutCancel), so the priorWithCancel/defer cancel()was theatre.
Test infrastructure that catches real regressions:
httptest.NewTLSServer(notNewServer). The state cookie is set withSecure: true+Path: /auth/callback; without HTTPS the prior tests bypassed those attributes via manualreq.AddCookie.loginAndExtractnow returns a jar-backed*http.Client(shallow copy ofsrv.Client()so per-test mutations don't leak) and asserts the cookie made it into the jar for/auth/callback. A regression that widened the cookie path or droppedSecurefails fast.testHTTPTimeout = 5son every test client. In-process handlers should complete in microseconds; a deadlocked handler now fails the offending test in seconds instead of stalling the suite for Go's 10-minute default.url.QueryEscape(nonce)consistently on all callback URLs.NewNonceis hex today so this is a no-op in practice, but locks down the contract for future encoding changes.- Negative-mint tests use
assertNoSecretsWritten(t, k8s)which checks both the world namespace and the broker namespace. A regression that writes the issuances Secret before returning the error would otherwise slip past a world-only check. TestMintLabelCollisionExhaustsRetriesexercises the terminal exhaustion path (allmaxLabelRetriesattempts collide) and assertslabelGenwas called exactlymaxLabelRetriestimes — guards against a future change that bails early.
Things still open for Slice C (unchanged from plan §6.2):
Groups-claim authorization, expiry sweeper + Lease-based leader election, rotate endpoint, rate limit, SCIM webhook, drift-pruning sweeper test, RBAC-denied test, owner-check 403 test. Plus allowEmails: [...] on WorldConfig if individual-user carve-outs are wanted alongside groups.