Closes#57. Consumes mcp-chassis v0.6.0 (mcp-chassis#4).
Two halves of one six-day outage. The chassis fix alone would not have fixed
this one.
/HEALTHZ WAS A CONSTANT
newHealthzHandler marshalled its body ONCE at startup and served those bytes for
the process lifetime. On 2026-09-14 it reported jwt.status=degraded for six days
after Authentik had recovered, because the answer was computed before Authentik
was up and could never change. A health endpoint that cannot change its answer
is not a health endpoint.
It now evaluates readiness per request. That is also what drives recovery: the
chassis resolves a pending issuer on demand, so every probe is a retry.
/READYZ IS NEW, AND IT IS THE ACTUAL CONTROL
Through all three occurrences the pod was 1/1 Running and readiness-green while
401ing every OAuth caller. k8s had no signal and kept routing to it. /readyz
answers 503 while JWT auth is degraded, so the pod leaves the Service instead of
serving guaranteed failures, and the kubelet's probe cadence becomes the retry
loop — no background goroutine, no context to own.
/healthz deliberately stays 200. The process is alive; a liveness probe must not
restart it, because restarting does not make the IdP come back sooner.
A server with no issuer configured is always ready. Making readiness depend on an
issuer it was never given would pull every static-token-only MCP out of its
Service — asserted, not assumed.
THE RETRY-WITHOUT-THE-K8S-ISSUER FALLBACK IS GONE
It existed because one unreachable issuer used to fail the whole constructor and
take Authentik auth down with it. Since v0.6.0 each issuer resolves lazily and
independently, so a down issuer degrades only itself. The warning it logged —
"multi-issuer init failed; retrying without the k8s issuer" — was the first line
of today's incident; it no longer has a reason to exist.
A constructor error now means a config error, which exits non-zero rather than
warning and continuing.
TESTS
healthz_live_test.go, written first and red (newReadyzHandler undefined, and
newHealthzHandler's signature could not express a state that changes). The
sharpest assertion is that readiness is evaluated TWICE for two requests — a
handler that caches cannot pass it.
The existing table test is ported, not weakened: same three cases, same expected
statuses, only the mechanism by which state reaches the handler changed.
Verified: task check EXIT=0 (lint, vet, race). The lint failure it surfaced was
pre-existing and is fixed in the preceding commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This repo's copy of context-sync.sh was two days stale against the version
Mathias's Cursor/Aider decision (2026-09-01) already landed in dma-cli:
still generated .cursorrules and .aider.conventions.md, and lacked the
--force/placeholder pre-flight and .context/mcp.json root-merge dma-cli's
copy has had since.
Found while building the estate-wide context-sync drift gate (ADR-0019) --
this is exactly the drift the gate exists to catch, found by hand while
establishing the baseline it will check going forward.
- script synced to the canonical hash (dma-cli's current copy)
- .cursorrules, .aider.conventions.md, .aider.conf.yml deleted
- Taskfile's drift assertion and file list narrowed to match
- AGENTS.md, .context/system-prompt.txt regenerated against canonical
task check -> exit 0.
- Dockerfile: GOPRIVATE and the http→https insteadOf rewrite now name
git.d-ma.be (was the pre-rename gitea.d-ma.be; masked at build time only by
GOPROXY=direct + GOSUMDB=off).
- .context/PROJECT.md Repo URL → git.d-ma.be, adapters regenerated
(CLAUDE.md, AGENTS.md, .cursorrules, .aider.conventions.md, system-prompt.txt).
- main.go: version is now a `-ldflags -X main.version` overridable var defaulting
to "dev" instead of a hardcoded, drifting "0.1.0".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Derived adapters drifted after the root ~/dev/.context/AGENT.md gained the
rule-0 pre-task ritual; task check's context:check gate failed on it
(pre-existing, unrelated to #42). Regenerate via context-sync.sh.
First real port of the MCP chassis library — abort-criterion check for
spike S3 of the 2026-05 homelab architecture review.
Changes:
- Drop internal/auth/jwt.go (~79 LOC) — chassis provides JWTValidator
with identical signature.
- Drop internal/auth/bearer.go (~42 LOC) — chassis BearerMiddleware
has the same static-or-JWT semantics plus an optional WWW-Authenticate
resource_metadata challenge (consumed via new resourceMetadataURL arg).
- Drop internal/auth/bearer_test.go — same scenarios are covered in
the chassis bearer_test.go now.
- main.go: import chassis as `chassisauth`, build resourceMetadataURL
only when both DexIssuerURL + MCPResourceURL are set, replace the
inline /.well-known/oauth-protected-resource handler with the chassis
ProtectedResourceHandler.
internal/auth/caller.go (oauth2-proxy header → context) stays — chassis
out-of-scope.
Net LOC change: -~150 LOC duplicated infra + a 5-LOC import.
go.mod gains gitea.d-ma.be/mathias/mcp-chassis v0.1.0 (jwx/v2 + testify
already transitive, no new top-level deps).
Verifies abort criterion: one PR, one binary's worth of port, task check
green (lint + test + vet + govulncheck clean). Per the S3 spike spec,
this clears the chassis to continue. Next port: hyperguild/ingestion
(brain-mcp), filed as a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#27.
PROJECT.md
- Git section: TBD as the convention. Commit to main, one logical
change per commit, `task check` locally before push, CI is the
quality gate. PRs only for the parallel-agent exception.
- Agent rule 6: rewritten to match.
.gitea/workflows/cd.yml
- Drop the pull_request trigger — vestigial under TBD.
- Drop the `if: github.event_name != 'pull_request'` guard on the
build job (now always true since pull_request no longer fires).
Tag pushes still build (no version gating regression).
- Deploy `if` left alone — already correctly limits deploy to
main pushes, skipping tag-push builds.
.githooks/pre-push (new)
- Runs `task check` before every push. Set up via `task setup:hooks`,
which sets core.hooksPath to the in-repo .githooks dir.
Taskfile.yml
- New `setup:hooks` task to install the pre-push hook on a fresh
clone.
README.md
- Quickstart section showing `task setup:hooks` + the TBD policy.
Derived adapters regenerated via `task context:sync` and committed
in the same commit (single-commit invariant).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upstream .context/PROJECT.md gained a branch-protection rule + an
extra agent instruction. Pure regeneration via scripts/context-sync.sh
to make task check pass before force-push.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Derived adapters drifted from canonical root .context/AGENT.md after
the pgvector default change landed upstream. Pure regeneration via
scripts/context-sync.sh, no manual edits. Required to make task check
pass before the feature commits on this branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- internal/auth/jwt.go: JWTValidator via lestrrat-go/jwx/v2, JWKS auto-refresh
- internal/auth/bearer.go: replace Gitea PAT validation with JWT->static->default chain
- internal/gitea/client.go: always use service PAT; remove TokenFromContext lookup
- internal/config/config.go: add DexIssuerURL, MCPAudience, MCPResourceURL, StaticToken
- cmd/gitea-mcp/main.go: wire validator, fix /.well-known to return real AS list
- bearer_test.go: rewrite for new API