Rejections logged. chassis v0.2.0 → v0.7.1: mcp auth rejected (reason, remote, token fingerprint, path) and mcp auth accepted lines. TestBuildMuxLogsARESTAuthRejection (red on v0.2.0, green after) asserts it through buildMux and that the credential is not logged.
Fallout — behaviour change worth reading. Since chassis v0.7 (mcp-chassis#4) NewJWTValidator never fails on OIDC discovery; it resolves lazily and answers JWTs 503-and-retry. #83's boot retry loop became dead (its 3 tests went red). buildJWTValidator now builds once and Warn-logs an unresolved issuer. A misconfigured issuer no longer exits the process: JWTs get 503 forever, static bearer works. internal/startupretry is now unused (left in place). New test: JWT on a REST route with unresolved issuer → 503 through buildMux.
Metrics premise corrected. Authenticated REST requests were already labelled by route (REST mux sets Pattern on the same *http.Request the middleware reads — pinned by a test). Only auth-refused requests showed path="/". labelRESTPattern sets the REST pattern before auth; unmatched paths keep / (bounded label set, tested).
Closes #155.
1. **Rejections logged.** chassis v0.2.0 → v0.7.1: `mcp auth rejected` (reason, remote, token fingerprint, path) and `mcp auth accepted` lines. `TestBuildMuxLogsARESTAuthRejection` (red on v0.2.0, green after) asserts it through buildMux and that the credential is not logged.
2. **Fallout — behaviour change worth reading.** Since chassis v0.7 (mcp-chassis#4) `NewJWTValidator` never fails on OIDC discovery; it resolves lazily and answers JWTs 503-and-retry. #83's boot retry loop became dead (its 3 tests went red). `buildJWTValidator` now builds once and Warn-logs an unresolved issuer. **A misconfigured issuer no longer exits the process**: JWTs get 503 forever, static bearer works. `internal/startupretry` is now unused (left in place). New test: JWT on a REST route with unresolved issuer → 503 through buildMux.
3. **Metrics premise corrected.** Authenticated REST requests were already labelled by route (REST mux sets `Pattern` on the same `*http.Request` the middleware reads — pinned by a test). Only auth-refused requests showed `path="/"`. `labelRESTPattern` sets the REST pattern before auth; unmatched paths keep `/` (bounded label set, tested).
`CI=1 task check` exit 0. Auth-adjacent → gatekeeper review.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---
_Created via git-mcp on behalf of @mathias_
1. Rejections are logged. chassis v0.2.0 refused without a log line, so
since #149 an unknown REST caller got silent 401s. v0.7.1 writes
`mcp auth rejected` (reason, remote, token fingerprint, path) and
`mcp auth accepted` per authenticated request. Tested through
buildMux.
2. Fallout: since chassis v0.7 (mcp-chassis#4) NewJWTValidator no longer
fails on OIDC discovery; the issuer resolves lazily and a JWT gets
503-and-retry until it does. #83's boot retry loop therefore never
retried, and its tests failed. buildJWTValidator now builds once and
logs an issuer still unresolved at boot. Behaviour change: a
misconfigured issuer no longer exits the process; JWTs get 503, the
static bearer keeps working. internal/startupretry is now unused.
3. Metrics: authenticated REST requests were already labelled by route
(the REST mux sets Pattern on the same *http.Request the middleware
reads). Requests refused by auth never reached the REST mux and were
labelled path="/". labelRESTPattern now sets the REST route's pattern
before auth; unmatched paths keep "/", so the label set stays bounded.
Closes#155
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ingestion/cmd/server/routes_observability_test.go:28: the "credential is never logged" assertion does not test that.TestBuildMuxLogsARESTAuthRejection asserts that the log does not contain the header form of the wrong credential (the Bearer prefix plus the token). The chassis logs fields, not headers, so that string could never show up. If the chassis logged the raw presented token as a bare field value, this assertion would still pass. Flip test: I pointed a local copy of chassis v0.7.1 that wrote the raw token into token_fp on rejection at the module with a replace directive. The suite stayed green. The assertion message says "the credential itself is never logged", so the reader trusts a guard that does not exist (AGENT.md rule 9). The same gap applies to the mcp auth accepted line: no hyperguild test checks that the line leaves out the valid static token. Flip test: making accept log the raw token also stayed green.
Today the chassis behaves correctly. I checked by execution: across the full case matrix, the static, wrong and JWT sentinels appear 0 times in either log line. But the next chassis bump is the regression this assertion claims to catch, and it would not catch it.
Fix (a correction, not a judgement call): use a distinctive sentinel as the wrong token and assert the bare sentinel is absent. Add the same check for the accepted line using the valid token. Then re-run the flip: a chassis that logs the raw token must turn the suite red.
Non-blocking (fix the comment in the same pass, or file a follow-up)
ingestion/cmd/server/routes.go (labelRESTPattern doc comment) says "the label set stays bounded by the registered routes". That is true for refused requests only. An authenticated request to an unregistered path reaches the inner REST mux, which 404s and clears Pattern. metrics.Middleware then falls back to the raw URL path, so an authenticated caller can still create one series per unique path. This is pre-existing: the label set was identical on main and on this head across 1,872 requests. This PR does not cause it, but the comment claims it is solved.
ingestion/internal/startupretry no longer has any callers. Remove it or track it.
Behaviour change, accepted: when the issuer is unresolved, every non-static bearer (JWT or a wrong opaque token) now gets 503 instead of 401. The static bearer still gets 200, a missing token still gets 401, and nothing is newly accepted. There is no oracle, because every non-static bearer gets the same 503. While a discovery fetch is in flight, JWT requests queue on the chassis' per-issuer lock (measured with retry interval 0). At the default 30s interval that costs at most one issuer-latency per 30s, and the static path is unaffected. That is a chassis property, not a regression from this PR.
Behaviour change, accepted and documented (main.go comment and commit body): a misconfigured issuer no longer exits the process. It logs one warning at boot, and every JWT then gets reason=jwt_dex_unavailable 503. The cost is that a bad issuer URL no longer fails fast. It shows up as a stream of 5xx and reject log lines instead of a CrashLoop.
Verified by execution
Ran an auth differential (16 cases × validator nil/resolved/unresolved/recovered) through the chassis v0.2.0 and v0.7.1 middleware. In every mode where both versions run, the verdicts are identical. Under v0.2.0 an unresolved issuer exits at construction. v0.7.1 newly accepts nothing (alg-none, tampered signature, foreign key, wrong iss/aud and expired are all 401 once resolved). /mcp and REST share the same BearerMiddleware call with the same arguments.
Metrics/routing: ran 1,872 requests (9 methods × 26 paths × 2 hosts × 4 auth states) through metrics.Middleware(buildMux) on main and on this head. Status codes and Location headers are identical for every request, and the set of distinct path labels is identical. Only the label changed: 66 refused requests to registered routes moved from / to their route pattern. labelRESTPattern does not change routing or auth decisions.
Flip tests on the PR's tests all went red, except the two named above: dropping labelRESTPattern, labelling by raw path, setting an empty pattern, dropping the boot warning, always warning, and restoring fail-on-unresolved.
Local gate: task check (CI=1) exit 0, golangci-lint 0 issues, 0 FAIL lines. CI run 9376 is success. Green covers none of the blocking finding.
Not checked
Live cluster behaviour. Whether any alert watches ingestion 5xx or jwt_dex_unavailable, which matters now that a bad issuer no longer CrashLoops. Log volume from mcp auth accepted being written at Info on every authenticated /mcp and REST request.
## pr-gatekeeper verdict: BLOCKED (PR #181, closes #155)
Mode: PR mode. Head `474473c`. Reviewed 2026-10-05.
### Blocking finding
**`ingestion/cmd/server/routes_observability_test.go:28`: the "credential is never logged" assertion does not test that.** `TestBuildMuxLogsARESTAuthRejection` asserts that the log does not contain the *header form* of the wrong credential (the `Bearer ` prefix plus the token). The chassis logs fields, not headers, so that string could never show up. If the chassis logged the raw presented token as a bare field value, this assertion would still pass. Flip test: I pointed a local copy of chassis v0.7.1 that wrote the raw token into `token_fp` on rejection at the module with a `replace` directive. The suite stayed **green**. The assertion message says "the credential itself is never logged", so the reader trusts a guard that does not exist (AGENT.md rule 9). The same gap applies to the `mcp auth accepted` line: no hyperguild test checks that the line leaves out the **valid** static token. Flip test: making `accept` log the raw token also stayed **green**.
Today the chassis behaves correctly. I checked by execution: across the full case matrix, the static, wrong and JWT sentinels appear 0 times in either log line. But the next chassis bump is the regression this assertion claims to catch, and it would not catch it.
Fix (a correction, not a judgement call): use a distinctive sentinel as the wrong token and assert the bare sentinel is absent. Add the same check for the accepted line using the valid token. Then re-run the flip: a chassis that logs the raw token must turn the suite red.
### Non-blocking (fix the comment in the same pass, or file a follow-up)
- `ingestion/cmd/server/routes.go` (labelRESTPattern doc comment) says "the label set stays bounded by the registered routes". That is true for **refused** requests only. An **authenticated** request to an unregistered path reaches the inner REST mux, which 404s and clears `Pattern`. `metrics.Middleware` then falls back to the raw URL path, so an authenticated caller can still create one series per unique path. This is pre-existing: the label set was identical on main and on this head across 1,872 requests. This PR does not cause it, but the comment claims it is solved.
- `ingestion/internal/startupretry` no longer has any callers. Remove it or track it.
- Behaviour change, accepted: when the issuer is unresolved, every non-static bearer (JWT **or** a wrong opaque token) now gets 503 instead of 401. The static bearer still gets 200, a missing token still gets 401, and nothing is newly accepted. There is no oracle, because every non-static bearer gets the same 503. While a discovery fetch is in flight, JWT requests queue on the chassis' per-issuer lock (measured with retry interval 0). At the default 30s interval that costs at most one issuer-latency per 30s, and the static path is unaffected. That is a chassis property, not a regression from this PR.
- Behaviour change, accepted and documented (main.go comment and commit body): a misconfigured issuer no longer exits the process. It logs one warning at boot, and every JWT then gets `reason=jwt_dex_unavailable` 503. The cost is that a bad issuer URL no longer fails fast. It shows up as a stream of 5xx and reject log lines instead of a CrashLoop.
### Verified by execution
- Ran an auth differential (16 cases × validator nil/resolved/unresolved/recovered) through the chassis v0.2.0 and v0.7.1 middleware. In every mode where both versions run, the verdicts are identical. Under v0.2.0 an unresolved issuer exits at construction. v0.7.1 newly accepts nothing (alg-none, tampered signature, foreign key, wrong iss/aud and expired are all 401 once resolved). /mcp and REST share the same `BearerMiddleware` call with the same arguments.
- Metrics/routing: ran 1,872 requests (9 methods × 26 paths × 2 hosts × 4 auth states) through `metrics.Middleware(buildMux)` on main and on this head. Status codes and Location headers are identical for every request, and the set of distinct path labels is identical. Only the label changed: 66 refused requests to registered routes moved from `/` to their route pattern. `labelRESTPattern` does not change routing or auth decisions.
- Flip tests on the PR's tests all went red, except the two named above: dropping labelRESTPattern, labelling by raw path, setting an empty pattern, dropping the boot warning, always warning, and restoring fail-on-unresolved.
- Local gate: `task check` (CI=1) exit 0, golangci-lint 0 issues, 0 FAIL lines. CI run 9376 is **success**. Green covers none of the blocking finding.
### Not checked
Live cluster behaviour. Whether any alert watches ingestion 5xx or `jwt_dex_unavailable`, which matters now that a bad issuer no longer CrashLoops. Log volume from `mcp auth accepted` being written at Info on every authenticated /mcp and REST request.
PR #181 review (comment 15483):
- TestBuildMuxLogsARESTAuthRejection asserted only that the "Bearer <tok>"
header form was absent, which a field-logging chassis could never emit.
Replaced by TestBuildMuxRejectionLogLineOmitsTheBarePresentedToken (a
sentinel wrong token; neither it nor the static token may appear) and
TestBuildMuxAcceptedLogLineOmitsTheBareStaticToken. A local chassis
v0.7.1 copy logging rawToken as token_fp (deny, then accept) turns each red.
- labelRESTPattern doc: the label bound holds for refused requests only;
the authenticated-404 raw-path gap is hyperguild#185.
- Remove internal/startupretry: nothing imports it since the chassis bump.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blocking: the "credential never logged" check could not fail
TestBuildMuxLogsARESTAuthRejection is replaced by TestBuildMuxRejectionLogLineOmitsTheBarePresentedToken. It presents the sentinel wrong-tok-sentinel-q7zx and asserts that neither the bare sentinel nor the static token appears in the log. The new TestBuildMuxAcceptedLogLineOmitsTheBareStaticToken makes the same check on the mcp auth accepted line.
I built a local copy of chassis v0.7.1 and wired it in with a replace. Unmodified, it is green. With deny logging "token_fp", rawToken, the rejection test goes red. With accept logging "token_fp", rawToken, the accepted test goes red.
labelRESTPattern doc over-claimed
The comment now says the bound holds for refused requests only, and that an authenticated 404 falls back to the raw path. That gap is filed as #185.
—
internal/startupretry unused
Deleted. Nothing imports it.
gate green
The PR is labelled risk/medium (auth code). CI=1 GOFLAGS=-p=2 task check exits 0 with 0 FAIL lines, and the commit was chained && after it. Not merged and not tagged; this needs a reviewer re-check. #183 is rebased onto this head next.
Round 2 (e05a13a) responds to the BLOCK in 15483.
| Finding | Change | Flip evidence |
|---|---|---|
| Blocking: the "credential never logged" check could not fail | `TestBuildMuxLogsARESTAuthRejection` is replaced by `TestBuildMuxRejectionLogLineOmitsTheBarePresentedToken`. It presents the sentinel `wrong-tok-sentinel-q7zx` and asserts that neither the bare sentinel nor the static token appears in the log. The new `TestBuildMuxAcceptedLogLineOmitsTheBareStaticToken` makes the same check on the `mcp auth accepted` line. | I built a local copy of chassis v0.7.1 and wired it in with a `replace`. Unmodified, it is green. With `deny` logging `"token_fp", rawToken`, the rejection test goes **red**. With `accept` logging `"token_fp", rawToken`, the accepted test goes **red**. |
| `labelRESTPattern` doc over-claimed | The comment now says the bound holds for refused requests only, and that an authenticated 404 falls back to the raw path. That gap is filed as **#185**. | — |
| `internal/startupretry` unused | Deleted. Nothing imports it. | gate green |
The PR is labelled `risk/medium` (auth code). `CI=1 GOFLAGS=-p=2 task check` exits 0 with 0 FAIL lines, and the commit was chained `&&` after it. Not merged and not tagged; this needs a reviewer re-check. #183 is rebased onto this head next.
---
_Created via git-mcp on behalf of @mathias_
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #155.
mcp auth rejected(reason, remote, token fingerprint, path) andmcp auth acceptedlines.TestBuildMuxLogsARESTAuthRejection(red on v0.2.0, green after) asserts it through buildMux and that the credential is not logged.NewJWTValidatornever fails on OIDC discovery; it resolves lazily and answers JWTs 503-and-retry. #83's boot retry loop became dead (its 3 tests went red).buildJWTValidatornow builds once and Warn-logs an unresolved issuer. A misconfigured issuer no longer exits the process: JWTs get 503 forever, static bearer works.internal/startupretryis now unused (left in place). New test: JWT on a REST route with unresolved issuer → 503 through buildMux.Patternon the same*http.Requestthe middleware reads — pinned by a test). Only auth-refused requests showedpath="/".labelRESTPatternsets the REST pattern before auth; unmatched paths keep/(bounded label set, tested).CI=1 task checkexit 0. Auth-adjacent → gatekeeper review.🤖 Generated with Claude Code
Created via git-mcp on behalf of @mathias
pr-gatekeeper verdict: BLOCKED (PR #181, closes #155)
Mode: PR mode. Head
474473c. Reviewed 2026-10-05.Blocking finding
ingestion/cmd/server/routes_observability_test.go:28: the "credential is never logged" assertion does not test that.TestBuildMuxLogsARESTAuthRejectionasserts that the log does not contain the header form of the wrong credential (theBearerprefix plus the token). The chassis logs fields, not headers, so that string could never show up. If the chassis logged the raw presented token as a bare field value, this assertion would still pass. Flip test: I pointed a local copy of chassis v0.7.1 that wrote the raw token intotoken_fpon rejection at the module with areplacedirective. The suite stayed green. The assertion message says "the credential itself is never logged", so the reader trusts a guard that does not exist (AGENT.md rule 9). The same gap applies to themcp auth acceptedline: no hyperguild test checks that the line leaves out the valid static token. Flip test: makingacceptlog the raw token also stayed green.Today the chassis behaves correctly. I checked by execution: across the full case matrix, the static, wrong and JWT sentinels appear 0 times in either log line. But the next chassis bump is the regression this assertion claims to catch, and it would not catch it.
Fix (a correction, not a judgement call): use a distinctive sentinel as the wrong token and assert the bare sentinel is absent. Add the same check for the accepted line using the valid token. Then re-run the flip: a chassis that logs the raw token must turn the suite red.
Non-blocking (fix the comment in the same pass, or file a follow-up)
ingestion/cmd/server/routes.go(labelRESTPattern doc comment) says "the label set stays bounded by the registered routes". That is true for refused requests only. An authenticated request to an unregistered path reaches the inner REST mux, which 404s and clearsPattern.metrics.Middlewarethen falls back to the raw URL path, so an authenticated caller can still create one series per unique path. This is pre-existing: the label set was identical on main and on this head across 1,872 requests. This PR does not cause it, but the comment claims it is solved.ingestion/internal/startupretryno longer has any callers. Remove it or track it.reason=jwt_dex_unavailable503. The cost is that a bad issuer URL no longer fails fast. It shows up as a stream of 5xx and reject log lines instead of a CrashLoop.Verified by execution
BearerMiddlewarecall with the same arguments.metrics.Middleware(buildMux)on main and on this head. Status codes and Location headers are identical for every request, and the set of distinct path labels is identical. Only the label changed: 66 refused requests to registered routes moved from/to their route pattern.labelRESTPatterndoes not change routing or auth decisions.task check(CI=1) exit 0, golangci-lint 0 issues, 0 FAIL lines. CI run 9376 is success. Green covers none of the blocking finding.Not checked
Live cluster behaviour. Whether any alert watches ingestion 5xx or
jwt_dex_unavailable, which matters now that a bad issuer no longer CrashLoops. Log volume frommcp auth acceptedbeing written at Info on every authenticated /mcp and REST request.Round 2 (
e05a13a) responds to the BLOCK in 15483.TestBuildMuxLogsARESTAuthRejectionis replaced byTestBuildMuxRejectionLogLineOmitsTheBarePresentedToken. It presents the sentinelwrong-tok-sentinel-q7zxand asserts that neither the bare sentinel nor the static token appears in the log. The newTestBuildMuxAcceptedLogLineOmitsTheBareStaticTokenmakes the same check on themcp auth acceptedline.replace. Unmodified, it is green. Withdenylogging"token_fp", rawToken, the rejection test goes red. Withacceptlogging"token_fp", rawToken, the accepted test goes red.labelRESTPatterndoc over-claimedinternal/startupretryunusedThe PR is labelled
risk/medium(auth code).CI=1 GOFLAGS=-p=2 task checkexits 0 with 0 FAIL lines, and the commit was chained&&after it. Not merged and not tagged; this needs a reviewer re-check. #183 is rebased onto this head next.Created via git-mcp on behalf of @mathias