Repository navigation
fix: keep upstream provider identity out of catalogue metadata (#1284) - #1300
Conversation
GET /v1/models served, live against api-hive.scubed.co, a description field
naming the upstream serving provider for two aliases:
{"id":"hive-stt", ..., "description":"Serverless speech-to-text (Groq Whisper) for /v1/audio/transcriptions."}
{"id":"hive-tts", ..., "description":"Serverless text-to-speech (Groq PlayAI) for /v1/audio/speech."}
Both strings were seeded into public.model_aliases.summary by
20260717_02_voice_groq_stt_tts.sql and published verbatim. Every
provider-blindness guard this repo had sat on an error path, so catalogue
metadata reached the customer unexamined.
The guard is at the boundary, not on the two rows. Three customer-facing
endpoints render the same rows through two wire shapes: GET /v1/models and
GET /catalog/models on edge-api, and GET /api/v1/catalog/models on
control-plane, the last of which is reachable without a bearer token. Each
builder now scrubs display name, summary, owned_by and capability badges on
the way out, so a row seeded later cannot leak the same way, and edge-api
repeats the check after decoding the snapshot per the both-boundaries rule in
CLAUDE.md.
The vocabulary is deliberately narrower than the eighteen-word error-path list
in apps/edge-api/internal/errors/provider_blind.go. It matches serving
infrastructure (gateways, aggregators, hosting clouds and internal route
slugs), not model vendors: deepseek-v4-pro and "Deepseek V4 Pro" are shipped
customer-facing names, and the error-path list would rename two live models and
blank their descriptions. A test asserts that non-regression directly.
A migration repairs today's two rows so the useful sentence survives rather
than being dropped by the boundary, and fixes copy that was stale on its own
terms: Groq deprecated playai-tts on 2025-12-31 and the route has pointed at
canopylabs/orpheus-v1-english ever since. It rewrites a row only while that row
still names a provider, so it is safe to re-run and cannot overwrite curated
copy, and it raises a NOTICE for any other customer-facing row that still names
one.
Verified on a throwaway Postgres with all 109 migrations applied: both rows
repaired, a second run of the file updates nothing, and a scan of every text
and jsonb column in the public schema finds no remaining provider name on a
customer-reachable field.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (8)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe change adds provider-identity detection and metadata redaction to control-plane catalog builders and the edge snapshot funnel. It adds regression tests for catalog responses and alias handling. A migration repairs voice summaries and reports remaining provider-bearing catalog fields and alias IDs. ChangesProvider-blind catalog enforcement
Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: 🔵 Low · up to The PR removes provider names from catalogue descriptions and related metadata, but alias IDs remain unchanged for compatibility; any provider-named alias would therefore remain publicly visible and needs explicit owner follow-up. The change is otherwise mergeable with normal checks. Sequence Diagram(s)sequenceDiagram
participant CatalogService
participant fetchSnapshot
participant redactSnapshot
participant ModelsAPI
CatalogService->>fetchSnapshot: catalog snapshot
fetchSnapshot->>redactSnapshot: decoded snapshot
redactSnapshot-->>fetchSnapshot: provider-blind snapshot
fetchSnapshot-->>ModelsAPI: response data for /v1/models and /catalog/models
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Linked Issues checkExplanation The changes satisfy issue Full details: Docstring CoverageExplanation Docstring coverage is 70.97% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 31 functions across 7 files. (1 skipped: 1 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
|
sakibsadmanshajib
left a comment
There was a problem hiding this comment.
Adversarial review, pipeline mode
Independent review of the whole diff, plus a census pass done from scratch rather than from the PR body. This PR had no review at all before this one.
Verdict: do not merge yet. Two one line changes stand between this and a boundary that holds. Everything else here is a follow up.
The change is right in shape and the diagnosis is right. Boundary plus row repair is the correct pair, both builders and the edge funnel are the correct three call sites, the mutation check is real (I reran the touched packages and they pass, and the seven assertions are load bearing), and putting the guard on catalogue metadata closes a family that every existing guard in this repo missed because they all sit on error paths.
Narrowing the vocabulary: right call, wrong boundaries
Not reusing the eighteen word error path list is correct, and I want to be explicit about that because it is the decision most likely to be second guessed. deepseek-v4-pro and "Deepseek V4 Pro" are shipped customer facing names from 20260822_02_catalog_alias_restructure.sql; the error path list would have renamed two live models and blanked their descriptions, and the invariant really is "the customer cannot tell who serves the request", not "no AI company may be named". TestRedactAliasLeavesModelVendorNamingIntact holds the right line.
The list as written is too narrow in a way that is mechanical rather than editorial: the trailing \b loses GroqCloud, and \btogether\.?ai\b loses Together AI while its three sibling patterns all accept a separator. It is also too wide on four items (perplexity is an evaluation metric, hyperbolic and bedrock are English words, route- matches route-planning), each of which silently blanks a description with no customer visible signal. Detail inline.
Is display_name guarded
Yes, in both modules, and 'Openrouter Auto (Task Aware)' is matched. But the fallback then writes the alias id into that field, and for that specific row the alias id carries the same identity, so the redaction produces three occurrences of the provider name where the raw row had one. Demonstrated with a probe test on this branch, output inline.
Census
I walked it independently. /v1/models, /catalog/models and /api/v1/catalog/models all funnel through the three guarded call sites; the console model detail page reads /api/v1/catalog/models (apps/web-console/lib/control-plane/client.ts), so it inherits the fix; CatalogSnapshot.Routes and .AliasPolicies are json:"-" and edge's Snapshot has no equivalent field; orpheusVoices in apps/edge-api/internal/audio/handler_voices.go is six first names with no provider token, which I checked because the voice surface is where the leak was and PlayAI style voice ids would have been a second instance; the cached repository caches raw aliases and redaction happens after the cache read, so there is no stale bypass; redactAlias is called on a loop copy rather than mutating the cached slice, which is the trap that was worth avoiding and was avoided.
The gaps I found are alias_id (above) and the static list versus the runtime custom_providers table (inline).
Review streams
- Plain adversarial pass: RAN. Findings above and inline.
- CodeRabbit CLI: RAN, after the earlier repo wide limit reset. Two major findings, both incorporated: the unsanitized
alias_idreaching the wire (same root cause I found independently) andredactSnapshotmutating the caller's backing arrays. Its suggested remedy for the first one, renaming or mapping the id, I disagree with and have not passed on; the invocation handle should stay stable and the fallback is the thing to fix. - CodeRabbit PR bot: SKIPPED. Rate limited on the automatic run at 21:34Z, re-triggered at 22:27Z, refused again ("Review rate limited", plus it does not re-review already reviewed commits). Not a pass.
- Codex: SKIPPED. Both the connector on this PR and the local CLI report the usage limit hard exhausted; the CLI names Sep 10 as the reset. Not a pass. The pipeline calls for this stream on security diffs, so it is an outstanding gap rather than a satisfied requirement.
ecc:code-reviewand theadversarial-pr-reviewskill could not be invoked from this reviewer's toolset (no Skill tool available to it). The streams above were run directly instead.
Findings by severity
| Severity | Finding |
|---|---|
| HIGH | Display name falls back to a leaky alias id, tripling the leak on openrouter-auto |
| HIGH | Trailing \b loses GroqCloud; together\.?ai loses Together AI |
| MEDIUM | Migration census omits alias_id, the only column no guard repairs |
| MEDIUM | Three divergent vocabularies inside the migration, none equal to the Go one |
| MEDIUM | Static list versus admin managed custom_providers, guard stops tracking the deployment |
| LOW | Per request unbounded logging on an unauthenticated endpoint |
| LOW | redactSnapshot writes through the caller's backing array |
| LOW | visibility <> 'internal' drops NULL rows |
To merge
Fix the two HIGH items, both inside files this PR already creates, and extend one whole payload test to use a leaky alias id so the assertion covers the path. Add alias_id to the migration census and align the migration's three vocabularies with the Go one while you are in the file; those are copy and paste and cheaper now than as a follow up PR. File the custom_providers gap as its own issue. The LOW items can ride along or wait.
Migration numbering was not raised, deliberately: apply-migrations.sh orders by full filename under LC_COLLATE=C, the ledger is keyed on filename, and eight duplicate prefixes already exist on main.
…ew of #1300) Two defects in the boundary guard, both found by review of this PR. First, the display name fallback wrote the alias id into the field it had just scrubbed, without checking whether the id was itself what tripped the check. On public.model_aliases row 'openrouter-auto', which 20260822_30_openrouter_auto_variable_pricing.sql seeds and whose own comment says flipping it to public is a one line follow up, that turned one occurrence of the provider name on the wire into four: models[].id and catalog[].id are published contract, and the fallback added models[].name and catalog[].display_name on top. Redaction was amplifying the leak it exists to remove. The fallback now degrades to the empty string when the alias id itself names a provider, and still returns the readable id when it does not. The whole payload test that should have caught this used a clean fixture id, so its assertion never met a leaky one. Both modules now carry a fixture built on the real 'openrouter-auto' row, asserting over the serialised payload that the published id is the only carrier of the name. Second, two word boundaries made the pattern miss the spellings a human is most likely to write, including the vendor this issue is about. The trailing boundary in the token alternation lost GroqCloud, which is Groq's own product name, because q followed by C is word character to word character. The same shape lost OpenRouterAI, FireworksAI and CerebrasCloud. The token list now anchors on the leading boundary only, which costs nothing because none of the eleven tokens prefixes a common English word. The multiword patterns keep their trailing boundary, because together and azure are common English words. Together AI, the company's own spelling, is now matched, and Google Vertex and Azure ML are added. The migration carried three vocabularies, none equal to the Go one, so a summary edited to name NVIDIA NIM was neither repaired nor reported while the boundary dropped it on every request, arriving silently at the permanently blank description the migration exists to prevent. All three are now one plpgsql constant, kept identical to the Go pattern. The census gained a pass over alias_id, the one column no guard ever repairs, deliberately without the visibility filter so 'openrouter-auto' is named before somebody flips it public. The visibility predicate moved to `is distinct from` so a NULL visibility is no longer silently dropped. Also folded in from the same review: redactSnapshot no longer writes through the caller's backing array, and both boundaries log a given redaction once per process rather than once per request, which matters because /catalog/models is unauthenticated and uncached. Follow up filed separately: the vocabulary is a compile time literal while public.custom_providers is admin managed runtime data.
Review found four statements the page published that the system does not
actually keep, plus two gaps in how the page fails and what it admits to
not covering.
Retention. The blanket "the gateway does not store the content of your
requests or responses" was false for three endpoints behind the same
bearer key: batch jobs persist the customer's verbatim request bodies as
the input file and the verbatim upstream responses as the output file,
file uploads are stored by definition, and RAG documents are stored in
full alongside the text chunks in public.rag_chunks.content. The page now
scopes the no-content statement to the synchronous relay path and names
the three surfaces that do store content, with the deletion route for
each.
Structural claim. "There is no field in that record a body could land in"
rested on UsageEventRow in the console, which is a read projection, not
the table. public.usage_events carries internal_metadata jsonb,
customer_tags jsonb and provider_request_id, and what keeps content out
of internal_metadata is usage.RedactMetadata, a key-name denylist that
passes through any value under a key it does not list. The sentence is
now phrased as behaviour ("message content is stripped from that metadata
before the record is written"), which is what the code actually does.
Provider blindness. "Hive keeps that identity out of every customer-facing
surface" is disproved two clicks away by catalogue summaries that name the
vendor (issue #1284, fix still open as PR #1300). Narrowed to the two
surfaces where it is enforced: error responses and this page.
Alias to route. "Every alias resolves to exactly one upstream route" is a
property of today's seed data, not of SelectRoute, which returns
FallbackRouteIDs with no same-provider constraint. Softened to one route
per request, with the caveat that an alias can have several eligible
routes.
Also added, both raised in review: the data-collection posture, which is
enforced by provider.data_collection deny on the free routes and on
nothing else and is the most decision-relevant fact on the page for a
regulated buyer, and a closing card naming what the page does not cover
(region, personnel access, breach notification, chat storage, and that the
page is English only while the navigation is translated).
A failed catalog fetch no longer renders as an empty catalog, which could
read as "no models, so nothing leaves".
Tests. Eight new assertions on the page, including negative ones that fail
if any of the four corrected sentences comes back. New Go guard
TestUsageEventInsertWritesOnlyKnownColumns pins the column list the usage
insert writes, which is where the metering sentence is actually decided;
anchoring to UsageEventRow would not have caught this, since that type
already omits three columns of the real table.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three findings from the Antigravity adversarial stream on PR #1300. The defensive slice copy added in the previous commit reintroduced a bug it was not aiming at. append with nothing to append returns its first argument, so append([]Model(nil), emptySlice...) returns nil. fetchSnapshot replaces a nil Models or Catalog with an empty slice on the line above precisely so GET /v1/models serialises "models":[] rather than "models":null, and the unguarded copy silently undid that for an empty catalogue. Confirmed by test before the fix: the marshalled snapshot read {"models":null,"catalog":null}. Both copies are now guarded on length, and TestFetchSnapshotKeepsEmptyListsNonNil asserts the exact serialised bytes. GoogleVertexAI leaked through both vertex patterns at once. The trailing boundary in \bgoogle[ _-]?vertex\b fails because A is a word character, and \bvertex[ _-]?ai\b never starts because e is one. Neither vertex nor google vertex is an English word in the way together and azure are, so both drop the trailing boundary, which also recovers VertexAIStudio. The migration's pattern follows, and is re verified at thirteen for thirteen parity against the Go table on Postgres 17. The census matched capability_badges without printing it, so a row leaking only through a badge reported four clean looking fields and read as a regex false positive. It is now selected and printed. Also pinned: fireworks joins perplexity, bedrock and route- in the accepted false positives test, since it is an ordinary English word whose failure mode is a silently blanked description.
Adversarial review, round twoTwo independent streams ran against
FixedHIGH, and it was mine. The defensive slice copy added in Both copies are now guarded on length and the test asserts the exact serialised bytes. Worth stating plainly: this is a defect the round one fix introduced and a second independent stream caught, which is the argument for running the stream at all. MEDIUM, MEDIUM, LOW, the census matched The migration's pattern follows the vertex change and is re verified at thirteen for thirteen parity against the Go table on Postgres 17, applied from empty through all 109 migrations, RebuttedCodeRabbit, major: "do not publish provider-bearing alias IDs". Declined, and this is the one deliberate design decision in the change. Antigravity LOW 3, Antigravity LOW 6, |
## Summary
Hive's console had no privacy or data-policy surface at all (parity
re-score, criterion weight 9, credit 0.25). This is strategically odd
for Hive specifically because data sovereignty is the product's wedge
for Hive Enterprise, sold to regulated buyers in finance, legal,
healthcare and government on that exact claim.
Adds `/console/privacy`, disclosing the tenant's actual posture. The
hard rule for this task: never state a guarantee the system does not
enforce. Every claim on the page is backed by verified, enforced
behavior, not policy prose:
- **Retention**: `UsageEventRow` (`lib/control-plane/client.ts`) carries
no request/response content field at all, only token counts, cost,
status, model alias, and error codes. "No content stored" is a schema
property, not a promise layered on top of storage that could hold
content. Verified no retention TTL job exists either (states "no
automatic deletion schedule" rather than inventing one).
- **Third-party routing**: states plainly that every model in the
catalog is served by third-party infrastructure, and that content leaves
this deployment's boundary to reach it (with the
self-hosted-inference-only exception named), without naming which vendor
serves which model. Revised mid-review: the first version of this PR
named OpenRouter and Groq explicitly. CodeRabbit's committed-diff review
flagged that as violating the console-wide provider-blind convention
(`PublicCatalogModel`/`CatalogModel` both omit a provider field
everywhere else in the console;
`apps/edge-api/internal/errors/provider_blind_test.go` strips provider
identity from every error response). Re-reading the task brief's own
wording ("say so accurately rather than implying otherwise" about
routing to "third-party providers", generic, not a vendor name)
confirmed the generic phrasing satisfies the honesty requirement without
introducing a provider-identity leak this product has deliberately never
had anywhere else. Copy, the file-header design comment, and the tests
were all corrected accordingly (see commit `fix: drop OpenRouter/Groq
brand names from the privacy page`).
- **Provider allow/block**: `routing.SelectionInput.AllowedProviders`
exists on the wire type
(`apps/control-plane/internal/routing/types.go`), but verified no call
site in `apps/edge-api` ever sets it (checked every `SelectRouteInput{}`
construction site). No tenant control persists a choice into it today,
so the page says so plainly instead of rendering a toggle that would not
do anything. Model access itself IS enforced per API key
(`AllowedAliases`/`AllowAllModels`), and every alias resolves to exactly
one route, so that's what's linked to instead.
Also adds the "Privacy" nav entry to `ConsoleShell` (Shield icon,
Workspace group) and en/bn translation keys.
## Files touched
- `apps/web-console/app/console/privacy/page.tsx` (new)
- `apps/web-console/components/app-shell/console-shell.tsx` (nav entry
only)
- `apps/web-console/messages/{en,bn}.json` (translation keys only)
- `apps/web-console/__tests__/privacy-page.test.tsx` (new)
- `apps/web-console/tests/unit/console-shell.test.tsx` (regression guard
for the new nav link)
Did not touch: `app/console/page.tsx`, catalog pages,
`lib/viewer-gates.ts`, providers/feature-gates/marketplace pages,
`app/console/logs`, analytics — all out of scope per file ownership.
## Verify
- `docker compose run --no-deps --build web-console npm run test:unit` —
green on the current HEAD (69/70 files, 758/758 tests; the one failing
file, `tests/unit/ci-web-e2e-secret-free.test.ts`, fails on `ENOENT:
.github/workflows/ci.yml` because `Dockerfile.web-console` never COPYs
`.github/` into the image — pre-existing, unrelated to this diff).
- `docker compose run --no-deps --build web-console npm run build` —
clean, `/console/privacy` listed as a dynamic route alongside every
other console page.
- `node tools/lint-no-token-in-proof-captures.mjs` — passes.
## Visual proof
Substrate note: this sandbox's shared dev `.env` points at a Supabase
Cloud project (`yimgflllgdsbcibnaxqe`) deleted during the self-hosted
cutover, verified live this session (`psql` returns `FATAL: tenant/user
... not found`), so no real signed-in console session is obtainable here
on any branch right now. Full detail and exactly what was captured (the
real page's components/CSS/copy through the real `next dev` server, mock
data at the network boundary, throwaway harness route never committed)
is in `docs/proof/privacy-data-policy-2026-08-28/capture-log.md`,
including a revision note for the second screenshot posted after the
provider-name fix.
## Buglog entry
For the follow-up buglog-only PR (never appended directly to this
branch, per `.claude/rules/openwolf.md`):
```json
{"date":"2026-08-28","error_message":"n/a","root_cause":"console had no privacy/data-policy surface at all (parity re-score, criterion weight 9, credit 0.25)","fix":"added /console/privacy grounded in verified backend state (UsageEventRow schema, routing.SelectionInput.AllowedProviders dead code path, provider-blind error handling), never rendering a claim or control that is not enforced; corrected mid-review to drop literal OpenRouter/Groq vendor names per the console-wide provider-blind convention, keeping the third-party-routing disclosure itself","tags":["web-console","privacy","compliance","parity"]}
```
## Adversarial review
- **CodeRabbit CLI** (`coderabbit review --agent --committed --base
main`): ran (first attempt hit a rate limit, retried once and
completed). One MAJOR finding (provider names), addressed, see commit
`fix: drop OpenRouter/Groq brand names from the privacy page`. Posted as
a PR comment below.
- **`ecc:code-review`, plain adversarial pass, domain specialists**:
**SKIPPED — structural capability gap.** This builder's toolset has no
Skill-tool or Agent/Task-dispatch capability, so the parallel
independent-context subagent streams the `adversarial-pr-review` skill's
pipeline mode calls for could not be run. Performed a self-review pass
instead (same-context, not independent, disclosed as a real limitation)
covering: accessibility, empty/edge states, locale, interaction coverage
(added the missing link-href assertions), and re-verified every factual
claim against the code a second time. Orchestrator should dispatch
`ecc:code-review` and a fresh-context adversarial pass before merge.
- **Security stream**: this diff touches a compliance-claim surface but
no auth/money/input-parsing path; flagging for the orchestrator to
confirm whether `security-reviewer` is still warranted given the
compliance-claim risk named in the task brief.
## Test plan
- [x] Unit tests pass on touched files (full suite green apart from one
pre-existing, unrelated failure)
- [x] Full build passes, route listed
- [x] Visual proof captured and logged (revised after the provider-name
fix)
- [x] CodeRabbit CLI run, one finding addressed
- [ ] `ecc:code-review` / independent adversarial pass / domain
specialist review — needs orchestrator dispatch (capability gap, see
above)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
## Review round two (2026-08-28)
Six review threads found that the page published four content claims the
system does not enforce. All six are addressed in `62559f822` and
`15f5fda19`, and every thread has a reply on it.
**What was false, and what it says now.**
1. **Blanket "the gateway does not store the content of your requests or
responses".** False for three endpoints behind the same bearer key:
`/v1/batches` (the input file holds verbatim request bodies, the output
file holds verbatim upstream responses, both registered in
`public.files`), `/v1/files` (persistence is the endpoint's purpose),
and `/v1/rag/*` (`public.rag_chunks.content TEXT NOT NULL` holds the
full document text). The card now scopes the no-content statement to the
synchronous relay path and names all three surfaces with the deletion
route for each.
2. **"There is no field in that record a body could land in".** Rested
on `UsageEventRow`, which is a console-side read projection, not the
table. `public.usage_events` carries `internal_metadata jsonb`,
`customer_tags jsonb` and `provider_request_id`, and content is kept out
of `internal_metadata` by `usage.RedactMetadata`, a key-name denylist.
Replaced with a behavioural sentence: message content is stripped from
that metadata before the record is written.
3. **"Hive keeps that identity out of every customer-facing surface".**
Disproved by catalogue summaries that still name vendors (issue #1284,
PR #1300 open). Narrowed to error responses and this page, with an
explicit warning that model descriptions elsewhere can name a vendor.
4. **"Every alias resolves to exactly one upstream route".** A property
of today's seed data, not of `SelectRoute`, which returns
`FallbackRouteIDs` with no same-provider constraint. Softened to one
route per request, with the caveat that an alias can have several
eligible routes.
**Added on review request.** The data-collection posture
(`provider.data_collection: deny` with `allow_fallbacks: false` on the
free routes and on nothing else), which was the most decision-relevant
undisclosed fact for a regulated buyer, and a fourth card naming what
the page does not cover (physical location, personnel access, breach
notification, chat storage, English only). A failed catalog fetch now
renders as a load error rather than as an empty catalog.
**Bengali translation is explicitly out of scope for this PR**, and said
so on the page itself rather than only here. All 21 pages under
`app/console/` are hardcoded English today and none call
`useTranslations` or `getTranslations`; only the shell chrome is
translated, which is why the nav entry has a `bn` string. Translating
one page's body would not change what a Bengali-locale user experiences
anywhere else in the console, and machine translating a compliance
disclosure is the same class of risk this review round is about. Worth
its own issue, with this page as the first candidate.
**Tests.** Eight new assertions on the page, including negative ones
that fail if any of the four corrected sentences returns. New Go guard
`TestUsageEventInsertWritesOnlyKnownColumns`
(`apps/control-plane/internal/usage/insert_columns_test.go`) pins every
`INSERT INTO public.usage_events` column list against an allow list,
with a failure message naming this page. Anchoring to `UsageEventRow`
instead would have been green while the table drifted, since that type
already omits three of the table's columns. Suite: 766 passing (was
758), plus the one pre-existing unrelated `ci-web-e2e-secret-free`
failure caused by `.github/` not being copied into the web console
image.
**Second adversarial stream.** Antigravity (`agy`, gemini-3.1-pro-high)
reviewed the corrected diff and raised five findings; two were real and
are fixed in `15f5fda19`:
- `logProviderBlindUpstreamError` writes the raw upstream error text to
this deployment's server logs, and the orchestrator reads up to 4 KiB of
the failing response body to produce it, so a provider that echoes part
of a request in its error text gets that fragment logged. The page now
names that exception instead of implying it does not exist.
- The Go guard used `FindSubmatch`, which inspects only the first
insert; switched to `FindAllSubmatch` so a second insert added later
cannot slip past.
It also confirmed `customer_tags` gets no redaction at all in
`RecordEvent`. Nothing writes that column today (no call site in
`apps/edge-api` sets `CustomerTags`), so the page needs no sentence
about it yet; the file header records the constraint so that whoever
wires up customer-supplied tags sees this card has to change with them.
Two smaller findings (a truthy length check in the catalog ternary,
missing spaces after two bold labels) are also fixed.
**Known gap, stated rather than hidden.** The two sentences about
logging are verified by reading `apps/edge-api` but have no test behind
them; nothing would fail if a request-body log were added tomorrow.
Worth a follow-up issue rather than a token assertion.
## Buglog entry (revised)
```json
{"date":"2026-08-28","error_message":"n/a","root_cause":"the console privacy page published four content-storage claims the system does not enforce: a blanket no-content-stored statement that was false for /v1/batches, /v1/files and /v1/rag, a structural claim resting on UsageEventRow (a console-side projection that omits internal_metadata, customer_tags and provider_request_id from the real usage_events table), a product-wide provider-blindness claim disproved by catalogue summaries that name vendors (#1284), and a 1:1 alias-to-route claim that is a property of seed data rather than of SelectRoute, which returns FallbackRouteIDs with no same-provider constraint","fix":"rescoped every claim to what the code enforces, named the three surfaces that do store content with their deletion routes, disclosed the litellm data_collection posture and the upstream-error-text logging exception, added a what-this-page-does-not-cover card, split catalog fetch failure from an empty catalog, and anchored the metering claim with a Go guard over every INSERT INTO public.usage_events column list plus negative page assertions that fail if any corrected sentence returns","tags":["web-console","privacy","compliance","parity","honesty"]}
```
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
## Summary This is the batched buglog follow-up for the 21 pull requests merged during the 2026-08-28 session. Its diff is `.wolf/buglog.jsonl` and nothing else. Per `.claude/rules/openwolf.md`, every fixed bug, error, failed test or failed build must be logged, but the line may never be appended on a fix branch: `merge=union` in `.gitattributes` resolves concurrent appends locally and is ignored by GitHub's server side merge, so two branches that both appended land in hard conflict there. An unmergeable pull request gets no `refs/pull/N/merge`, no `pull_request` run and therefore zero checks, and the required status gate then blocks the merge for a reason the page never states (issue #873). Each fix accordingly carried its entry in its own pull request body, and this pull request copies them onto `main` in one batch, which the protocol explicitly prefers over one pull request per entry. ## Source pull requests 1240, 1251, 1253, 1257, 1268, 1276, 1277, 1278, 1281, 1287, 1292, 1293, 1294, 1296, 1300, 1301, 1303, 1305, 1313, 1335, 1337. All merged. Every entry came from a "Buglog entry" heading in one of those bodies. Nothing was invented for a pull request that carried none. ## What landed 36 entries appended, one JSON object per line, append only. The 196 pre-existing lines are byte identical to `origin/main`. | Source | Entries | |---|---| | #1240 | 1 | | #1251 | 1 | | #1253 | 1 | | #1257 | 4 | | #1268 | 5 | | #1276 | 2 | | #1277 | 1 (of 2 in the body) | | #1278 | 0 (merged into #1296) | | #1281 | 2 | | #1287 | 2 | | #1292 | 3 | | #1293 | 1 | | #1294 | 3 | | #1296 | 1 | | #1300 | 1 | | #1301 | 1 | | #1303 | 1 | | #1305 | 3 | | #1313 | 1 | | #1335 | 1 | | #1337 | 1 | Note on #1268: its first "Buglog entry" heading says "None yet" in prose and carries no JSON. Its two later headings, from the CI live lane and from the intermittent tool call failure, carry the five entries taken here. ## Deduplication - **#1278 dropped, folded into #1296.** Both describe the same defect: `omitempty` on `StreamContentBlock.Text` dropped the required `"text":""` from every text `content_block_start`, crashing the real Anthropic SDK's stream accumulator (issue #1274). #1278 is the conformance suite that found it and shipped it marked xfail; #1296 is the fix, and its entry carries the fuller root cause and the actual remedy. One bug, one entry. #1296's entry gains a `discovered_by` field naming #1278 so the discovery is not lost. - **#1277's first entry dropped.** The same body carries a later "Buglog entry (revised)" heading written after the review round found the page's claims did not match what the code enforces. The revised entry is the one taken. - Checked and kept as distinct: #1313 and #1337 are two different hooks (`decision-citation-check.js` and `secrets-scanner.js`) blind to the same MultiEdit payload shape, fixed in two different pull requests, so two entries. #1240 and #1335 are two different `account_not_provisioned` defects, one an observability gap at the edge boundary and one a console mint that should have refused, so two entries. #1305's three entries are three separate rounds of defects in the same money path change, each with its own root cause. ## Corrections against what actually merged Each entry was checked against the merged tree at `origin/main`, not against its own claim. - **#1240.** The entry said the log line went in at `AuthSnapshot.TenantUUID`. On `main` the check is the exported `authz.ParseTenantID(TenantLookup)` that `TenantUUID` delegates to, which the images and audio routing adapters (two further silent call sites found in the same review) also call, and `key_id` is deliberately not logged because CodeQL's clear text logging check flags any field named `*Key*` (alert #31). The `fix` field now says so. - **#1276, first entry.** The entry named `app/console/analytics/page.tsx` as the home of the five fetch helpers and their `Promise.all`. On `main` they live in `apps/web-console/lib/analytics/overview-fetch.ts`, extracted during review. Path corrected. - **#1277.** Its `error_message` was the placeholder `n/a`. Reconstructed from the pull request's own correction narrative: the page as first written published a blanket no content stored claim false for `/v1/batches`, `/v1/files` and `/v1/rag`, a product wide provider blindness claim disproved by catalogue summaries that name vendors (#1284), a metering claim anchored to the console side `UsageEventRow` projection rather than the `usage_events` table, and a 1:1 alias to route claim that is a property of seed data rather than of `SelectRoute`. **#1303 needed no correction.** Its original root cause asserted a live mid stream provider leak on the session chat relay that measurement disproved, and the author had already corrected the body before merge. The corrected version is what was taken, including the sentence recording that the session chat relay did not leak an error frame but silently truncated instead. Every other entry's central claim was verified present in the merged tree, among them `metering.SupportsIncludeUsage`, `sanitize.VariablePriceFrame` in the batch dispatcher, the revoke and regrant in `20260828_01_service_role_public_schema_grant.sql` with the `anon` assertions in `ci-throwaway-db.sh`, `normalizeReasoningUsage` now called from `normalizeChatCompletion`, `signup.SyncTenantMembershipRole`, `TestKeyViewHidesALimitThatIsNotEnforced`, `TestListEventsLatencyCrossesTheWire`, `mask-api-keys.mjs` and `md-table.mjs`, `StreamContentBlock.Text` as `*string`, `redactSnapshot`, `httpx.ReadBody`, `sanitize.ReplaceErrorFrame` with the default deny tail in `provider_blind.go`, `pinCompletionCeiling` and `captureInputTokens` with `applyReasoningHeadroom` gone, `requireBillingTenant`, and `hooks.selfcheck.js` wired into the Repo policy lints check. ## Verification - `node .wolf/hooks/bugstore.selfcheck.js` reports `bugstore selfcheck OK`. - All 232 lines parse as a single JSON object each. - Every appended entry carries `error_message`, `root_cause`, `fix` and `tags`. - Scanned for credentials: no API key, bearer token, JWT, password, AWS key or Postgres DSN with a password appears in any entry. The `hk_` occurrences are prefix descriptions in prose, not keys. - `git diff origin/main...HEAD --name-only` prints `.wolf/buglog.jsonl` and nothing else. No `.wolf/` telemetry was staged. ## Review No adversarial review streams were run, deliberately. This change is records only: it adds no code, no test, no configuration and no behavior, and `.wolf/buglog.jsonl` is on the inert path allowlist in `.github/workflows/ci.yml`, so the six required checks report green without running their heavy steps. If a check does fail here, that is a real signal about the file rather than about the pipeline. https://claude.ai/code/session_01WyEwUxZCArdn1ZUDkTvuQ1 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
## Summary This is the batched buglog follow-up for the pull requests merged to `main` on 2026-08-29. Its diff is `.wolf/buglog.jsonl` and nothing else. Per `.claude/rules/openwolf.md`, every fixed bug, error, failed test or failed build must be logged, but the line may never be appended on a fix branch. `merge=union` in `.gitattributes` resolves concurrent appends locally and is ignored by GitHub's server side merge, so two branches that both appended land in hard conflict there. An unmergeable pull request gets no `refs/pull/N/merge`, no `pull_request` run and therefore zero checks, and the required status gate then blocks the merge for a reason the page never states (issue #873). Each fix accordingly carried its entry in its own pull request body, and this pull request copies them onto `main` in one batch, which the protocol explicitly prefers over one pull request per entry. ## Scope examined Fifty nine pull requests merged to `main` on 2026-08-29. Forty eight of them carried at least one entry, for eighty two entries in total. Thirty two of those were already on `main` and are skipped, leaving fifty appended here from thirty four pull requests. The largest block of skips comes from #1342, the equivalent batch for the 2026-08-28 merges, which merged earlier the same day and already landed thirty six entries covering #1257, #1268, #1276, #1277, #1287, #1292, #1293, #1294, #1296, #1301, #1303, #1305, #1313, #1335 and #1337. ## What landed Fifty entries appended, one JSON object per line, append only. The 232 pre-existing lines are byte identical to `origin/main` (verified by hashing the first 232 lines of the result against the base file). Every line in the resulting file parses as JSON and carries `error_message`, `root_cause`, `fix` and `tags`. | Source | Entries | |---|---| | #1083 | 2 | | #1277 | 1 | | #1278 | 1 | | #1298 | 1 | | #1334 | 1 | | #1336 | 3 | | #1343 | 1 | | #1346 | 1 | | #1351 | 1 | | #1365 | 2 | | #1368 | 1 | | #1369 | 1 | | #1371 | 3 | | #1375 | 3 | | #1376 | 1 | | #1378 | 1 | | #1379 | 2 | | #1388 | 5 | | #1389 | 3 | | #1390 | 2 | | #1393 | 1 | | #1394 | 1 | | #1410 | 1 | | #1417 | 1 | | #1421 | 1 | | #1423 | 1 | | #1424 | 1 | | #1426 | 1 | | #1429 | 1 | | #1431 | 1 | | #1433 | 1 | | #1434 | 1 | | #1436 | 1 | | #1439 | 1 | Entries are copied verbatim from their source pull request bodies. Nothing was rewritten, no field was invented, and no field was added. No JSON needed repair: all eighty two extracted entries parsed on the first attempt and all four required fields were present on every one. ## Merged pull requests that carried no entry Eleven of the fifty nine. Recorded here because the gap is itself the useful signal. | Pull request | Title | Assessment | |---|---|---| | #1013 | chore(deps): bump the go-minor-patch group across 1 directory with 4 updates | Dependabot bump, no defect fixed, no entry expected | | #1015 | chore(deps): bump the go-minor-patch group across 1 directory with 6 updates | Dependabot bump, no entry expected | | #1016 | chore(deps): bump golang from 1.26-alpine to 1.27-alpine in /deploy/docker | Dependabot bump, no entry expected | | #1218 | chore(deps): bump postcss from 8.5.19 to 8.5.26 in /apps/desktop | Dependabot bump, no entry expected | | #1219 | chore(deps): bump golang.org/x/crypto from 0.41.0 to 0.52.0 in /apps/control-plane | Dependabot bump, no entry expected | | #1342 | chore: batch buglog entries for the 2026-08-28 merges | The previous batch pull request itself, correctly carries no entry of its own | | #1364 | chore: remove four dead skills and record the patterns that cost time | Protocol gap. The body records patterns that cost time, which is the shape of a buglog entry, but none was written as one | | #1383 | test: retire stale expected-failure markers, restore the ones that are true (#1381, #1382, #1324) | Protocol gap. Stale `it.fails` markers reading as red is a real defect that was fixed here and should have carried an entry | | #1384 | docs: correct D-047, hive-auto reverted to variable pricing (D-059) | Decision ledger correction, arguably a documentation defect, no entry written | | #1387 | chore(deps): bump next from 15.5.23 to 16.3.3 in /apps/agent-console | Dependabot bump, no entry expected | | #1398 | docs: rescue the 2026-08-25 parity captures and add the 2026-08-29 QA matrix evidence | Documentation and evidence rescue, no entry written | Six of the eleven are Dependabot bumps and one is the previous batch, so the genuine protocol gaps are #1364, #1383, #1384 and #1398. Of those, #1383 is the one worth a follow-up: it fixed a real defect class (a stale expected-failure marker reads as a red "Expect test to fail" and gets dismissed as pre-existing) and left no record. ## Entries skipped as already present Thirty two. Thirty of them matched an entry already on `main` on `error_message`, `id` or `fix`. Two more from #1278 are semantic duplicates that an exact match would have missed, and were skipped after reading the landed entries they duplicate: - #1278's `streaming content_block_start omits text field` entry is covered by the consolidated `bug-2026-08-28-anthropic-sdk-wire-conformance` entry landed from #1296, whose root cause names the same `omitempty` on `StreamContentBlock.Text`. - #1278's `GET /v1/models leaked an upstream provider name` entry is covered by `BUG-1284`, landed from #1300, which names the same `public.model_aliases.summary` publication path. #1278's third entry, on `top_k` forwarding producing a 400, is not covered anywhere on `main` and is appended here. #1342 recorded #1278 as fully "merged into #1296", which was accurate for two of its three entries. ## Note on entry quality One appended entry is thin: #1277's parity re-score record carries `error_message` of `n/a` and a root cause of "console had no privacy/data-policy surface at all". It is a parity gap record rather than a defect record. It is included exactly as written rather than embellished, per the protocol's preference for the author's own words. ## Test plan - [x] Branch cut fresh from `origin/main`, diff is `.wolf/buglog.jsonl` and nothing else - [x] First 232 lines byte identical to the base file (md5 match) - [x] All 282 resulting lines parse as JSON and carry `error_message`, `root_cause`, `fix` and `tags` - [x] No `.wolf/` telemetry (`anatomy.md`, `memory.md`, `token-ledger.json`, `hooks/_session.json`, `buglog.json`) in the commit - [ ] The six required checks report green via the inert path allowlist in `.github/workflows/ci.yml` --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Closes #1284.
What leaked, and where it came from
GET /v1/modelsserved this in production, confirmed live againsthttps://api-hive.scubed.co/v1through the OpenAI SDK:Origin:
public.model_aliases.summary, seeded bysupabase/migrations/20260717_02_voice_groq_stt_tts.sqllines 33 and 44. Not a hardcoded Go string. The column is published verbatim asdescriptiononGET /v1/modelsand assummaryon the two catalogue endpoints, and nothing between the row and the customer looked at it, because every provider-blindness guard in this repo sat on an error path (apps/edge-api/internal/errors/provider_blind.go, the batch executor'sSanitizeMessage, and thesystem_fingerprintand response-id stripping from PR #1222).The
hive-ttstext was also stale on its own terms. Groq deprecatedplayai-ttson 2025-12-31 and the route seeded by that same migration has pointed atcanopylabs/orpheus-v1-englishever since, so the description named a model that has never served a request here.Census of customer-reachable text that can carry a provider name
Run two ways: a code walk of every customer-facing response builder, and a scan of every
text,varchar,jsonandjsonbcolumn of every base table in thepublicschema on a throwaway Postgres with all 109 migrations applied.Catalogue metadata, the family this issue is in
Three endpoints, two wire shapes, one row source. Five columns of
public.model_aliasesreach a customer as six field names:summarydescriptionon/v1/models,summaryon/catalog/modelsand/api/v1/catalog/modelsdisplay_namenameon/v1/models,display_nameon both catalogue endpointsowned_byowned_byon/v1/modelshivecapability_badgescapability_badgeson both catalogue endpointsalias_ididon all threealias_idis deliberately not redacted and the row is not dropped: the id is the customer's invocation handle and published contract, so blanking it serves an unusable listing and hiding the row removes a working model from every picker silently. A leaky id is logged loudly instead and needs a migration to rename.GET /catalog/modelsis unauthenticated andGET /api/v1/catalog/modelsis optional-auth, so this was not only an authenticated surface.Live row scan
model_aliases.summarymodel_aliases.alias_idopenrouter-auto)visibility = 'internal', excluded by the existing internal filter in both buildersmodel_aliases.display_nameOpenrouter Auto (Task Aware), same row)provider_routes.route_id,.provider,.provider_model,.litellm_model_nameCatalogSnapshot.Routesisjson:"-"and edge-api'sSnapshothas no such field at allprovider_capabilities.route_idalias_route_policies.fallback_orderjson:"-"custom_providers.slug,.display_name,.base_url,.litellm_prefixrag_embedding_config.modelOther customer-reachable text, checked and clear
modelis rewritten to the alias innormalizeChatCompletion,idis gateway-minted,system_fingerprintis nil'd, and streaming chunks are stripped the same way (PR fix: mint gateway-owned response ids, strip system_fingerprint, drop DeepSeek post-finish chunk #1222 family).apps/edge-api/internal/errors/provider_blind.go, eighteen provider words plus a route-slug pattern plus LiteLLM exception-class patterns.SanitizeMessagein the local executor.GET /v1/audio/voices: six static voice ids and names, no provider token./api/v1/accounts/current/usage-eventsand the analytics sibling): a field allowlist that excludesprovider_request_idandinternal_metadataentirely.name,description): admin-authored, zero rows seeded, reachable only through the shared-secret internal read that agent-engine consumes. A future carrier, not a live one.model-detail.tsxandcache-metrics.ts;providers-manager.tsxis the admin panel.Where the guard went, and why
At the boundary, and also in the rows.
The boundary is the answer to the recurrence, because a migration only fixes rows that exist today. Three endpoints render these rows through two builders (
buildCatalogSnapshotandbuildPublicCatalogModels), and a fix applied to one of them would have left the other, unauthenticated, one leaking. Both builders now callredactAlias, and edge-api applies the same check to the decoded snapshot infetchSnapshot, which is the single funnelGET /v1/modelsandGET /catalog/modelsshare. That satisfies the both-boundaries rule in CLAUDE.md and covers a control-plane deployed one commit behind or a snapshot replayed from a stale Redis entry.The vocabulary is deliberately narrower than the error-path list, and this is the one design decision worth reading. The eighteen-word list includes
anthropic,deepseek,google,mistralandopenai. That is right for an error message, whose text is disposable, and wrong for catalogue copy: model vendor names are product copy this catalogue publishes on purpose.deepseek-v4-proand "Deepseek V4 Pro" are shipped, customer-facing names from20260822_02_catalog_alias_restructure.sql, so reusing the error-path list would have renamed two live models and blanked their descriptions. The invariant is that a customer cannot tell who serves the request, not that no AI company may be named, so this list carries serving infrastructure only: gateways, aggregators, hosting clouds and internal route slugs.TestRedactAliasLeavesModelVendorNamingIntactandTestFetchSnapshotKeepsModelVendorNaminghold that line.The migration is still worth having: without it the boundary would have to drop both voice descriptions permanently, and the stale PlayAI text would survive in the database. It rewrites a row only while that row still names a provider, so it is idempotent and cannot overwrite copy someone has since curated, and it raises a NOTICE naming any other customer-facing row that still names one.
The two vocabularies are duplicated across the two Go modules rather than shared. Sharing means a new
packages/module wired intogo.work, twogo.modfiles and five Dockerfiles for fifteen tokens, and the repo already carries this duplication twice (the batch executor mirrors the edge-api list). Each file names the other; promote all three to one shared module when a fourth copy is wanted.Verification
Unit tests, both modules, run through the toolchain image:
Full
./apps/edge-api/...and./apps/control-plane/...suites pass with noFAILline.gofmt -landgo vetare clean on both touched packages.Mutation check. With the three call sites disconnected (the
redactAliasandredactSnapshotcalls replaced by a reference that does nothing, so the packages still compile and every other test still runs), seven tests fail on assertions, not on build errors:Restoring the three calls returns both packages to
ok. The tests are load-bearing.Migration. Applied on a throwaway Postgres (
pgvector/pgvector:pg17,scripts/ci-throwaway-db.sh, 109 of 109 migrations executed, torn down after):Rows after:
hive-sttreads "Serverless speech-to-text for /v1/audio/transcriptions." andhive-ttsreads "Serverless text-to-speech for /v1/audio/speech." Running the file a second time reportsUPDATE 0twice and raises no NOTICE, so it is idempotent and no other customer-facing row still names a provider.Not done, and stated rather than implied: no capture against a running stack. This is an API payload rather than a UI surface, and the two boundary funnels are covered by unit tests at both ends; the deployed result is worth a
models.list()spot check after merge.Review round two, commit 75afa77
Review found two defects in the guard itself. Both are fixed here, and the second one is the more embarrassing of the pair because the test that should have caught it was already in the file.
The redaction amplified the leak it fixes. The display name fallback wrote the alias id into the field it had just scrubbed, without asking whether the id was itself what tripped the check. On
openrouter-auto, seeded by20260822_30_openrouter_auto_variable_pricing.sqland whose own comment says flipping it to public is a one line follow up, one occurrence of the provider name on the wire became four:models[].idandcatalog[].idare published contract, and the fallback addedmodels[].nameandcatalog[].display_nameon top.displayFallbacknow degrades to the empty string when the alias id names a provider, in both modules and at all three call sites, and still returns the readable id when it does not.The reason this shipped inside the fix is that
TestSnapshotJSONCarriesNoUpstreamProviderIdentityused the clean fixture idhive-voice, so its whole payload assertion never met a leaky one. That test cannot simply take a leaky id, because the publishedalias_idmakesContainsProviderIdentitytrue over the whole payload no matter what the guard does. So the whole payload assertion is now a count:TestSnapshotJSONDoesNotAmplifyALeakyAliasIDandTestFetchSnapshotDoesNotAmplifyALeakyAliasIDbuild the realopenrouter-autorow and assert the token appears exactly twice in the marshalled snapshot, which keeps the original property that a field added to either wire shape later is covered without anyone extending the test.The pattern missed the incident's own provider. The trailing
\bin the token alternation lostGroqCloud, Groq's own product name, because q followed by C is word character to word character, and the same shape lostOpenRouterAI,FireworksAIandCerebrasCloud. The list now anchors on the leading boundary only, which costs nothing because none of the eleven tokens prefixes a common English word. The multiword patterns keep their trailing boundary deliberately, becausetogetherandazureare common English words and\btogether[ ._-]?aiwithout an anchor matches "we work together aiming at one answer";Together AIis caught by\btogether[ ._-]?ai\b.Google VertexandAzure MLare added.The migration held three vocabularies, none equal to the Go one. A summary edited to read "Serverless speech-to-text via NVIDIA NIM" failed the five token update predicate, so the row was neither repaired nor reported, while the boundary dropped the description on every request, arriving silently at the permanently blank description this migration exists to prevent. All three are now one plpgsql
provider_patternconstant kept identical to the Go pattern, verified eleven for eleven against the Go table on Postgres 17. The census gained a pass overalias_id, the one column no guard ever repairs, deliberately without the visibility filter, andvisibility <> 'internal'becameis distinct fromso a NULL is no longer dropped.Also folded in from the same review:
redactSnapshotno longer writes through the caller's backing array, and both boundaries log a given redaction once per process rather than once per request, which matters because/catalog/modelsis unauthenticated and uncached.Filed rather than fixed: #1315, the vocabulary is a compile time literal while
public.custom_providersis admin managed runtime data.Mutation check, round two
Four mutations, each reverting one fix, each failing exactly the tests that cover it, on assertions rather than build errors:
displayFallbackreturnsaliasIDunconditionally, both modulesTestSnapshotJSONDoesNotAmplifyALeakyAliasID(want 2 occurrences, got 4),TestFetchSnapshotDoesNotAmplifyALeakyAliasIDTestContainsProviderIdentity, all seven new spellings, in both packagesappendlines removed fromredactSnapshotTestRedactSnapshotLeavesTheCallersSlicesAlone(caller's model slice was mutated)firstSightingstubbed toreturn trueTestLogRedactionSpeaksOncePerProcess(want 1 line for a repeated redaction, got 5)Migration re verified from empty on a throwaway Postgres, 109 of 109 applied:
The third line is what the
alias_idcensus pass bought; the old census printed nothing at all on the same database. Second run reportsrepaired: 0twice and leaves both summaries unchanged.Review round three, commit cbef673
A second independent adversarial stream (Antigravity, gemini-3.1-pro-high) found that the defensive slice copy added in round two reintroduced a bug it was not aiming at.
appendwith nothing to append returns its first argument, soappend([]Model(nil), emptySlice...)returnsnil, andfetchSnapshotreplaces a nilModelsorCatalogwith[]T{}on the line immediately above precisely soGET /v1/modelsserialises"models":[]rather than"models":null. Both copies are now guarded on length, andTestFetchSnapshotKeepsEmptyListsNonNilasserts the exact serialised bytes; before the fix it read{"models":null,"catalog":null}.Same stream, same class as the round two
GroqCloudfinding but in the two patterns that fix had not touched:GoogleVertexAIescaped both vertex patterns at once, because\bgoogle[ _-]?vertex\bhas a word character after thexand\bvertex[ _-]?ai\bhas one before thev. Both drop their trailing boundary, which also recoversVertexAIStudio. The migration pattern follows and is re verified at thirteen for thirteen parity against the Go table on Postgres 17.Also from that stream: the census matched
capability_badgeswithout printing it, so a row leaking only through a badge reported four clean looking fields and read as a false positive. It is now selected and printed. Andfireworksjoins the pinned accepted false positives, since it is an ordinary English word whose failure mode is a silently blanked description.Two findings were rebutted rather than fixed, with reasoning in the review comment on this PR:
lifecycleischeck (lifecycle in ('stable', 'preview', 'hidden'))so it cannot carry a provider name, and CodeRabbit's objection to publishing a provider-bearingalias_idis the deliberate design decision this PR already argues, for a row that isvisibility = 'internal'and on no customer surface.Buglog entry
{"id":"BUG-1284","date":"2026-08-28","title":"GET /v1/models leaked upstream provider identity in hive-stt and hive-tts descriptions","error_message":"models.list() returned description \"Serverless speech-to-text (Groq Whisper) for /v1/audio/transcriptions.\" and \"Serverless text-to-speech (Groq PlayAI) for /v1/audio/speech.\" live against api-hive.scubed.co","root_cause":"public.model_aliases.summary was seeded with the provider name by 20260717_02_voice_groq_stt_tts.sql and is published verbatim as the description field of GET /v1/models and as summary on both catalogue endpoints. Every provider-blindness guard in the repo sat on an error path, so catalogue metadata was never examined. The hive-tts text was additionally stale: Groq deprecated playai-tts on 2025-12-31 and the route has pointed at canopylabs/orpheus-v1-english since that same migration.","fix":"Boundary guard in both modules: redactAlias in control-plane's buildCatalogSnapshot and buildPublicCatalogModels, redactSnapshot in edge-api's catalog client fetchSnapshot, scrubbing display name, summary, owned_by and capability badges. Vocabulary is serving-provider identity only, not model vendors, so shipped names such as Deepseek V4 Pro survive. Migration 20260828_01 repairs the two seeded rows behind a still-leaking predicate and raises a NOTICE for any other customer-facing row that names a provider.","tags":["provider-blindness","catalog","models-endpoint","security","issue-1284"]}🤖 Generated with Claude Code
Summary by CodeRabbit