Files
home-network-k3s-impl/decisions
jakman85andClaude Opus 5 6a6fea8151 docs(auth): root-cause jackett's session loss; add the oauth2-proxy-session runbook
Reported as "GET works, PUT/POST to add indexers fail, reloading never helps,
and I am never re-prompted". Diagnosis lands here; the MANIFEST FIX DOES NOT,
deliberately -- see the blocker below.

ROOT CAUSE, measured from two pods' logs interleaved to the second:

  15:31:06  jackett  [AuthSuccess]
  15:31:29  radarr   "cookie signature not valid, removing session"
  15:31:32  jackett  "No valid authentication in request"   <- 3s later

Six oauth2-proxy instances (seven pods) emit the DEFAULT cookie name
`_oauth2_proxy` on `.home.keithjackson.dev` holding seven DIFFERENT
cookie-secrets (compared in-cluster by sha256; all distinct). `removing
session` is ClearSession, which expires the cookie on radarr's own zone-wide
--cookie-domain, so jackett's session is DELETED by a process in another
namespace, 26s after a clean login whose `expires` was 5 minutes out. 8 such
lines in radarr's log, 1 in jackett's, 0 elsewhere. It was never expiry and it
was never Jackett -- and it is why reloading does not help: the operator's
Radarr tab re-clears the cookie seconds after each new login.

Two contributing mechanisms, also measured:
  * session lifetime == Keycloak access-token lifetime, 300.000s exactly, with
    refresh_token:true on the same line and --cookie-refresh unset (default 0).
  * XHRs answered 302 not 401: oauth2-proxy's AJAX guard needs an exact
    `Accept: application/json`, which Jackett's jQuery does not send. On /api/
    paths: 33 x 302 vs 2 x 401. A browser following a 302 re-issues a POST as a
    GET and DROPS THE BODY -- 4 of 6 POSTs never reached Jackett at all.

WHY NO MANIFEST FIX HERE. apps/wave-0/jackett.yaml is a GENERATED GOLDEN. The
sidecar args come from one shared function, _oauth2_sidecar_container() in
catalog/render.py; catalog/jackett.yaml declares only auth.mode/auth.groups and
the auth schema is additionalProperties:false, so there is NO per-app override
hook. Hand-editing the rendered file is rejected by the gate exactly as
designed ("GOLDEN DRIFT ... render-golden: FAIL"). Changing the renderer
instead rewrites all five rendered apps -- measured:

  CHANGED: apps/wave-0/{jackett,pihole,qbittorrent}.yaml
  CHANGED: apps/wave-1/{radarr,sonarr}.yaml

and render-golden requires all five be regenerated in the same commit. Four of
them are owned by fix/oauth2-proxy-cookie-collision-fleet right now. The cookie
fix is ONE change, not five; it needs re-cutting as a single renderer commit
rather than two branches rewriting the same generated block.

Validated the intended argument list against the pinned image and the real
issuer before stopping: it printed back `name:_oauth2_proxy_jackett ...
domains:jackett.home.keithjackson.dev ... refresh:after 2m30s` and
`API route - Path: ^/api/`. Two corrections fell out of that: --api-route is
SINGULAR (--api-routes does not exist and the proxy refuses to start), and
--session-cookie-minimal is mutually exclusive with --cookie-refresh
("cookie_refresh > 0 requires oauth tokens in sessions"), so dropping
--pass-access-token does NOT shrink the cookie -- it is a least-privilege
change, not a size one.

Adds runbooks/oauth2-proxy-session.md: the four sidecar log lines that tell the
auth layers apart (including the upstream `-` that means the proxy answered a
request the app never saw), the cross-log collision diagnosis, and the
generated-golden warning with its blast radius. Registered in the
configMapGenerator AND runbooks/README.md so the URL resolves.

Jackett is NOT fixed by this commit and will keep losing its session to Radarr
until the renderer change ships.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-30 12:47:42 -05:00
..

decisions/ — the handoff log, one file per entry

This directory is the decision log. ../DECISIONS-LOG.md is the frozen archive of everything written before 2026-09-29 and is never edited again (see "The archive is frozen" below).

Why this exists

DECISIONS-LOG.md was append-only in a single file, and every agent appended to its tail. Two branches that both append hit the same last hunk, so git cannot auto-merge them: the second one to be reviewed conflicts, and so does the third, and the fourth. Measured on 2026-09-29: four of the five open PRs were unmergeable, and DECISIONS-LOG.md was the only conflicting file in all four:

#22 fix/crafty-oauth2-proxy-signin      CONFLICT (content): DECISIONS-LOG.md
#20 feat/conformance-job-template       CONFLICT (content): DECISIONS-LOG.md
#19 feat/conformance-trigger-kubeconfig CONFLICT (content): DECISIONS-LOG.md
#18 feat/conformance-runner-image       CONFLICT (content): DECISIONS-LOG.md
#17 feat/ci-is-the-approver             clean  (it touched no log entry)

Nothing was wrong with any of those changes. The log's storage format serialised work that was otherwise parallel, and it got worse with every concurrent branch: N branches produce N-1 rebases, each of which invalidates any gate run that preceded it.

The fix is the obvious one for concurrent appenders: stop sharing a file. Each handoff is its own file. Two agents writing handoffs touch disjoint paths, so there is nothing for git to merge and nothing to conflict. This scales to any number of parallel branches with zero coordination — which is the point, since the agents are deliberately isolated from each other in separate worktrees and cannot coordinate.

There is deliberately no index file and no counter. An index would be a shared mutable file — exactly the thing that just failed — and a sequence number would have two agents racing for 0043. The directory listing is the index.

Writing an entry

One file per handoff, named:

decisions/YYYY-MM-DD-<slug>.md

<slug> is lowercase [a-z0-9-]. Use your branch name's last segment — it is already unique per unit of work, which is precisely the property the filename needs. A branch fix/crafty-oauth2-proxy-signin handed off on 2026-09-29 writes:

decisions/2026-09-29-crafty-oauth2-proxy-signin.md

The file is YAML frontmatter followed by three required sections:

---
date: 2026-09-29
branch: fix/crafty-oauth2-proxy-signin
title: The Keycloak gate works — Crafty's own login was the wall
---

## What changed

Prose. What you actually did, and why.

## What I verified

The measurements, with their numbers. Quote the field you read, not the prose you
believed. Name the gate result and its exit code.

## What I did NOT verify

Everything else. An honest "UNVERIFIED" is worth more than an optimistic "green" —
this repo has been burned by the difference more than once.

All three headings are required and enforced (ci/decisions-log.sh). They are CLAUDE.md §7's contract; making them a checked structure is what stops an entry degrading into "done ✅".

Reading the log

The whole log, oldest first:

ls decisions/[0-9]*.md | sort

The current PM handoff — the newest entry. Ties within a day are broken by what landed on main last, which is the honest answer, so ask git rather than the filename:

git log --diff-filter=A --format='%H' --name-only -- 'decisions/[0-9]*.md' \
  | grep '^decisions/' | head -1

For a quick skim of who said what:

grep -H '^title:' decisions/[0-9]*.md | sort

The archive is frozen

DECISIONS-LOG.md is read-only history. Its SHA-256 is pinned in decisions/ARCHIVE.sha256 and ci/decisions-log.sh fails the merge gate if the file changes. That is not ceremony: without it, the next agent to read runbooks/ or an old CLAUDE.md habit appends to the tail again and re-creates the conflict for everybody else, silently, one PR at a time.

If the archive genuinely must change (a leaked credential to redact, say), that is a deliberate, reviewed act: change it, run sha256sum DECISIONS-LOG.md > decisions/ARCHIVE.sha256, and say in your handoff entry why. Re-pinning is visible in the diff. Appending is not.