Repository navigation
feat(compliance): provider headquarters filter - #3018
Conversation
Add an optional allowedCountries list to the provider compliance policy so enterprise orgs can restrict routing to providers headquartered in approved countries. The gateway enforces it through the existing isProviderCompliant chokepoint (fail-closed for unknown headquarters). The compliance page gains a flag-chip country selector limited to the countries defined in the models catalogue, and public per-country provider pages are generated under /providers/country/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KUzb6QZFn2ngP6eohSDPdU
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds provider-headquarters country restrictions to compliance policies, administration controls, provider compliance checks, and public country-specific provider directory pages with metadata and sitemap entries. ChangesProvider headquarters country support
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant OrganizationAdmin
participant ComplianceClient
participant OrganizationRoute
participant Gateway
participant isProviderCompliant
OrganizationAdmin->>ComplianceClient: Select headquarters countries
ComplianceClient->>OrganizationRoute: Save allowedCountries
OrganizationRoute->>Gateway: Apply saved policy to request
Gateway->>isProviderCompliant: Evaluate provider policy
isProviderCompliant-->>Gateway: Compliance result
Gateway-->>OrganizationAdmin: Allow request or return 403
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 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 |
Add compliance-requirement and headquarters-country filters to the public providers directory, reusing isProviderCompliant. Stop hardcoding country lists in the compliance docs and changelog and link to the providers directory instead, and document the no-hardcoded-lists rule in AGENTS.md (with an image/video-generation exception). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KUzb6QZFn2ngP6eohSDPdU
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/models/src/providers.ts (1)
1565-1615: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a test asserting all country names are human-readable.
PROVIDER_COUNTRY_NAMESis manually maintained. If a provider is added with a new country code but the name map isn't updated,getProviderCountriessilently falls back to the raw code (e.g., "DE" instead of "Germany"). A one-line assertion in the existinggetProviderCountriestest would catch this regression automatically.♻️ Suggested test addition in compliance.spec.ts
it("returns only distinct countries referenced by the catalogue, sorted by name", () => { const countries = getProviderCountries(); const codes = countries.map((c) => c.code); expect(new Set(codes).size).toBe(codes.length); expect(codes).toContain("US"); expect(codes).toContain("CN"); expect(codes).not.toContain(null); + // Every country should have a human-readable name, not a raw code fallback. + for (const country of countries) { + expect(country.name).not.toBe(country.code); + } const names = countries.map((c) => c.name); expect(names).toEqual([...names].sort((a, b) => a.localeCompare(b))); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/models/src/providers.ts` around lines 1565 - 1615, Add a test assertion for getProviderCountries that verifies every returned country name is human-readable rather than a raw two-letter country code, catching missing entries in PROVIDER_COUNTRY_NAMES. Place it in the existing getProviderCountries test and preserve the current country and sorting assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/ui/src/app/providers/country/`[country]/page.tsx:
- Around line 52-54: Align the country page’s model count with the stats bar in
ProvidersGrid by computing the summed per-provider model counts used there, or
by passing that value as a totalModels override to ProvidersGrid. Update the
modelCountForProviders usage and the ProvidersGrid call in the country page so
both the subheading and stats bar display the same count.
---
Nitpick comments:
In `@packages/models/src/providers.ts`:
- Around line 1565-1615: Add a test assertion for getProviderCountries that
verifies every returned country name is human-readable rather than a raw
two-letter country code, catching missing entries in PROVIDER_COUNTRY_NAMES.
Place it in the existing getProviderCountries test and preserve the current
country and sorting assertions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: cec5d6cb-1890-4175-adb1-bdc8effa5b66
📒 Files selected for processing (9)
apps/api/src/routes/organization.tsapps/docs/content/features/compliance.mdxapps/ui/src/app/dashboard/[orgId]/org/compliance/compliance-client.tsxapps/ui/src/app/providers/country/[country]/page.tsxapps/ui/src/app/sitemap.tsapps/ui/src/components/providers/providers-grid.tsxapps/ui/src/content/changelog/2026-07-12-provider-headquarters-filter.mdpackages/models/src/compliance.spec.tspackages/models/src/providers.ts
| const modelCount = modelCountForProviders( | ||
| new Set(countryProviders.map((p) => p.id)), | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Model count in subheading may differ from the stats bar in ProvidersGrid.
modelCountForProviders counts unique models across all country providers, while ProvidersGrid computes totalModels as the sum of per-provider model counts (modelCounts[p.id]). When a model is served by multiple providers headquartered in the same country, the subheading (unique count) will show a smaller number than the stats bar (summed count), creating a visible inconsistency.
Consider passing a totalModels override to ProvidersGrid or computing the model count the same way in both places.
🔧 Suggested fix: pass model count to ProvidersGrid
interface ProvidersGridProps {
countryCode?: string;
heading?: string;
subheading?: string;
+ totalModelsOverride?: number;
}
export function ProvidersGrid({
countryCode,
heading,
subheading,
+ totalModelsOverride,
}: ProvidersGridProps = {}) {
// ...
const totalModels = visibleProviders.reduce(
(sum, p) => sum + (modelCounts[p.id] || 0),
0,
);
+ const displayModels = totalModelsOverride ?? totalModels;
// Use displayModels in the stats bar and default subheadingThen in the page:
<ProvidersGrid
countryCode={country.code}
heading={`${country.flag} AI Providers in ${country.name}`}
subheading={`Access ${modelCount} models from ${countryProviders.length} AI ${
countryProviders.length === 1 ? "provider" : "providers"
} headquartered in ${country.name} through our unified API`}
+ totalModelsOverride={modelCount}
/>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/ui/src/app/providers/country/`[country]/page.tsx around lines 52 - 54,
Align the country page’s model count with the stats bar in ProvidersGrid by
computing the summed per-provider model counts used there, or by passing that
value as a totalModels override to ProvidersGrid. Update the
modelCountForProviders usage and the ProvidersGrid call in the country page so
both the subheading and stats bar display the same count.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KUzb6QZFn2ngP6eohSDPdU
|
Images automagically compressed by Calibre's image-actions ✨ Compression reduced images by 78.7%, saving 744.3 KB.
|
|
Images automagically compressed by Calibre's image-actions ✨ Compression reduced images by 20.9%, saving 42.1 KB.
|
|
Images automagically compressed by Calibre's image-actions ✨ Compression reduced images by 13.8%, saving 22.0 KB.
|
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KUzb6QZFn2ngP6eohSDPdU
|
Images automagically compressed by Calibre's image-actions ✨ Compression reduced images by 71.5%, saving 1.5 MB.
|
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KUzb6QZFn2ngP6eohSDPdU
There was a problem hiding this comment.
🧹 Nitpick comments (1)
apps/gateway/src/api.spec.ts (1)
741-827: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider extracting shared test setup to reduce duplication.
Both test cases share nearly identical org update, API key insert, provider key insert, and request structures — differing only in
allowedCountries, token IDs, and expected status. Extracting a helper would improve maintainability if more country-policy cases are added later.♻️ Optional refactor: shared helper for country compliance tests
+async function setupCountryComplianceTest(opts: { + allowedCountries: string[]; + tokenSuffix: string; +}) { + await db + .update(tables.organization) + .set({ + plan: "enterprise", + providerCompliancePolicy: { enabled: true, allowedCountries: opts.allowedCountries }, + }) + .where(eq(tables.organization.id, "org-id")); + + const suffix = opts.tokenSuffix; + await db.insert(tables.apiKey).values({ + id: `token-id-${suffix}`, + token: `real-token-${suffix}`, + projectId: "project-id", + description: "Test API Key", + createdBy: "user-id", + }); + + await db.insert(tables.providerKey).values({ + id: `provider-key-id-${suffix}`, + token: "sk-test-key", + provider: "openai", + organizationId: "org-id", + baseUrl: mockServerUrl, + }); + + return `real-token-${suffix}`; +} + +async function requestChatCompletions(token: string) { + return app.request("/v1/chat/completions", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + "x-no-fallback": "true", + }, + body: JSON.stringify({ + model: "openai/gpt-4o", + messages: [{ role: "user", content: "Hello country!" }], + }), + }); +} + test("/v1/chat/completions blocks providers outside the allowed countries", async () => { // OpenAI is headquartered in the US, so an allowedCountries policy that only // permits France removes it, leaving no provider for the pinned model. - await db - .update(tables.organization) - .set({ - plan: "enterprise", - providerCompliancePolicy: { enabled: true, allowedCountries: ["FR"] }, - }) - .where(eq(tables.organization.id, "org-id")); - - await db.insert(tables.apiKey).values({ - id: "token-id-compliance-country-block", - token: "real-token-compliance-country-block", - projectId: "project-id", - description: "Test API Key", - createdBy: "user-id", - }); - - await db.insert(tables.providerKey).values({ - id: "provider-key-id-compliance-country-block", - token: "sk-test-key", - provider: "openai", - organizationId: "org-id", - baseUrl: mockServerUrl, - }); - - const res = await app.request("/v1/chat/completions", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer real-token-compliance-country-block", - "x-no-fallback": "true", - }, - body: JSON.stringify({ - model: "openai/gpt-4o", - messages: [{ role: "user", content: "Hello country!" }], - }), - }); + const token = await setupCountryComplianceTest({ + allowedCountries: ["FR"], + tokenSuffix: "compliance-country-block", + }); + const res = await requestChatCompletions(token); expect(res.status).toBe(403); const json = await res.json(); expect(json.error.message).toContain("provider compliance policy"); }); test("/v1/chat/completions allows providers within the allowed countries", async () => { // OpenAI is headquartered in the US, so an allowedCountries policy that // permits the US lets it through. - await db - .update(tables.organization) - .set({ - plan: "enterprise", - providerCompliancePolicy: { enabled: true, allowedCountries: ["US"] }, - }) - .where(eq(tables.organization.id, "org-id")); - - await db.insert(tables.apiKey).values({ - id: "token-id-compliance-country-allow", - token: "real-token-compliance-country-allow", - projectId: "project-id", - description: "Test API Key", - createdBy: "user-id", - }); - - await db.insert(tables.providerKey).values({ - id: "provider-key-id-compliance-country-allow", - token: "sk-test-key", - provider: "openai", - organizationId: "org-id", - baseUrl: mockServerUrl, - }); - - const res = await app.request("/v1/chat/completions", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer real-token-compliance-country-allow", - "x-no-fallback": "true", - }, - body: JSON.stringify({ - model: "openai/gpt-4o", - messages: [{ role: "user", content: "Hello country!" }], - }), - }); + const token = await setupCountryComplianceTest({ + allowedCountries: ["US"], + tokenSuffix: "compliance-country-allow", + }); + const res = await requestChatCompletions(token); expect(res.status).toBe(200); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/gateway/src/api.spec.ts` around lines 741 - 827, Extract the duplicated setup and request logic from the country-policy tests into a shared helper, parameterized by allowedCountries, token/key identifiers, and expected response status. Update the tests “blocks providers outside the allowed countries” and “allows providers within the allowed countries” to use the helper while preserving their distinct policy values and assertions.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@apps/gateway/src/api.spec.ts`:
- Around line 741-827: Extract the duplicated setup and request logic from the
country-policy tests into a shared helper, parameterized by allowedCountries,
token/key identifiers, and expected response status. Update the tests “blocks
providers outside the allowed countries” and “allows providers within the
allowed countries” to use the helper while preserving their distinct policy
values and assertions.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 48170a45-371e-4b49-9dce-9198d455e956
⛔ Files ignored due to path filters (1)
apps/ui/public/changelog/provider-headquarters-filter.pngis excluded by!**/*.png
📒 Files selected for processing (1)
apps/gateway/src/api.spec.ts
Fail when a provider headquarters code lacks a display name or a valid flag emoji, so future country additions to the catalogue force the mappings to be updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KUzb6QZFn2ngP6eohSDPdU
The requireSoc2 flag accepted any SOC 2 report while the UI labelled it "Type 2", so a Type 1 provider slipped through. Relabel requireSoc2 as "any type" and add a strict requireSoc2Type2 requirement (soc2 === 2), enforced through the same isProviderCompliant chokepoint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KUzb6QZFn2ngP6eohSDPdU
Existing requireSoc2 policies were labelled "Type 2" in the UI, so treat them as Type 2: a data migration converts stored requireSoc2:true to requireSoc2Type2:true. Also make the "SOC 2 or ISO 27001" toggle require SOC 2 Type 2 (not any type) or ISO 27001. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KUzb6QZFn2ngP6eohSDPdU
…rters-compliance # Conflicts: # packages/db/migrations/meta/_journal.json
Summary
Adds a provider headquarters / country filter to the enterprise compliance policy and makes the gateway respect it per organization. Also adds compliance + country filtering to the public providers directory, generates per-country provider pages, distinguishes SOC 2 Type 1 vs Type 2 in the policy (with a data migration for existing policies), and tightens documentation so catalogue-derived lists aren't hardcoded.
Screenshots (verified running locally)
Compliance settings — Provider Headquarters selector. United States + France selected; the Provider Impact panel updates live to
24 of 39 providers, correctly blocking China/Netherlands/Japan providers (DeepSeek, Alibaba, ByteDance, MiniMax, Nebius, Sakana, …) while keeping Mistral (France) allowed.Public providers directory — compliance + headquarters filters. Filtering by Headquarters = France, GDPR, and No-training narrows the grid to just Mistral AI (FR).
Per-country provider page (
/providers/country/us).Compliance policy + gateway enforcement
packages/models/src/providers.ts: add optionalallowedCountries?: string[]toProviderCompliancePolicy; extendisProviderCompliant()to enforce it. Fail-closed — when the list is non-empty, providers whoseheadquartersisn't in it (including unknown/nullHQ) are blocked. Composes with the existing certification/data-policy requirements.getProviderCountries()(authoritative closed set derived from the catalogue) andcountryCodeToFlag().isProviderCompliant(chat/videos filter candidates; embeddings/speech/moderations/ocr callassertProviderCompliant; images/anthropic/responses/mcp forward to/v1/chat/completions). Added gateway integration tests (api.spec.ts) proving the country filter blocks (403) and allows (200) on the real request path.SOC 2 Type 1 vs Type 2 (with migration)
Previously the single
requireSoc2flag accepted any SOC 2 report (Type 1 or 2), yet the dashboard labelled it "SOC 2 (Type 2)". This PR resolves the mismatch by honoring what admins saw:requireSoc2Type2requirement — strictdataPolicy.soc2 === 2— with its own toggle.requireSoc2remains available as "SOC 2 (Type 1 or 2)" for the genuinely-any case.packages/db/migrations/…_migrate_require_soc2_to_type2.sql): converts every storedrequireSoc2: trueintorequireSoc2Type2: true(dropping the old key), so existing policies now mean what their old "Type 2" label promised.false/absent/NULLpolicies are untouched. Validated against Postgres.requireSoc2OrIso27001now requires SOC 2 Type 2 or ISO 27001 (previously any SOC 2 or ISO 27001).Backward-compat answer: an existing policy from before equals
requireSoc2: true; after migration it becomesrequireSoc2Type2: true(Type 2 required), matching the intent the old label conveyed. Policies that didn't set SOC 2 are unaffected.API
apps/api/src/routes/organization.ts:allowedCountriesandrequireSoc2Type2added to theproviderCompliancePolicyZod schema;allowedCountriesis refined to reject any code not present in the catalogue.Compliance settings UI
compliance-client.tsx: a flex-wrapped flag-chip country selector, limited to catalogue countries, disabled unless the policy is enabled (empty selection = no restriction). The "Provider Impact" preview reflects the country filter and the SOC 2 toggles live.Public providers directory
/providers: a Headquarters country dropdown (flag + name) and Compliance requirement toggle chips, reusingisProviderCompliant— the same rules the gateway enforces — composing with search/sort, with a Clear button./providers/country/[country]pages (static params + SEO metadata + JSON-LD), reusingProvidersGrid(now filterable by country). Registered insitemap.ts; each provider card's HQ badge links to its country page.Documentation
features/compliance.mdx, updated requirements table (SOC 2 Type 1-or-2, SOC 2 Type 2, SOC 2 Type 2 or ISO 27001), and a new changelog entry.Notes
headquarterscode has a display-name + valid flag mapping, so future additions can't ship without them.Testing
packages/models/src/compliance.spec.ts: 26 tests — country filter (fail-closed, empty list, composition with certs), SOC 2 Type 1/2 distinction (incl. real Type 1 providercanopywave), the newrequireSoc2OrIso27001Type-2 semantics,getProviderCountries()/countryCodeToFlag(), and mapping-completeness guards.apps/gateway/src/api.spec.ts: gateway integration tests proving country enforcement (403/200) on the real/v1/chat/completionspath.requireSoc2:truerows converted; others untouched).turbo run build --filter=api --filter=ui— green.pnpm format— green.🤖 Generated with Claude Code
https://claude.ai/code/session_01KUzb6QZFn2ngP6eohSDPdU