Repo: <project>-dev/<project> (Go). Docs-side fix; server behavior unchanged.
The repo wasn't present in the working directory, so I cloned <project>-dev/<project>, reproduced the ordering live, applied the docs-side fix, and verified it. Solution also saved at /workspace/solution.md.
Repo: <project>-dev/<project> (Go). Docs-side fix; server behavior unchanged.
internal/registry/agentsig.go's authorizeAgent is called before the handler's existence lookup, but the helper itself contains both a cheap presence check and the timestamp-window check. The existence lookup therefore lands mid-sequence:
| # | Check | Failure |
|---|---|---|
| 1 | X-Agent-ID / X-Agent-Ts present (any id) |
401 missing agent signature headers … |
| 2 | target agent exists in the registry | 404 agent not found |
| 3 | timestamp within ±30 s | 401 request timestamp outside allowed window (±30s) |
| 4 | ed25519 signature verifies against the registered key | 401 signature verification failed |
A registered agent with a stale timestamp gets 401, but an unregistered agent with the same stale timestamp gets 404 — existence wins because it runs before the timestamp/signature checks. The README only documented the 401 outcomes, so a tester probing a ghost id got an undocumented status. The order was derived by live probe (presence-controlled matrix), not by reading the handler.
CR_REQUIRE_AGENT_SIG default on)register known (public_key = 64 hex "ab") -> 201
1 ghost + no headers 401 missing agent signature headers
2 known + no headers 401 missing agent signature headers
3 known + trio, all-zero sig (fresh ts) 401 signature verification failed
4 ghost + trio, all-zero sig (fresh ts) 404 agent not found
5 known + trio, all-zero sig (ts = now-900) 401 request timestamp outside allowed window (±30s)
6 ghost + trio, all-zero sig (ts = now-900) 404 agent not found
Step 4 is reachable only when a syntactically valid trio is present; step 5's 401 appears only for a registered id. Together they pin the order without reading handler code.
README.md — document the four-step precedenceInserted in §5 after the existing "missing agent signature headers" sentence:
Every agent-scoped endpoint authorizes in four fixed steps, so the status names
exactly which one failed: (1) a request missing `X-Agent-ID` or `X-Agent-Ts`
answers `401` for any id; (2) an **unregistered** target id answers
`404 {"error":"agent not found"}` once the trio is present — the existence lookup
runs between the presence check and the timestamp/signature checks, so a ghost id
with a bogus signature is a `404`, not a `401`; (3) a timestamp outside ±30s
answers `401`; (4) a signature that does not verify against the registered public
key answers `401`. A registered agent with a stale timestamp is therefore a
`401`, while the same stale timestamp on an unregistered id is the `404` from
step 2.
docs/claims.yaml — anchor + status expectation - id: STATUS-AGENT-SIG-EXISTENCE-BEFORE-AUTH
kind: status
doc: README.md
quote: "runs between the presence check and the timestamp/signature checks, so a ghost id\nwith a bogus signature is a `404`, not a `401`"
expect: "404"
xfail: null
The quote is a byte-exact substring of the new README sentence, so deleting the sentence fails the existing verifyAnchors phase.
cmd/server/docsclaims_test.go — live 404 probe (never hardcoded)Add the id-keyed case to makeLiveStatusProbes and the probe:
const docsClaimsGhostAgent = "docsclaims-ghost"
func liveAgentSigExistenceBeforeAuth(client *http.Client, baseURL string) (int, error) {
ts := strconv.FormatInt(time.Now().Unix(), 10)
req, err := http.NewRequest(http.MethodGet, baseURL+"/agents/"+docsClaimsGhostAgent+"/inbox", nil)
if err != nil {
return 0, err
}
req.Header.Set("Authorization", "Bearer test-token")
req.Header.Set(registry.HeaderAgentID, docsClaimsGhostAgent)
req.Header.Set(registry.HeaderAgentTS, ts)
// 64 bytes = 128 hex chars; value irrelevant because existence runs first.
req.Header.Set(registry.HeaderAgentSig, strings.Repeat("00", 64))
resp, err := client.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body)
return resp.StatusCode, nil
}
docsclaims-ghost is deliberately never registered, and the probe returns resp.StatusCode — if the gate verified the signature before the lookup, it would return the live 401 and the claim would fail on expected=404 observed=401.
Add TestAgentSigExistenceClaimAnchorHasTeeth, proving both directions (no server): live README satisfies the anchor; removing the exact quoted sentence makes verifyAnchors report claim STATUS-AGENT-SIG-EXISTENCE-BEFORE-AUTH: anchor missing … observed=absent.
gofmt -l cmd/server/docsclaims_test.go && go vet ./cmd/server
go test -short -count=1 -run TestAgentSigExistenceClaimAnchorHasTeeth -v ./cmd/server
make docs-check # go test -short -count=1 -run 'TestDocsClaims' ./cmd/server
Observed:
anchor teeth: live=green, sentence-removed=claim STATUS-AGENT-SIG-EXISTENCE-BEFORE-AUTH: anchor missing ... observed=absent
--- PASS: TestAgentSigExistenceClaimAnchorHasTeeth
make docs-check exit=0
ok github.com/<project>-dev/<project>/cmd/server 5.3s
Wire-side non-vacuousness: temporarily setting the claim's expect to "401" fails the gate with the live status (expected=401 observed=404); restoring expect: "404" is green.
git diff --stat:
README.md | 11 +++++
cmd/server/docsclaims_test.go | 105 ++++++++++++++++++++++++++++++++++++++++++
docs/claims.yaml | 12 +++++
3 files changed, 128 insertions(+)
When a handler folds a cheap pre-auth check into an auth helper, the existence lookup lands mid-sequence and produces a status combination no doc predicts. Derive the order with a presence-controlled request matrix (headers absent / present-but-bogus / present-and-stale × subject exists / not) rather than from the middleware's name, then pin the observed order in the doc and in an executable claim whose anchor is a byte-exact doc substring and whose probe returns the live status — so neither side can drift alone.
# Evidence - Problem class: http-auth-gate-check-ordering - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-20T02:27:41.248Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "SYMPTOM: a docs-vs-wire mismatch that reads as a broken contract. A README/guide documents an agent-scoped endpoint's negative path as '401 missing signature headers' (and stale timestamps as 401), and an external tester probing the negative path with an UNREGISTERED agent id gets 404 'agent not found' instead, then reports a contract mismatch. Two independent falses: the doc omits a status the server really returns, and the tester's mental model ('auth first') disagrees with the real check order.\n\nROOT CAUSE (derived by live probe, not by reading docs): the agent-scoped handlers run the per-agent signature gate BEFORE the existence lookup, but the gate itself contains BOTH a presence check and a timestamp-window check, so the existence check sits BETWEEN two auth checks. The real order is: (1) signature trio PRESENT? no -> 401 missing agent signature headers (any id); (2) agent EXISTS in the registry? no -> 404 agent not found (only reachable once a trio is present); (3) timestamp inside the +/-30s window? no -> 401 outside allowed window; (4) ed25519 signature verifies against the REGISTERED public key? no -> 401 signature verification failed. A registered agent with a stale timestamp therefore 401s, while an unregistered agent with the SAME stale timestamp 404s: the existence check wins because it runs before the timestamp/signature checks. The intuitive model ('all auth before existence') predicts 401 in both cases and is wrong.\n\nHOW IT WAS PINNED: start a fresh build env-isolated on a scratch port with signature enforcement at its default ON; register one agent with a dummy 64-hex ed25519 public key; then issue six requests against the inbox endpoint and record the status+body of each: ghost id with no headers (401), registered id with no headers (401), registered id with the trio but a bogus all-zero 64-byte signature (401 signature verification failed), ghost id with the trio (404 agent not found), registered id with a 900s-stale timestamp (401 outside window), ghost id with the same stale timestamp (404). The 404 appears only when a syntactically valid trio is present, and the stale-timestamp 401 appears only for a registered id: the two probes together separate the existence check from both auth sub-checks without reading a line of handler code.\n\nFIX SHAPE (docs-side, behavior unchanged): document the four-step precedence as a reader-executable claim and pin it with a docs-claims probe that observes the real 404 rather than asserting a literal. The gate needs teeth: the new probe's id-keyed case must return the live status code from the response, never a hardcoded 404, and the anchor must be an EXACT substring of the new doc sentence so removing the sentence fails the build. Prove both directions (doc sentence removed -> gate FAILS naming the claim; restored byte-exact -> green).\n\nVERIFICATION: the six-request matrix above is the acceptance evidence and is deterministic (no LLM, no clock dependence beyond a fresh timestamp and a deliberately stale one). Gate: run the docs check and confirm the new claim is reported verified (not xfail, not skipped) and the command exits 0.\n\nGENERAL RULE: when a handler mixes a cheap pre-auth check into an auth helper, the existence lookup lands mid-sequence and produces a status combination no doc predicts. Derive the order with a presence-controlled request matrix (headers absent / present-but-bogus / present-and-stale x subject exists / not) rather than from the middleware's name, then pin the observed order in the doc AND in an executable claim so neither side can drift alone.", "environment": "", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "http-auth-gate-check-ordering", "provider": "openrouter", "solved_at": "2026-09-20T02:27:41.249Z", "version": ""}The repo wasn't present in the working directory, so I cloned <project>-dev/<project>, reproduced the ordering live, applied the docs-side fix, and verified it. Solution also saved at /workspace/solution.md.
Repo: <project>-dev/<project> (Go). Docs-side fix; server behavior unchanged.
internal/registry/agentsig.go's authorizeAgent is called before the handler's existence lookup, but the helper itself contains both a cheap presence check and the timestamp-window check. The existence lookup therefore lands mid-sequence:
| # | Check | Failure |
|---|---|---|
| 1 | X-Agent-ID / X-Agent-Ts present (any id) |
401 missing agent signature headers … |
| 2 | target agent exists in the registry | 404 agent not found |
| 3 | timestamp within ±30 s | 401 request timestamp outside allowed window (±30s) |
| 4 | ed25519 signature verifies against the registered key | 401 signature verification failed |
A registered agent with a stale timestamp gets 401, but an unregistered agent with the same stale timestamp gets 404 — existence wins because it runs before the timestamp/signature checks. The README only documented the 401 outcomes, so a tester probing a ghost id got an undocumented status. The order was derived by live probe (presence-controlled matrix), not by reading the handler.
CR_REQUIRE_AGENT_SIG default on)register known (public_key = 64 hex "ab") -> 201
1 ghost + no headers 401 missing agent signature headers
2 known + no headers 401 missing agent signature headers
3 known + trio, all-zero sig (fresh ts) 401 signature verification failed
4 ghost + trio, all-zero sig (fresh ts) 404 agent not found
5 known + trio, all-zero sig (ts = now-900) 401 request timestamp outside allowed window (±30s)
6 ghost + trio, all-zero sig (ts = now-900) 404 agent not found
Step 4 is reachable only when a syntactically valid trio is present; step 5's 401 appears only for a registered id. Together they pin the order without reading handler code.
README.md — document the four-step precedenceInserted in §5 after the existing "missing agent signature headers" sentence:
Every agent-scoped endpoint authorizes in four fixed steps, so the status names
exactly which one failed: (1) a request missing `X-Agent-ID` or `X-Agent-Ts`
answers `401` for any id; (2) an **unregistered** target id answers
`404 {"error":"agent not found"}` once the trio is present — the existence lookup
runs between the presence check and the timestamp/signature checks, so a ghost id
with a bogus signature is a `404`, not a `401`; (3) a timestamp outside ±30s
answers `401`; (4) a signature that does not verify against the registered public
key answers `401`. A registered agent with a stale timestamp is therefore a
`401`, while the same stale timestamp on an unregistered id is the `404` from
step 2.
docs/claims.yaml — anchor + status expectation - id: STATUS-AGENT-SIG-EXISTENCE-BEFORE-AUTH
kind: status
doc: README.md
quote: "runs between the presence check and the timestamp/signature checks, so a ghost id\nwith a bogus signature is a `404`, not a `401`"
expect: "404"
xfail: null
The quote is a byte-exact substring of the new README sentence, so deleting the sentence fails the existing verifyAnchors phase.
cmd/server/docsclaims_test.go — live 404 probe (never hardcoded)Add the id-keyed case to makeLiveStatusProbes and the probe:
const docsClaimsGhostAgent = "docsclaims-ghost"
func liveAgentSigExistenceBeforeAuth(client *http.Client, baseURL string) (int, error) {
ts := strconv.FormatInt(time.Now().Unix(), 10)
req, err := http.NewRequest(http.MethodGet, baseURL+"/agents/"+docsClaimsGhostAgent+"/inbox", nil)
if err != nil {
return 0, err
}
req.Header.Set("Authorization", "Bearer test-token")
req.Header.Set(registry.HeaderAgentID, docsClaimsGhostAgent)
req.Header.Set(registry.HeaderAgentTS, ts)
// 64 bytes = 128 hex chars; value irrelevant because existence runs first.
req.Header.Set(registry.HeaderAgentSig, strings.Repeat("00", 64))
resp, err := client.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body)
return resp.StatusCode, nil
}
docsclaims-ghost is deliberately never registered, and the probe returns resp.StatusCode — if the gate verified the signature before the lookup, it would return the live 401 and the claim would fail on expected=404 observed=401.
Add TestAgentSigExistenceClaimAnchorHasTeeth, proving both directions (no server): live README satisfies the anchor; removing the exact quoted sentence makes verifyAnchors report claim STATUS-AGENT-SIG-EXISTENCE-BEFORE-AUTH: anchor missing … observed=absent.
gofmt -l cmd/server/docsclaims_test.go && go vet ./cmd/server
go test -short -count=1 -run TestAgentSigExistenceClaimAnchorHasTeeth -v ./cmd/server
make docs-check # go test -short -count=1 -run 'TestDocsClaims' ./cmd/server
Observed:
anchor teeth: live=green, sentence-removed=claim STATUS-AGENT-SIG-EXISTENCE-BEFORE-AUTH: anchor missing ... observed=absent
--- PASS: TestAgentSigExistenceClaimAnchorHasTeeth
make docs-check exit=0
ok github.com/<project>-dev/<project>/cmd/server 5.3s
Wire-side non-vacuousness: temporarily setting the claim's expect to "401" fails the gate with the live status (expected=401 observed=404); restoring expect: "404" is green.
git diff --stat:
README.md | 11 +++++
cmd/server/docsclaims_test.go | 105 ++++++++++++++++++++++++++++++++++++++++++
docs/claims.yaml | 12 +++++
3 files changed, 128 insertions(+)
When a handler folds a cheap pre-auth check into an auth helper, the existence lookup lands mid-sequence and produces a status combination no doc predicts. Derive the order with a presence-controlled request matrix (headers absent / present-but-bogus / present-and-stale × subject exists / not) rather than from the middleware's name, then pin the observed order in the doc and in an executable claim whose anchor is a byte-exact doc substring and whose probe returns the live status — so neither side can drift alone.
# Evidence - Problem class: http-auth-gate-check-ordering - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-20T02:27:41.248Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "SYMPTOM: a docs-vs-wire mismatch that reads as a broken contract. A README/guide documents an agent-scoped endpoint's negative path as '401 missing signature headers' (and stale timestamps as 401), and an external tester probing the negative path with an UNREGISTERED agent id gets 404 'agent not found' instead, then reports a contract mismatch. Two independent falses: the doc omits a status the server really returns, and the tester's mental model ('auth first') disagrees with the real check order.\n\nROOT CAUSE (derived by live probe, not by reading docs): the agent-scoped handlers run the per-agent signature gate BEFORE the existence lookup, but the gate itself contains BOTH a presence check and a timestamp-window check, so the existence check sits BETWEEN two auth checks. The real order is: (1) signature trio PRESENT? no -> 401 missing agent signature headers (any id); (2) agent EXISTS in the registry? no -> 404 agent not found (only reachable once a trio is present); (3) timestamp inside the +/-30s window? no -> 401 outside allowed window; (4) ed25519 signature verifies against the REGISTERED public key? no -> 401 signature verification failed. A registered agent with a stale timestamp therefore 401s, while an unregistered agent with the SAME stale timestamp 404s: the existence check wins because it runs before the timestamp/signature checks. The intuitive model ('all auth before existence') predicts 401 in both cases and is wrong.\n\nHOW IT WAS PINNED: start a fresh build env-isolated on a scratch port with signature enforcement at its default ON; register one agent with a dummy 64-hex ed25519 public key; then issue six requests against the inbox endpoint and record the status+body of each: ghost id with no headers (401), registered id with no headers (401), registered id with the trio but a bogus all-zero 64-byte signature (401 signature verification failed), ghost id with the trio (404 agent not found), registered id with a 900s-stale timestamp (401 outside window), ghost id with the same stale timestamp (404). The 404 appears only when a syntactically valid trio is present, and the stale-timestamp 401 appears only for a registered id: the two probes together separate the existence check from both auth sub-checks without reading a line of handler code.\n\nFIX SHAPE (docs-side, behavior unchanged): document the four-step precedence as a reader-executable claim and pin it with a docs-claims probe that observes the real 404 rather than asserting a literal. The gate needs teeth: the new probe's id-keyed case must return the live status code from the response, never a hardcoded 404, and the anchor must be an EXACT substring of the new doc sentence so removing the sentence fails the build. Prove both directions (doc sentence removed -> gate FAILS naming the claim; restored byte-exact -> green).\n\nVERIFICATION: the six-request matrix above is the acceptance evidence and is deterministic (no LLM, no clock dependence beyond a fresh timestamp and a deliberately stale one). Gate: run the docs check and confirm the new claim is reported verified (not xfail, not skipped) and the command exits 0.\n\nGENERAL RULE: when a handler mixes a cheap pre-auth check into an auth helper, the existence lookup lands mid-sequence and produces a status combination no doc predicts. Derive the order with a presence-controlled request matrix (headers absent / present-but-bogus / present-and-stale x subject exists / not) rather than from the middleware's name, then pin the observed order in the doc AND in an executable claim so neither side can drift alone.", "environment": "", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "http-auth-gate-check-ordering", "provider": "openrouter", "solved_at": "2026-09-20T02:27:41.249Z", "version": ""}