Skip to content

Fix all 42 findings from the 2026-06-10 codebase audit#31

Merged
dotwaffle merged 29 commits into
mainfrom
audit-fixes-2026-06-10
Jun 10, 2026
Merged

Fix all 42 findings from the 2026-06-10 codebase audit#31
dotwaffle merged 29 commits into
mainfrom
audit-fixes-2026-06-10

Conversation

@dotwaffle

Copy link
Copy Markdown
Owner

A full-codebase audit (multi-agent: 10 dimension finders, dedup, adversarial verification) confirmed 42 findings — 4 high, 16 medium, 22 low. This branch fixes all of them across 28 commits, one logical change per patch.

Highs

  • rest: redact the gated ixf_ixp_member_list_url inside eager-loaded edges — it leaked to anonymous callers via internet-exchanges / ix-prefixes / network-ix-lans (privacy)
  • grpcserver: cap ConnectRPC inbound messages at 1 MB (WithReadMaxBytes) — unary bodies were unbounded incl. decompression (gzip-bomb OOM)
  • sync: fetch the ?since=<cursor> tombstone window during full-mode staging — daily forced-full cycles permanently discarded the window's deletes
  • graphql: fan-out-aware complexity costing — nested unpaginated edge lists could materialize millions of rows under the old field-count limit

Mediums (16)

pdbcompat parity (since gate/boundary/ordering, limit/skip validation + clamp removal, location-filter icontains coercion, ISO 8601 time filters with day-window semantics, case-insensitive fold-routed __in), tombstone visibility (depth≥2 sets, all web-UI queries, networkByAsn), caching/Vary/WriteHeader-order fixes, replica readiness-latch recovery, full-mode reconcile-all (upsert skip-gate bypass), pooled SQLite page-cache sizing, global in-flight response-budget admission, GraphQL CollectFields batching, CI drift gate covering allowlist_gen.go.

Lows (22)

/sync 409-on-busy + WAF retry short-circuit + status-write error logging, serve-failure OTel flush, REST error detail passthrough, OpenAPI error-schema accuracy, fly.toml /readyz checks, Dockerfile.prod CGO_ENABLED=0, DEVELOPMENT.md procedure rewrite, unifold œ/ð/ı, divergence-registry completeness, dead-code removal, shell-completion fixes, loadtest upstream guard + ramp reporting.

Verification

go build, go vet, golangci-lint (0 issues), and go test -race ./... all green on the full tree; every behavioral fix carries a regression test (privacy e2e, parity tests with upstream citations, tombstone-window worker tests, complexity limit tests, etc.).

Post-deploy

Trigger one ?mode=full sync to converge stale _fold values and heal orphan-nulled FKs (full mode now genuinely reconciles). Full cycles issue 13 extra ?since= requests (~7–13 s of rate-limit budget).

🤖 Generated with Claude Code

dotwaffle and others added 29 commits June 10, 2026 20:00
The global MaxBytesBody middleware deliberately exempts all
/peeringdb.v1.* paths so server-to-client streaming responses are not
truncated, but nothing replaced that cap at the connect protocol layer.
connect-go's default ReadMaxBytes is zero, which permits messages of any
size, and the limit also governs the decompressed size of compressed
requests. A single oversized or gzip-bombed unary POST to any of the
unary endpoints therefore buffered fully in heap inside the 30s read
window — enough to OOM-kill a 256 MB replica with one request.

Add connect.WithReadMaxBytes(maxRequestBodySize) to the shared handler
options, extracted into connectHandlerOpts so the wiring is testable.
Request messages on these services are small filter/pagination structs,
so the 1 MB cap shared with the HTTP-level middleware is generous.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
restFieldRedactMiddleware only buffered /rest/v1/ix-lans* responses and
only inspected top-level JSON keys. entrest eager-loads the ixlan edge
unconditionally for both list and detail operations, so every endpoint
that embeds an ixlan — internet-exchanges, ix-prefixes, network-ix-lans
— returned the tier-gated ixf_ixp_member_list_url unredacted under
edges.ix_lans / edges.ix_lan, including to anonymous callers on the
default public deployment.

Broaden the middleware to all /rest/v1/ paths and locate gated objects
by a recursive walk keyed on the _visible companion's presence rather
than by response shape. This also retires the restListWrapperKey
constant whose lock-step coupling to entrest's PagedResponse tag was
itself a documented leak hazard.

New E2E sub-tests cover the embedded-edge shapes: anonymous callers
must not see the gated URL under edges.ix_lans on internet-exchange
detail and list responses, and users-tier callers must still see it
there (guarding the walker against over-redaction).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A bare /api/<type> list contains only status='ok' rows (upstream
filters bare lists), and committing a full snapshot advances the
derived MAX(updated) cursor past the pre-cycle window. Every full-mode
fetch — the daily PDBPLUS_FULL_SYNC_INTERVAL escalation and the
per-type incremental-fallback — therefore silently discarded any
deletes that landed upstream inside that window. With
inference-by-absence deliberately removed, nothing ever healed those
rows: they stayed 'ok' across all five API surfaces forever and never
appeared in our own ?since= exports.

Stage a follow-up ?since=<cursor> fetch on top of the bare snapshot
whenever the table has a non-zero cursor. The scratch table's INSERT
OR REPLACE is keyed on id, so window rows — including tombstones — win
over their bare-list versions. In explicit full mode a window-fetch
failure fails the type so the cycle retries instead of advancing the
cursor past unseen deletes; on the incremental-fallback path the fetch
is best-effort (it is the same request shape that just failed) and a
second failure is logged and tolerated so the fallback keeps its
purpose.

StreamAll's since-pagination now stops on a short page rather than
only on an empty one. Upstream applies its status filters before
limit/skip, so every non-final page is exactly pageSize rows; this
saves the trailing empty-page request on every incremental fetch and
keeps naive single-payload test stubs terminating.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gqlgen's default complexity charges 1 per field regardless of list
fan-out, so FixedComplexityLimit(500) bounded the number of fields in
the query text, not the rows it materializes. Nested unpaginated edge
lists multiply: internetExchanges(first:1000) expanding ixLans ->
networkIxLans -> ixLan -> networkIxLans costs ~10 units under default
costing yet materializes millions of ent rows plus their JSON — an
OOM-kill on the 256 MB replicas.

Register complexity functions for every Relay connection and
offset/limit flat list (multiplying by the requested page size, with
the resolvers' own defaults) and for every unpaginated entity edge
list (multiplying by an order-of-magnitude weight for the live
corpus's average per-parent cardinality, with the heavy-tailed
ixlan.networkIxLans member list weighted highest). The limit budget
rises from 500 to 1,000,000 because the unit changes from fields
mentioned to approximately fields-of-rows fetched: a full first:1000
page of networks costs ~50k and passes, a single exchange's complete
member list costs ~30k and passes, and the nested-expansion-of-
everything shapes are rejected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The depth>=2 branches of the org, net, ix, carrier, and campus getters
eager-loaded their reverse sets with bare With*() calls, so the DEFAULT
detail shape (depth=2) serialized status='deleted' rows into the _set
arrays as if they were live — re-publishing deleted POC contact data
indefinitely, among others. Upstream prefetches nested sets filtered to
status="ok" (peeringdb_server/serializers.py:928), and our own depth-1
ID-list builders and the ixlan getter already filtered StatusIn("ok",
"pending"), so the unfiltered eager loads were both a parity break and
an internal inconsistency.

Filter every depth>=2 reverse-set eager load with StatusIn("ok",
"pending"), matching the live-validated depth-1 choice. To-one FK
expansions stay unfiltered, as before (an ok row's parent is ok by
sync-order construction).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three divergences on the incremental-update path that peeringdb-py
style pollers depend on:

The since matrix activated for any parseable value, but upstream gates
it with 'if since > 0' (rest.py:696) — ?since=0 there serves the plain
status='ok' list, while the mirror returned the entire tombstone corpus.
ParseSinceParam now treats since<=0 as absent.

The window filter used updated >= since, but django-handleref's since()
filters strictly greater. A client polling with the max updated value
it had already seen re-received every boundary row on each poll.
applySince now uses GT; updated__gt alone subsumes created__gt because
created <= updated on every row.

Since responses kept the newest-first default ordering, but upstream
returns incremental updates ordered by updated ascending so pollers can
resume from the last row they processed. A listOrder helper now picks
updated-ascending (id tiebreak) whenever ?since= is present, across all
13 list closures.

New parity sub-tests lock each behaviour with upstream citations; the
existing since=0 test fixtures move to since=1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No web-UI query filtered on entity status, so soft-deleted tombstone
rows — written by the sync worker for upstream deletions and never
garbage-collected — rendered as live data everywhere: search results,
the per-type listing pages, detail pages reached by ASN or ID, all
lazy-loaded detail fragments, and the comparison view. The templates
never render the status column, so a deleted entity was
indistinguishable from a live one, and the UI silently disagreed with
the same deployment's /api/ surface about what exists.

Add a StatusIn("ok", "pending") predicate to every entity query in
internal/web (52 sites), filtering each traversal hop that produces
rendered rows (e.g. ix -> ixlan -> netixlan participants). List/count
pairs keep identical predicates, mirroring the pdbcompat
shared-predicates invariant. The sync_status freshness reads are not
entity tables and stay unfiltered, as do WithX() eager-loads that only
resolve display labels for already-filtered rows.

The regression test seeds ok + deleted siblings and locks: search
excludes the deleted network, the deleted network's ASN 404s while the
live sibling renders, and the IX participants fragment excludes a
deleted netixlan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ParsePaginationParams silently ignored non-numeric limit/skip values
(treating them as absent) and clamped explicit limits above 1000 down
to MaxLimit. Upstream 400s non-numeric values (rest.py:490-497,
"'limit' needs to be a number") and slices qset[skip:skip+limit] with
no upper cap (rest.py:734-735). The silent-ignore turned a typo'd
limit into a full-table dump; the clamp silently truncated every page
for clients paginating with windows larger than 1000 — rows past the
clamp were permanently skipped with nothing in the envelope to signal
it.

Return 400 application/problem+json naming the offending parameter for
non-numeric (and negative) limit/skip, and honour large explicit
limits unmodified — the response-memory budget remains the actual cost
bound, exactly the rationale that removed the old 250-row default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Upstream special-cases bare location filters before generic handling
(rest.py:562-574): address1, city, and state become <field>__icontains
substring matches, and country becomes __iexact for 2-char values or
__icontains for longer ones. The mirror routed all bare string filters
through exact matching, so GET /api/fac?city=Frankfurt returned only
rows whose city is exactly "Frankfurt" while upstream also returns
"Frankfurt am Main" — geographic tooling ported from upstream
silently got fewer rows with no error.

Coerce the operator in buildLocalPredicate, after the field-existence
check. Only bare local filters coerce: upstream's special-case list
matches the raw param name, so explicit operator suffixes and relation
prefixes take the generic path there too. The 2-char country case
needs no rewrite because the exact builder's string branch is already
case-insensitive. The folded city columns keep their _fold routing via
the existing buildContains path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Time-typed filters (created/updated/rir_status_updated/...) parsed
values with an epoch-seconds-only parser, so every upstream-style
query like ?updated__gt=2024-01-01 returned 400 — no time-filter
syntax worked against both systems, and bare date equality (a natural
query) was unexpressible.

parseTimeValue now accepts the ISO 8601 layouts DRF's DateTimeField
accepts upstream (date-only, datetime with T or space separator,
RFC 3339 with offset) alongside epoch seconds, and carries upstream's
two semantic layers (rest.py:619-658): a bare 10-char date in gt/lte
gets its time forced to end-of-day (updated__gt=2024-01-01 means
"after that whole day"), and bare date equality becomes a
[00:00, 24:00) day-window range, mirroring the __startswith rewrite.

?since= deliberately keeps the strict epoch parser (parseEpoch):
upstream coerces since with int(), so an ISO value 400s there and must
keep 400ing here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
buildIn's string branch compared raw CSV values against the column
under SQLite's BINARY collation, so __in was case- and
diacritic-sensitive while every sibling operator on the same field
(exact, contains, startswith) folds case and routes folded fields
through the <field>_fold shadow column. ?name=decix matched "DECIX"
but ?name__in=decix missed it. Upstream applies unidecode to ALL
filter values including __in (rest.py:576) and matches under MySQL's
case-insensitive collation.

Thread the folded flag into buildIn; string lists lower-case each
element (after unifold.Fold when the field is folded) and compare
LOWER(col) — or LOWER(<field>_fold) — against the json_each set. Int
lists keep the bare numeric comparison.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three related caching-header defects from the 2026-06-10 audit:

The caching middleware stamped public Cache-Control (sync interval +
120s) and the sync-keyed ETag unconditionally before dispatch, so
every 400/404/413/500 was publicly cacheable for over an hour, and
/healthz and /readyz responses — including 503s — could be pinned by
shared caches. Worse, a conditional GET with a matching If-None-Match
returned 304 from the middleware without ever running the readiness
probes, so revalidating monitors were told their cached 200 was still
valid on an unhealthy node. Error responses now pass through a
cacheStripWriter that downgrades to no-store at WriteHeader time for
any status >= 400, and /healthz + /readyz join the no-store skip list
(which also bypasses the 304 short-circuit).

Web render paths and the root discovery handler called
Header().Set("Vary", ...), clobbering the Vary: Accept-Encoding that
gzhttp adds before dispatch — with public Cache-Control on the same
responses, a shared cache could replay a gzipped variant to clients
that never sent Accept-Encoding. All sites now Add instead of Set,
and the root handler's default branch (which negotiates on both
Accept and User-Agent) gains the Vary it was missing entirely.

Web 404/500 pages called WriteHeader before renderPage set
Vary/Content-Type; net/http drops header mutations after WriteHeader,
so non-gzip clients got no Vary and a body-sniffed text/plain
Content-Type on JSON problem bodies. The status now travels in
PageContent.Status and renderPage commits it after the headers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The generated-code drift check scoped its diff to ent/ gen/ graph/
internal/web/templates/, but go generate also rewrites
internal/pdbcompat/allowlist_gen.go — the security-load-bearing /api
traversal allowlist that CLAUDE.md and DEVELOPMENT.md both claim the
gate protects. Editing ent/schema/pdb_allowlists.go without
regenerating shipped a stale allowlist through CI silently.

Add the file to the diff pathspec and stage intent for untracked files
first so newly created generated files fail the gate instead of being
invisible to git diff.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 13 hand-written *List resolvers and NetworkByAsn built bare ent
queries and returned query.All(ctx) without CollectFields, so every
nested edge selection fell into ent's generated per-row lazy-load
path: networksList(limit:1000){organization{name}} issued 1 + 1000
sequential SQLite queries, each through database/sql + otelsql — and
when sampled, 1001 spans toward Tempo's per-trace cap. The relay
connection resolvers were already safe because entgql's Paginate calls
CollectFields internally.

Chain CollectFields onto each query so requested edges eager-load in
O(edges) batched queries. NetworkByAsn additionally gains the
StatusIn("ok", "pending") filter the /api/ and /ui/ surfaces
already apply — the curated by-ASN lookup no longer resolves
soft-deleted tombstones.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Worker.synced gates every data route via readinessMiddleware and the
gRPC health poller, but it was evaluated from sync_status exactly once
at StartScheduler entry. A replica that booted before the primary's
first successful sync — fresh-fleet bootstrap, wiped-primary recovery,
or a transient read failure at boot — latched unready permanently:
503 on all data routes and NOT_SERVING health for the life of the
process, even after LiteFS replication delivered a fully-synced
database.

While the latch is unset, the replica heartbeat now re-reads
sync_status each interval and flips to ready when a successful sync
appears. The read is a single local-SQLite row, paid only while
unsynced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cache_size is per connection, so the 32 MB setting multiplied by the
pool: MaxOpenConns=10 capped page cache alone at 320 MB — more than an
entire 256 MB replica VM — and the 5 idle connections retained up to
160 MB for ConnMaxLifetime after a read burst. The sizing comment
reasoned only about the primary's single-connection bulk-upsert burst
under the 512 MB cap; replicas run the same Open() and serve the
read-heavy full-dump traffic that actually warms ten separate caches.

8 MB per connection bounds the pooled worst case at 80 MB while still
quadrupling SQLite's ~2 MB default for the upsert transaction's page
reuse.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
skipUnchangedPredicate's godoc claimed full-mode runs ignore the
updated-timestamp gate and reconcile completely, but the predicate was
attached unconditionally in all 13 upsert closures — no mode ever
reached upsert.go. Rows the sync mutated locally without bumping
updated (facility campus_id or netixlan side FKs nulled by the orphan
filter) therefore never re-converged with upstream: excluded.updated
equalled the stored value, so the DO UPDATE was skipped on every
subsequent cycle including the daily forced full whose documented
purpose is exactly this reconciliation. Newly added _fold columns
would likewise never backfill on existing rows.

Carry a reconcile-all marker on the cycle context (cycle-scoped data,
set once in syncCycle for full-mode runs) and have
skipUnchangedPredicate emit an always-true UpdateWhere when present,
keeping the 13 OnConflict call sites uniform. Incremental cycles keep
the optimization and its documented bounded same-second-drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CheckBudget gated each list request in isolation against the full
PDBPLUS_RESPONSE_MEMORY_LIMIT, with no process-wide accounting of
in-flight responses. serveList fully materializes results before
streaming, and fly.toml's Fly-Proxy defaults allow 200 concurrent
requests per replica — so two concurrent near-budget dumps, each
individually under the 128 MiB budget, could jointly materialize more
than an entire 256 MB replica and OOM it.

Admission now also charges the request's estimated bytes against a
shared atomic in-flight pool: when the pool would exceed the budget
the request is rejected with 503 + Retry-After: 1 instead of stacking,
and the charge is released when serveList returns. The architecture's
413 semantics for single oversized requests are unchanged — the 503
is specifically "try again shortly, the server is busy serving other
large responses".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The short-page break added to StreamAll's since-pagination left
TestFetchAllRateLimiter and TestFetchAllPagination expecting the old
trailing empty-page request: their stubs returned fewer than pageSize
rows on every page, so the request counts and rate-limit timing they
asserted no longer matched. Serve full pages with a short final page
and update the expectations to the new (one fewer request) contract.

Also drop the three FetchType tests ahead of the dead FetchType
removal in a following commit — they were the function's only callers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four operability gaps from the 2026-06-10 audit's low-severity batch:

A sync trigger arriving while a cycle was already running was dropped
by the running-CAS guard with a silent nil return — the operator's
?mode=full escape hatch could no-op invisibly. The guard now logs the
drop with the requested mode and returns ErrSyncAlreadyRunning;
SyncWithRetry propagates it without entering the retry ladder, and the
new Worker.Running() probe lets the HTTP handler answer 409.

SyncWithRetry retried into upstream WAF blocks; IsWAFBlocked existed
with a godoc claiming the worker used it, but had zero callers. The
ladder now short-circuits on WAF detection — retrying into a WAF only
digs the hole deeper.

The per-chunk backfill pre-pass ran even with the backfill cap set to
0 (backfill disabled), emitting misleading 'fk backfill cap reached'
warnings and rate-limited metrics every cycle. It now returns early.

Terminal sync_status writes swallowed errors entirely, leaving rows
stuck 'running' with no signal; failures now log at ERROR with the
status id and outcome. Comments claiming dropped orphans are 'retried
next cycle' were corrected: recovery comes from the next full-mode
cycle, which since this branch re-fetches every row, stages the
tombstone window, and bypasses the upsert skip gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five fixes from the 2026-06-10 audit's low batch, all in the main
package:

On-demand syncs (POST /sync) ran bare SyncWithRetry with none of the
demotion monitoring the scheduler's cycles get — a node demoted
mid-cycle kept burning upstream PeeringDB quota through the rest of
the cycle and retry ladder. They now run under the same 1s-poll
demotion monitor pattern.

POST /sync answered 202 Accepted even when the worker would drop the
trigger because a cycle was already in flight; the handler now probes
Worker.Running() and answers 409 with a JSON conflict body.

A ListenAndServe failure called os.Exit(1) inside the serve goroutine,
skipping every deferred cleanup — including the OTel flush carrying
the log records that explain the failure. Serve errors now propagate
through a channel to main's normal return path so the defers run.

restErrorMiddleware rewrote every entrest error to a generic
problem+json body, discarding the validation detail clients need to
fix their request. 4xx responses now carry entrest's error detail;
5xx detail stays generic (no SQL/driver internals on the wire).

The served OpenAPI spec documented entrest's error schema, which the
middleware guarantees is never emitted. The spec is patched at startup
to declare application/problem+json error responses instead. The
startup warning (and CONFIGURATION.md) describing an empty
PDBPLUS_SYNC_TOKEN as leaving /sync open now states the real
fail-closed behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fly.toml's HTTP health check hit /healthz, which always returns 200 —
so the documented Fly Proxy exclusion of cold-syncing replicas never
happened; hydrating machines took traffic and served 503s. The check
now hits /readyz, which fail-closes during hydration and on stale
data. DEPLOYMENT.md updated to match.

Dockerfile.prod built without CGO_ENABLED=0, contradicting the
documented pure-Go build and diverging from the dev image.

DEVELOPMENT.md's 'Adding a new ent field' procedure told contributors
to hand-edit ent/schema/{type}.go — a file cmd/pdb-schema-generate
rewrites on every go generate, silently stripping the edit. Rewritten
around the real paths: schema/peeringdb.json for upstream-derived
fields, sibling-file mixins for local ones.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Upstream's unidecode folds these code points but NFKD leaves them
intact, so ASCII queries failed to match affected names through the
_fold shadow columns: a search for 'oeuvre' missed 'Œuvre', 'd' forms
missed Icelandic eth names, and Turkish dotless-i names were
unreachable. Add the three pairs to the hand map with table-driven
coverage plus an end-to-end fold-filter round trip.

Existing _fold column values containing these runes stay stale until
their rows re-upsert; the next full-mode cycle (which now bypasses the
upsert skip gate) converges them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
toMap serialized every nested-set element through a JSON
marshal+unmarshal round trip on the default depth-2 detail path. It
now reuses the cached reflect field map from the search path, with an
encoding/json-mirroring emptiness check so omitempty behaviour — load-
bearing for the redacted ixf_ixp_member_list_url key — is preserved
byte-for-byte (locked by a wire-equivalence test).

Also close three gaps in the divergence registry contract: the /api/
error envelope (RFC 9457 problem+json instead of upstream's
{"meta":{"error":...}}) was an intentional divergence registered
nowhere; it now has a DIVERGENCE_ parity test and a Known Divergences
row. The poc_set-privacy and fold-window rows claimed guarding tests
that did not exist; both are now test-locked, so docs/API.md's 'every
row has a guarding test' claim holds again. The ligature list in the
Validation Notes gains the new unifold pairs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The grpcserver stream-cursor wire codec (encodeStreamCursor,
decodeStreamCursor, parseCursorTime) had no production callers and its
doc comments described a page_token wire contract that does not exist
— the cursor is in-memory state inside StreamEntities only. The type
doc now says so.

peeringdb.FetchType had zero callers outside its own tests; one doc
comment claimed a /readyz role it never had.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pdb() wrapper in both the bash and zsh scripts expanded arguments
with "$@", so 'pdb asn 13335' ran curl with two separate URLs — the
bare ID fetched as a second request against the current directory
host. Join arguments with IFS=/ instead. The zsh _arguments spec was
single-quoted, so $subcmds never expanded and completion offered the
literal string. Tests assert the script content and (hermetically,
with a stubbed curl) the URL a real bash/zsh constructs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The upstream-host refusal (never point the generator at
www.peeringdb.com, whose 1 req/hour/IP cap would block the operator)
only ran in ramp mode; endpoints, sync, and soak could still target
upstream. The guard now runs right after flag parsing for all modes.

Ramp's no-inflection path discarded every measured step except the
baseline, so a probe that never found its inflection point reported
almost nothing. All measured steps now appear in the markdown table,
with the final row labelled max-concurrency.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gqlgen's config loader takes the target package name from the
alphabetically-first .go file in the exec/model directory.
collectfields_test.go (package graph_test) sorted before
custom.resolvers.go, so 'go generate ./graph' — and therefore CI's
drift gate — failed with 'exec and model define the same import path
(graph vs graph_test)'. Rename the file to resolver_collectfields_test
so the first file stays in package graph, and record the sharp edge in
CLAUDE.md. Full 'go generate ./...' now converges with zero drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dotwaffle dotwaffle merged commit c9f2827 into main Jun 10, 2026
2 checks passed
@dotwaffle dotwaffle deleted the audit-fixes-2026-06-10 branch June 10, 2026 22:07
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