Skip to content

fix(agent-setup): the docs-link gate could report ok without probing the link (#549 audit) - #553

Merged
ZacxDev merged 1 commit into
mainfrom
fix/docs-link-probe-guards
Sep 11, 2026
Merged

fix(agent-setup): the docs-link gate could report ok without probing the link (#549 audit)#553
ZacxDev merged 1 commit into
mainfrom
fix/docs-link-probe-guards

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #549 (already merged as 7b7d5f1). An adversarial audit found five
defects in the guards around the managed block's ### Docs section, not in the
section itself. Every one was reproduced on merged main before it was fixed.

Context that changed since the audit: https://developer.civitai.com/apps/examples
now resolves 200. Re-verified here — all four links answer 200 at exactly the
spelling that ships, with zero redirect hops. The findings are still live,
because they are about the guard, which is now the only thing that would catch the
link dying later.


Red → green matrix

Every row is a real run. "before" = origin/main's agent_setup_docs_test.go
restored over the same mutated tree; "after" = this branch.

# Mutation Before After (exact assertion watched)
1 First link's host unresolvable + /apps/examples → known-404 path --- SKIPok (the dead link was never fetched) --- FAIL: DEAD LINK IN THE MANAGED BLOCK … url: …/apps/examples-does-not-exist status: 404, preceded by checked 3 of 4 link(s); 1 unreachable, 2 live, 0 moved, 1 gone, 0 undiagnosable
1b First link's host unresolvable only --- SKIPok --- SKIP: PARTIAL RUN: 3 of 4 link(s) checked, 1 unreachable at the transport layer. … so it reports SKIP rather than a pass that reads as 'all links verified'. — and the other three were probed
2a Local server answering 403 to every path, incl. mustNotResolve --- FAIL ×4 DEAD LINK IN THE MANAGED BLOCK … fix: correct it in internal/cmd/templates/agents-app.md (the wrong instruction) --- SKIP: CANNOT TELL whether these links are dead — they answered a status that a BLOCKED or FAILING EDGE produces just as readily as a broken page (401/403/407/429/5xx) + 🔴 This is NOT a dead-link finding. + NO link in this run answered < 300, so there is no positive control at all
2b One link 403, other three live same DEAD LINK text as a genuine 404 --- FAIL: same CANNOT TELL message + (At least one other link in this run answered < 300, so this prober is not being blocked wholesale — which is what makes this worth failing on rather than skipping.)
2c All links 404, no live link anywhere four DEAD LINKs, no caveat --- FAIL: four DEAD LINKs plus POSITIVE CONTROL ABSENT: 4 link(s) are reported gone above and NOT ONE link in this run answered < 300.
3 One link → local 301200 --- PASS, logging http://127.0.0.1:18733/apps/guide -> 200 --- FAIL: MOVED LINK IN THE MANAGED BLOCK — the page answers, but NOT at the spelling that ships. status: 301 location: http://127.0.0.1:18733/apps/guide/
4a New unledgered file spelling the URL (no such guard) --- FAIL: claudedocs/zz-mutant-unledgered.md spells one of the block's docs URLs and is NOT in docsURLSpellingLedger.
4b Same file renamed to claudedocs/handoff-zz-mutant.md --- PASS, logging 2 exempt dated record(s) — the exemption is exercised, not assumed
4c URL removed from a ledgered file (decisions/36) --- FAIL: docsURLSpellingLedger names claudedocs/decisions/36-… but it no longer spells ANY of the block's docs URLs.
4d Walk misrooted (repoRootDir(t)t.TempDir()) --- FAIL: CONTROL failure, not a finding: the walk read 0 file(s) … want >= 200positive control, count moved 696 → 0
5a allProjectShapesForTest kind list → {npm, npm, npm} --- PASS (4 shapes ≥ 3) --- FAIL: CONTROL failure, not a finding: allProjectShapesForTest returned 4 shape(s) covering kind(s) npm, but no-build, none is declared in agent_setup_project.go and never rendered.
5b Fourth projectKind constant added, no shape (nothing fired) --- FAIL ×2: the control above naming mutant-4th-kind, and projectKind "mutant-4th-kind" is declared … and rendered by NO shape in allProjectShapesForTest.
5c Shape with an undeclared kind --- FAIL: allProjectShapesForTest renders kind "bogus-undeclared", which is not declared as a projectKind constant …
5d declaredProjectKindRe broken --- FAIL: CONTROL failure … matched 0 constant(s) … want >= 3

Harness validation for rows 2–3: the local status servers were themselves probed
first (18731/anything -> 403, 18732/x -> 404, 18733/apps/reference -> 301 loc=…/apps/reference/, 18733/apps/guide/ -> 200) before any verdict was read
off them.

Finding 3's other half: a false measured claim, withdrawn

The comment at the foot of the file said /apps/showcase is 200 and
/apps/showcase/ 404s — true — "while /apps/guide/ is the other way round",
which reads as /apps/guide 404ing. It does not. Re-measured 2026-09-11 from this
host, probe User-Agent, no redirect following:

/apps/guide       301 -> http://developer.civitai.com/apps/guide/
/apps/guide/      200
/apps/reference   301 -> http://developer.civitai.com/apps/reference/
/apps/reference/  200
/apps/examples    200
/apps/examples/   404
/apps/showcase    200
/apps/showcase/   404
/llms.txt         200
/__must-404__     404   (negative control)

Note the redirect target's scheme: the hop lands on cleartext http, so a
following probe scores an https→http downgrade as a clean 200. The claim is
withdrawn, not replaced with a narrower one — five paths show four distinct
behaviours and there is no rule to state.

Finding 4: the grep count

Whole tree, ignored files included (find … | xargs gnugrep, not the
.gitignore-blind wrapper):

claudedocs/handoff-agent-setup-onboarding.md          4
claudedocs/decisions/36-agents-block-per-project.md   2
internal/cmd/agent_setup_docs_test.go                 2
internal/cmd/templates/agents-app.md                  1

9 lines, 4 files — the auditor's "four places" was a file count and is right;
"two edits and nothing else" was wrong, and was wrong on the day it was written,
since decisions/36 was added by #549 itself.

The replacement is not a narrower sentence. TestEveryDocsURLSpellingIsLedgered
walks the tree and asserts the file set both ways. The one exemption —
claudedocs/handoff- — is stated in the code with its reason (dated records are
evidence; correcting them falsifies the record) and is itself exercised by
mutation 4b.

Out-of-scope neighbour, called out separately

The audit flagged agent_setup_project_test.go:27-30: the comment claimed
allProjectShapesForTest is "derived from the projectKind constants' own list
rather than from a literal" while the code is a literal. It is fixed here,
because finding 5's fix needed the derivation anyway: declaredProjectKinds reads
the projectKind constants out of agent_setup_project.go's source (Go cannot
enumerate a string-constant set at runtime), and
TestAllProjectShapesCoversEveryDeclaredKind compares the two sets both ways.
Mutations 5b/5c/5d are its red-then-green.

Gates

$ make ci
go mod tidy
go vet ./...
go test ./...
ok  	github.com/civitai/cli	1.979s
ok  	github.com/civitai/cli/cmd/civitai	15.902s
…
ok  	github.com/civitai/cli/internal/cmd	33.744s
…
go build -ldflags "…" -o bin/civitai ./cmd/civitai

22 packages reported ok, 1 [no test files] — no package silently compiled to
zero tests.

$ nix-shell -p golangci-lint --run "make lint"
golangci-lint run
0 issues.

That zero is instrument-validated: with a planted ineffassign in the new test it
went red —
internal/cmd/agent_setup_docs_test.go:283:2: ineffectual assignment to lintProbe (ineffassign) /
make: *** [Makefile:30: lint] Error 1 — then back to 0 issues. once reverted.

gofmt -s -l . flagged 0 files, with 472 .go files present (so the zero is not a
zero-files zero).

Live probe on this branch:

$ CIVITAI_CHECK_DOCS_LINKS=1 go test -run TestBlockDocsLinksResolve -count=1 -v ./internal/cmd/
    https://developer.civitai.com/apps/guide/ -> 200
    https://developer.civitai.com/apps/reference/ -> 200
    https://developer.civitai.com/apps/examples -> 200
    https://developer.civitai.com/llms.txt -> 200
    checked 4 of 4 link(s); 0 unreachable, 4 live, 0 moved, 0 gone, 0 undiagnosable
--- PASS

Docs

  • claudedocs/decisions/36-agents-block-per-project.md: added the no-redirect
    re-measurement table, the withdrawn-claim note, the new guards, an "audit
    repairs" subsection, and corrected WHAT IS NOT ESTABLISHED — the page is
    live now, stated as a measurement of that day rather than a standing fact.
  • AGENTS.md item 36's trigger now reads "…or what its Docs section links and
    where
    ?", so a change to where a docs URL is spelled routes to the item.
    +10 bytes: 30,453 / 30,500. The ceiling was not raised.

Not verified

  • CI has not run yet at the time of writing; make ci here is a full clone, and
    make ci-shallow was not run (no test in this change reads git history).
  • The 403/301 cases are simulated with a local status server, not observed against
    developer.civitai.com. The Cloudflare 1010 block is quoted from the handoff,
    not re-triggered — deliberately, since re-triggering it means getting this host
    blocked.

🤖 Generated with Claude Code

https://claude.ai/code/session_018mJg86oQbTwuyPo5zcq3qP

…the link (#549 audit)

An adversarial audit of #549 found five defects in the GUARDS around the managed
block's `### Docs` section, not in the section itself. Each was reproduced on the
merged tree before it was fixed.

1. The gate could pass without probing the link it exists to check. The per-link
   loop did `case err != nil: t.Skipf(...)`, abandoning the whole run on the first
   transport error. With only the FIRST link's host made unresolvable and
   `/apps/examples` pointed at a known-404 path, the pre-change test printed one
   skip line and exited `ok` — the dead link was never fetched. Every link is now
   probed before anything is diagnosed; transport errors are collected per URL and
   a run that could not reach them all reports SKIP with `checked n of m`.

2. Negative control present, positive control absent. `mustNotResolve >= 400` is
   satisfied by an edge that 403s everything — which is also what makes every real
   link >= 400. Against a local server answering 403 to every path, the pre-change
   test emitted four `DEAD LINK IN THE MANAGED BLOCK … correct it in
   internal/cmd/templates/agents-app.md` errors, the exact wrong instruction; this
   host is known to hard-block non-browser agents with Cloudflare `error code:
   1010`. Statuses are now split into gone (404/410) and CANNOT TELL
   (401/403/429/5xx), with different messages, and the run skips rather than
   guesses when no link answered < 300.

3. The probe followed redirects while the comment claimed the spelling was
   measured. Go's default CheckRedirect follows up to 10 hops; `/apps/guide`
   301s to `http://developer.civitai.com/apps/guide/` — a different spelling AND a
   downgrade to cleartext http — which a following probe scores as a clean 200.
   CheckRedirect now returns ErrUseLastResponse and a 3xx is reported as MOVED,
   with its Location.

   The comment at the foot of the file also carried a false measurement: it said
   `/apps/showcase` is 200 while `/apps/showcase/` 404s (true) "while `/apps/guide/`
   is the other way round", implying `/apps/guide` 404s. It does not — it 301s.
   Re-measured all nine paths; the claim is withdrawn, not replaced with a
   narrower one, because the table shows no rule to state.

4. "Correcting the URL is those two edits and nothing else" was false when it was
   written — #549 itself added a third spelling, in
   claudedocs/decisions/36-agents-block-per-project.md. Measured over the whole
   tree, ignored files included: 9 lines in 4 files name
   developer.civitai.com/apps/examples. Replaced by
   `TestEveryDocsURLSpellingIsLedgered`, a bidirectional ledger that walks the
   repo and fails both when an unledgered file spells a docs URL and when a
   ledgered one stops. Dated `claudedocs/handoff-` records are exempt: they record
   what was true when written and are not corrected.

5. The three-branch control counted shapes, not kinds. `len(shapes) < 3` is
   satisfied by three shapes of one kind. The wanted set is now read from
   `agent_setup_project.go`'s own `projectKind` constants — which also repairs an
   adjacent comment the audit flagged: `allProjectShapesForTest` claimed to be
   "derived from the projectKind constants' own list rather than from a literal"
   while being a literal. `TestAllProjectShapesCoversEveryDeclaredKind` is the
   derivation that makes the sentence true.

Red-then-green matrix, `make ci` and `make lint` output are in the PR body.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018mJg86oQbTwuyPo5zcq3qP
@ZacxDev
ZacxDev merged commit ff7a211 into main Sep 11, 2026
13 checks passed
@ZacxDev
ZacxDev deleted the fix/docs-link-probe-guards branch September 11, 2026 16:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant