Skip to content

fix: keep upstream provider identity out of catalogue metadata (#1284) - #1300

Merged
sakibsadmanshajib merged 3 commits into
mainfrom
fix/models-description-provider-leak
Aug 28, 2026
Merged

sakibsadmanshajib merged 3 commits into
mainfrom
fix/models-description-provider-leak

Conversation

@sakibsadmanshajib

@sakibsadmanshajib sakibsadmanshajib commented Aug 28, 2026 •

Copy link
Copy Markdown
Owner

Closes #1284.

What leaked, and where it came from

GET /v1/models served this in production, confirmed live against https://api-hive.scubed.co/v1 through the OpenAI SDK:

{"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."}

Origin: public.model_aliases.summary, seeded by supabase/migrations/20260717_02_voice_groq_stt_tts.sql lines 33 and 44. Not a hardcoded Go string. The column is published verbatim as description on GET /v1/models and as summary on 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's SanitizeMessage, and the system_fingerprint and response-id stripping from PR #1222).

The hive-tts text was also stale on its own terms. Groq deprecated playai-tts on 2025-12-31 and the route seeded by that same migration has pointed at canopylabs/orpheus-v1-english ever 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, json and jsonb column of every base table in the public schema 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_aliases reach a customer as six field names:

Column Reaches Guarded here
summary description on /v1/models, summary on /catalog/models and /api/v1/catalog/models yes, dropped when it names a provider
display_name name on /v1/models, display_name on both catalogue endpoints yes, falls back to the alias id
owned_by owned_by on /v1/models yes, falls back to hive
capability_badges capability_badges on both catalogue endpoints yes, the offending badge is dropped, the rest survive
alias_id id on all three no, see below

alias_id is 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/models is unauthenticated and GET /api/v1/catalog/models is optional-auth, so this was not only an authenticated surface.

Live row scan

Table and column Rows matching a provider token Customer-reachable
model_aliases.summary 2 before this change, 0 after yes, this is the leak
model_aliases.alias_id 1 (openrouter-auto) no, visibility = 'internal', excluded by the existing internal filter in both builders
model_aliases.display_name 1 (Openrouter Auto (Task Aware), same row) no, same reason, and now also scrubbed if that row is ever made visible
provider_routes.route_id, .provider, .provider_model, .litellm_model_name 27, 25, 25, 27 no, CatalogSnapshot.Routes is json:"-" and edge-api's Snapshot has no such field at all
provider_capabilities.route_id 27 no, routing-internal
alias_route_policies.fallback_order 12 no, json:"-"
custom_providers.slug, .display_name, .base_url, .litellm_prefix 3 each no, admin panel only, where naming providers is the point
rag_embedding_config.model 1 no, admin surface

Other customer-reachable text, checked and clear

  • Inference responses: model is rewritten to the alias in normalizeChatCompletion, id is gateway-minted, system_fingerprint is 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).
  • Error bodies on inference, audio, images, RAG, chat dispatch and the Anthropic-compatible surface: apps/edge-api/internal/errors/provider_blind.go, eighteen provider words plus a route-slug pattern plus LiteLLM exception-class patterns.
  • Batch output files: SanitizeMessage in the local executor.
  • GET /v1/audio/voices: six static voice ids and names, no provider token.
  • Customer usage endpoints (/api/v1/accounts/current/usage-events and the analytics sibling): a field allowlist that excludes provider_request_id and internal_metadata entirely.
  • Marketplace entries (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.
  • The served OpenAPI spec and support matrix: zero matches.
  • Web console customer pages: zero rendered matches. The three source matches are code comments in model-detail.tsx and cache-metrics.ts; providers-manager.tsx is the admin panel.
  • Open WebUI static hooks and patches: two matches, both Python comments in a server-side patch file.

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 (buildCatalogSnapshot and buildPublicCatalogModels), and a fix applied to one of them would have left the other, unauthenticated, one leaking. Both builders now call redactAlias, and edge-api applies the same check to the decoded snapshot in fetchSnapshot, which is the single funnel GET /v1/models and GET /catalog/models share. 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, mistral and openai. 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-pro and "Deepseek V4 Pro" are shipped, customer-facing names from 20260822_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. TestRedactAliasLeavesModelVendorNamingIntact and TestFetchSnapshotKeepsModelVendorNaming hold 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 into go.work, two go.mod files 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:

ok  github.com/sakibsadmanshajib/hive/apps/edge-api/internal/catalog
ok  github.com/sakibsadmanshajib/hive/apps/control-plane/internal/catalog

Full ./apps/edge-api/... and ./apps/control-plane/... suites pass with no FAIL line. gofmt -l and go vet are clean on both touched packages.

Mutation check. With the three call sites disconnected (the redactAlias and redactSnapshot calls 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:

--- FAIL: TestSnapshotRedactsUpstreamProviderFromModelDescriptions
    model "hive-stt" description names an upstream provider: "Serverless speech-to-text (Groq Whisper) for /v1/audio/transcriptions."
--- FAIL: TestTenantSnapshotRedactsUpstreamProviderFromModelDescriptions
--- FAIL: TestPublicCatalogModelsRedactsUpstreamProviderFromSummaries
--- FAIL: TestSnapshotJSONCarriesNoUpstreamProviderIdentity
--- FAIL: TestFetchSnapshotRedactsUpstreamProviderIdentity
--- FAIL: TestFetchSnapshotForTenantRedactsUpstreamProviderIdentity
FAIL github.com/sakibsadmanshajib/hive/apps/edge-api/internal/catalog
FAIL github.com/sakibsadmanshajib/hive/apps/control-plane/internal/catalog

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):

::group::applying 20260828_01_voice_alias_summaries_provider_blind.sql
BEGIN
UPDATE 1
UPDATE 1
DO
COMMIT

Rows after: hive-stt reads "Serverless speech-to-text for /v1/audio/transcriptions." and hive-tts reads "Serverless text-to-speech for /v1/audio/speech." Running the file a second time reports UPDATE 0 twice 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 by 20260822_30_openrouter_auto_variable_pricing.sql and 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[].id and catalog[].id are published contract, and the fallback added models[].name and catalog[].display_name on top. displayFallback now 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 TestSnapshotJSONCarriesNoUpstreamProviderIdentity used the clean fixture id hive-voice, so its whole payload assertion never met a leaky one. That test cannot simply take a leaky id, because the published alias_id makes ContainsProviderIdentity true over the whole payload no matter what the guard does. So the whole payload assertion is now a count: TestSnapshotJSONDoesNotAmplifyALeakyAliasID and TestFetchSnapshotDoesNotAmplifyALeakyAliasID build the real openrouter-auto row 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 \b in the token alternation lost GroqCloud, Groq's own product name, because q followed by C is word character to word character, and the same shape lost OpenRouterAI, FireworksAI and CerebrasCloud. 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, because together and azure are common English words and \btogether[ ._-]?ai without an anchor matches "we work together aiming at one answer"; Together AI is caught by \btogether[ ._-]?ai\b. Google Vertex and Azure ML are 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_pattern constant kept identical to the Go pattern, verified eleven for eleven against the Go table on Postgres 17. The census gained a pass over alias_id, the one column no guard ever repairs, deliberately without the visibility filter, and visibility <> 'internal' became is distinct from so a NULL is no longer 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.

Filed rather than fixed: #1315, the vocabulary is a compile time literal while public.custom_providers is 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:

Mutation Fails
displayFallback returns aliasID unconditionally, both modules TestSnapshotJSONDoesNotAmplifyALeakyAliasID (want 2 occurrences, got 4), TestFetchSnapshotDoesNotAmplifyALeakyAliasID
Old regex restored, both modules TestContainsProviderIdentity, all seven new spellings, in both packages
The two append lines removed from redactSnapshot TestRedactSnapshotLeavesTheCallersSlicesAlone (caller's model slice was mutated)
firstSighting stubbed to return true TestLogRedactionSpeaksOncePerProcess (want 1 line for a repeated redaction, got 5)

Migration re verified from empty on a throwaway Postgres, 109 of 109 applied:

NOTICE:  hive-stt summary rows repaired: 1
NOTICE:  hive-tts summary rows repaired: 1
NOTICE:  model_aliases alias_id itself names a serving provider and is published verbatim as the model id: alias_id=openrouter-auto visibility=internal.

The third line is what the alias_id census pass bought; the old census printed nothing at all on the same database. Second run reports repaired: 0 twice 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. append with nothing to append returns its first argument, so append([]Model(nil), emptySlice...) returns nil, and fetchSnapshot replaces a nil Models or Catalog with []T{} on the line immediately above precisely so GET /v1/models serialises "models":[] rather than "models":null. Both copies are now guarded on length, and TestFetchSnapshotKeepsEmptyListsNonNil asserts the exact serialised bytes; before the fix it read {"models":null,"catalog":null}.

Same stream, same class as the round two GroqCloud finding but in the two patterns that fix had not touched: GoogleVertexAI escaped both vertex patterns at once, because \bgoogle[ _-]?vertex\b has a word character after the x and \bvertex[ _-]?ai\b has one before the v. Both drop their trailing boundary, which also recovers VertexAIStudio. 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_badges without 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. And fireworks joins 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: lifecycle is check (lifecycle in ('stable', 'preview', 'hidden')) so it cannot carry a provider name, and CodeRabbit's objection to publishing a provider-bearing alias_id is the deliberate design decision this PR already argues, for a row that is visibility = '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

  • Bug Fixes
    • Redacted upstream provider names and metadata from customer-facing model catalogs and snapshots.
    • Applied consistent redaction across public model listings, tenant catalogs, and edge API responses.
    • Removed provider-specific capability badges while preserving safe model identifiers.
    • Repaired existing provider-leaking voice alias summaries in the catalog.

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.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c748843-bb0c-4a55-b65f-7aebc062e3a8

📥 Commits

Reviewing files that changed from the base of the PR and between b442af4 and 75afa77.

📒 Files selected for processing (8)
  • apps/control-plane/internal/catalog/http.go
  • apps/control-plane/internal/catalog/providerblind.go
  • apps/control-plane/internal/catalog/providerblind_test.go
  • apps/control-plane/internal/catalog/service.go
  • apps/edge-api/internal/catalog/client.go
  • apps/edge-api/internal/catalog/providerblind.go
  • apps/edge-api/internal/catalog/providerblind_test.go
  • supabase/migrations/20260828_01_voice_alias_summaries_provider_blind.sql

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The 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.

Changes

Provider-blind catalog enforcement

Layer / File(s) Summary
Control-plane catalog redaction
apps/control-plane/internal/catalog/providerblind.go, apps/control-plane/internal/catalog/service.go, apps/control-plane/internal/catalog/http.go, apps/control-plane/internal/catalog/providerblind_test.go
Control-plane catalog snapshots and public catalog models redact provider identities from aliases. Tests cover detection, fallback values, serialized output, logging, and clean vendor naming.
Edge snapshot redaction
apps/edge-api/internal/catalog/providerblind.go, apps/edge-api/internal/catalog/client.go, apps/edge-api/internal/catalog/providerblind_test.go
The shared snapshot fetch path redacts model and catalog fields before both catalog endpoints return data. Tests verify tenant flows, alias IDs, slice isolation, and capability badge filtering.
Voice alias data repair and audit
supabase/migrations/20260828_01_voice_alias_summaries_provider_blind.sql
The migration repairs provider-bearing hive-stt and hive-tts summaries, scans customer-visible metadata, and reports provider-named alias IDs across visibility states.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🔵 Low · up to 75afa

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
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning 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: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy issue #1284. They redact provider identity at control-plane and edge-api catalogue boundaries, cover the relevant model and catalogue endpoints, update the hive-stt and stale hive-…
Out of Scope Changes check ✅ Passed The changes remain within the linked issue scope. The added redaction logic, boundary enforcement, migration, logging, and tests directly support provider-blind catalogue metadata.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preventing upstream provider identity from leaking through catalogue metadata.
Full details: Linked Issues check

Explanation

The changes satisfy issue #1284. They redact provider identity at control-plane and edge-api catalogue boundaries, cover the relevant model and catalogue endpoints, update the hive-stt and stale hive-tts summaries through a migration, and add regression tests.

Full details: Docstring Coverage

Explanation

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.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/models-description-provider-leak

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sakibsadmanshajib

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 28, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@sakibsadmanshajib sakibsadmanshajib left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_id reaching the wire (same root cause I found independently) and redactSnapshot mutating 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-review and the adversarial-pr-review skill 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.

Comment thread apps/control-plane/internal/catalog/providerblind.go Outdated
Comment thread apps/control-plane/internal/catalog/providerblind.go Outdated
Comment thread apps/control-plane/internal/catalog/providerblind.go
Comment thread apps/control-plane/internal/catalog/providerblind.go
Comment thread apps/edge-api/internal/catalog/providerblind.go Outdated
Comment thread supabase/migrations/20260828_01_voice_alias_summaries_provider_blind.sql Outdated
Comment thread supabase/migrations/20260828_01_voice_alias_summaries_provider_blind.sql Outdated
…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.
sakibsadmanshajib added a commit that referenced this pull request Aug 28, 2026
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.
@sakibsadmanshajib

Copy link
Copy Markdown
Owner Author

Adversarial review, round two

Two independent streams ran against 75afa77f5. Both found something. Everything actionable is fixed in cbef67362.

Stream State Outcome
CodeRabbit CLI RAN 1 major, rebutted below
Antigravity (agy, gemini-3.1-pro-high, effort high) RAN 1 HIGH, 2 MEDIUM, 3 LOW. HIGH and both MEDIUMs fixed, one LOW fixed, two LOWs rebutted
Codex SKIPPED Quota exhausted until 2026-09-10. Replaced by the Antigravity stream above rather than left absent

Fixed

HIGH, and it was mine. The defensive slice copy added in 75afa77f5 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 []T{} on the line immediately above, precisely so GET /v1/models serialises "models":[] rather than "models":null and a strict OpenAI client does not choke, and the unguarded copy silently undid that for an empty catalogue. Reproduced before fixing:

--- FAIL: TestFetchSnapshotKeepsEmptyListsNonNil
    empty lists must serialise as [] and not null, got {"models":null,"catalog":null}

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, 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. This is the same class as the GroqCloud finding from round one, in the two patterns that had not been touched by it. Neither vertex nor google vertex is an English word in the way together and azure are, so both drop their trailing boundary, which also recovers VertexAIStudio. Both are now in TestContainsProviderIdentity in both modules.

MEDIUM, fireworks is an ordinary English word and was not pinned. Taken as a pin rather than a removal, consistent with how perplexity, bedrock and route- were handled: "The model produces fireworks on creative prompts." joins TestContainsProviderIdentityAcceptedFalsePositives. Note this is not a regression from round one, since the token matched \bfireworks\b before the boundary change too; it was simply an accepted false positive nobody had written down.

LOW, the census matched capability_badges without printing it. A row leaking only through a badge reported four clean looking fields and read as a regex false positive, which is exactly how a real leak gets dismissed. Now selected and printed.

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, repaired: 0 on the second run.

Rebutted

CodeRabbit, major: "do not publish provider-bearing alias IDs". Declined, and this is the one deliberate design decision in the change. alias_id is the customer's invocation handle and published contract: blanking it serves a listing nobody can call, and dropping the row removes a working model from every picker silently. The row CodeRabbit names, openrouter-auto, is visibility = 'internal' and is excluded by the internal filter in both builders, so it is not on any customer surface today. What this PR adds for it is a loud log line at both boundaries and, as of 75afa77f5, a migration notice naming it explicitly and deliberately without the visibility filter, so the rename is scheduled rather than discovered. Renaming a published model id needs its own migration and a deprecation window, which is a separate change, not a line in this one.

Antigravity LOW 3, lifecycle is unguarded. Not reachable. lifecycle is text not null check (lifecycle in ('stable', 'preview', 'hidden')) in 20260331_01_model_catalog.sql, so "stable (groq-hosted)" cannot be stored. A guard there would be dead code defending against a value the database rejects.

Antigravity LOW 6, TestSnapshotRedactsUpstreamProviderFromModelDescriptions asserts too little. True of that test read alone and false of the package. Its fixture is deliberately the two live rows from 20260717_02_voice_groq_stt_tts.sql verbatim, so it fails on the exact payload the SDK observed rather than on a paraphrase, and those rows leak only through summary. The fields it does not assert are covered by TestRedactAliasFallsBackWithoutBlankingTheRow field by field and by TestSnapshotJSONCarriesNoUpstreamProviderIdentity, whose fixture leaks through display name, owned_by, summary and a capability badge at once and asserts over the whole marshalled payload. Widening the verbatim-fixture test would cost it the property that makes it worth having.

@sakibsadmanshajib
sakibsadmanshajib merged commit 54bd2d4 into main Aug 28, 2026
30 checks passed
@sakibsadmanshajib
sakibsadmanshajib deleted the fix/models-description-provider-leak branch August 28, 2026 23:32
sakibsadmanshajib added a commit that referenced this pull request Aug 29, 2026
## 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>
sakibsadmanshajib added a commit that referenced this pull request Aug 29, 2026
## 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>
sakibsadmanshajib added a commit that referenced this pull request Aug 29, 2026
## 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant