Skip to content

feat(organizations): Enterprise groups with composable model-access policies - #4891

Merged
jrf0110 merged 9 commits into
mainfrom
feature/groups
Aug 3, 2026
Merged

feat(organizations): Enterprise groups with composable model-access policies#4891
jrf0110 merged 9 commits into
mainfrom
feature/groups

Conversation

@jrf0110

@jrf0110 jrf0110 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds Enterprise-only, organization-local member groups that compose strict, discriminated policies. Groups are flat and many-to-many with direct members. Model access is the first policy type, built so that adding another requires an explicit entry in each layer's exhaustive registry.

Screenshot 2026-07-31 at 12 26 37 PM Screenshot 2026-07-31 at 12 26 44 PM Screenshot 2026-07-31 at 12 26 54 PM Screenshot 2026-07-31 at 12 27 00 PM Screenshot 2026-07-31 at 12 27 25 PM Screenshot 2026-07-31 at 12 27 33 PM Screenshot 2026-07-31 at 12 27 49 PM Screenshot 2026-07-31 at 12 30 39 PM
  • Data model: organization_groups, organization_group_memberships, and organization_group_policy_settings tables with a single generated Drizzle migration (0203_organization_groups.sql).
  • Domain: organization-scoped advisory locking, revision tracking, strict policy parsing, CRUD, assignments, and auditing — nine audit actions covering group, membership, and policy changes.
  • API: organizations.groups tRPC router gated on Enterprise plan and role (owner manages; billing manager read-only; members see only their own group names).
  • Enforcement: additive model-access evaluation layered on the organization ceiling, applied across the OpenRouter/FIM/edit/embedding/transcription gateways, model and default catalogs, provider-route metadata, Vercel BYOK routing, and Slack/Discord/Linear/GitHub integration defaults. Direct BYOK and custom LLM access remain organization-wide.
  • UI: DrawerStack-based management — group details with an inline policy collection, an add-policy type picker, and per-policy editors. The model-access editor reuses the existing model/provider selector with group-specific grant semantics.

Effective access

A member's access is organization ceiling ∩ (default policies + the union of their group grants).

Absence of configuration is never a restriction: when no default or group policy applies, the member keeps existing organization-wide access, and only an explicit none mode (or an empty selected grant) yields no access. Organization model deny lists and provider allow-lists remain a hard ceiling that group grants cannot exceed.

Policy architecture

Persistence stays generic — @kilocode/db/schema-types owns only the jsonb column shapes and holds no runtime policy logic. Everything else is split by runtime boundary:

  • runtime Zod contracts and per-policy schemas under apps/web/src/lib/organizations/group-policies/
  • server normalization and evaluation in *.server.ts plus registry.server.ts
  • client labels, icons, summaries, and editors under components/organizations/groups/policies/ plus registry.client.ts

Exhaustive registries keyed by OrganizationGroupPolicyType force a new variant to be implemented in every layer, and each schema asserts structural compatibility with its persisted DB shape through AssertTrue, so drift fails typecheck rather than passing silently.

UX notes

  • Each policy type declares its own icon, matching the product surface it governs (Model Access → Layers).
  • The policy type picker ends with a non-interactive "More policies coming soon" row, so it still reads as a list of choices once model access is configured.
  • Compact drawer rhythm (p-5, eyebrow section labels); clickable rows use cursor-pointer.
  • "Add policy" opens the type picker directly; an existing policy row opens its editor directly.
  • Policy removal lives in the policy editor as an inline two-step confirm rather than a dialog: the drawer stack renders above the dialog layer, so a modal opened from inside a drawer would appear behind it.
  • The model-access editor enforces the organization ceiling and marks out-of-ceiling rows as unavailable.

Deploy note

defaultFreeModel on GET /api/organizations/:id/defaults is now string | null. It returns null only when the organization's policy denies every free model, rather than falling back to defaultModel, which may be paid. Client handling of null should be confirmed in Kilo-Org/kilocode before this ships.

Rebase note

This branch has been rebased onto main twice. #4846 (owner-configurable Efficient model pools) made a policy subject required on getAvailableModelsForOrganization, so auto-routing-pool-validation.ts passes { type: 'defaultAccess' }: pool membership is organization-level configuration, and a member subject would additionally require direct membership that a parent-organization owner or platform admin may not have. The second rebase also renumbered this branch's migration to 0203.

Testing

  • Focused suites passing: organization-groups, effective-model-access.server, the defaults route, the OpenRouter gateway route, the Vercel provider (including new BYOK-pinning regressions), and auto-routing pool validation.
  • pnpm --filter web typecheck, pnpm --filter @kilocode/db typecheck, web + db lint, pnpm format:check, and git diff --check — passing.
  • pnpm drizzle generate reports no pending schema changes against the committed migration.
  • Browser-tested create → add policy → edit policy → remove → back navigation, plus the 375px layout.

Notes

  • Enterprise-only; Teams stores but does not enforce group policy, and neither the API nor the UI is exposed there.
  • The product contract and session handoff documents are kept outside this repository.

Comment thread apps/web/src/lib/organizations/organization-groups.ts Outdated
Comment thread apps/web/src/lib/organizations/organizations.ts Outdated
Comment thread apps/web/src/app/api/gateway/embedding-models/route.ts Outdated
Comment thread apps/web/src/lib/organizations/organization-groups.ts
Comment thread apps/web/src/lib/ai-gateway/providers/vercel/index.ts Outdated
Comment thread apps/web/src/app/api/organizations/[id]/defaults/route.ts
Comment thread apps/web/src/app/api/openrouter/models-by-provider/route.ts Outdated
Comment thread apps/web/src/app/api/openrouter/models-by-provider/route.ts Outdated
Comment thread apps/web/src/routers/organizations/organization-groups-router.ts Outdated
Comment thread apps/web/src/components/organizations/groups/OrganizationGroupsPage.tsx Outdated
@kilo-code-bot

kilo-code-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

The incremental revision adds allowNonMember: true to two authorized read paths (modelPreferences.get and listAvailableModels) so Kilo admins and parent-organization owners resolve organization-level policy instead of 500ing, plus a legacy-restrictions parity test suite; all verified against current code with no new findings.

Verified in this revision
  • allowNonMember reachability — in model-preferences-router.ts, getAllowedModelIdsForOrg has a single caller (get, line 61) gated by ensureOrganizationAccess (line 57); in organization-settings-router.ts, listAvailableModels runs behind organizationMemberProcedure, which calls ensureOrganizationAccess (routers/organizations/utils.ts line 238). The flag is only reachable after authorization, matching its documented contract (organization-group-policy-context.server.ts lines 25-32): non-member callers resolve against organization-level policy only, with the group query skipped (isDirectMember gate, line 153). The inline comments' claims are accurate: ensureOrganizationAccess admits is_admin users (line 35) and parent-org owner/billing_manager inherit rows (lines 48-59) without direct membership. Gateway/token-authenticated enforcement paths keep the strict subject and still fail closed.
  • Parity suite correctness — every import in legacy-model-restrictions-parity.test.ts resolves (getEffectiveModelDecision/resolveOrganizationMemberModelPolicy via the effective-model-access.server barrel re-export; helpers createTestOrganization/insertTestUser confirmed, with createTestOrganization setting plan: 'teams' when requireSeats is true). Expected decisions were hand-traced for all four fixture models (denied/model, allowed/model, blocked-provider/model, unknown-routes/model) against both the legacy predicate (createAllowPredicateFromRestrictions) and the new evaluator (getEffectiveModelDecision) across all four cases — deny+allow lists, deny-only, provider-only, and Teams-unenforced — including the empty-routes branch where both return allowed. The policyRevision === 0 assertion matches settings?.policy_revision ?? 0, and the no-materialization assertion holds because the policy read path performs only SELECTs. The afterEach cleanup splices both module-level tracking arrays to empty, so no cross-test state or memory accumulation.
Files Reviewed (3 files)
  • apps/web/src/lib/organizations/legacy-model-restrictions-parity.test.ts (new)
  • apps/web/src/routers/model-preferences-router.ts
  • apps/web/src/routers/organizations/organization-settings-router.ts

Scope: PR-only incremental diff b7bd102b..1624c27bd. Assumption stated for transparency: this was a read-only review, so the Jest suite was not executed — parity was verified by tracing each fixture through both the legacy predicate and the new evaluator implementations, and authorization claims were verified against ensureOrganizationAccess source.

Previous Review Summaries (5 snapshots, latest commit b7bd102)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit b7bd102)

Status: No Issues Found | Recommendation: Merge

Executive Summary

The incremental revision removes the unenforced MCP server access policy end-to-end and fixes the authorized-non-member 500 in the organization defaults endpoint; both changes verified against the current code with no new findings.

Verified in this revision
  • Authorized non-member 500 (previously WARNING) — fixed. subject: { type: 'member', kiloUserId, allowNonMember: true } now suppresses the UNAUTHORIZED throw only for callers already authorized elsewhere, and getOrganizationGroupPolicyContext gates the group-membership query on isDirectMember so a non-member resolves against organization-level policy. Confirmed against getAuthorizedOrgContext, which grants non-membership access only to Kilo admins and parent-organization owner/billing_manager roles, so the flag's documented scope matches reality. Enforcement paths (organization-models.ts, model-access.server.ts, openrouter/[...path]) all omit the flag and still fail closed; no call site forwards a caller-controlled subject. Organization-level deny list and provider ceiling still apply to the non-member path via evaluateEffectiveModelAccessPolicy, and the new regression test asserts a 200 with PRIMARY_DEFAULT_MODEL.
  • MCP server access policy removal — complete and consistent. Repository-wide search finds no remaining mcp_server_access / McpServerAccess references: the schema, server normalizer, tRPC editor-data branch, client definition, registry entries (registry.client.ts, registry.server.ts), and the DB structural type were all dropped together, so OrganizationGroupPolicyType, the discriminated union, the z.enum, and both exhaustive registries stay in agreement. The organization_group_policy_settings migration carries no type-specific constraint or seeded policy, and normalizeOrganizationGroupPolicy remains referenced by the trimmed test file.
  • PolicyTypePickerPanel — the removed empty state is replaced by a non-interactive "More policies coming soon" row whose copy branches on available.length; loading and error branches are unchanged and no interactive affordance was lost.
Files Reviewed (15 files)
  • apps/web/src/app/api/organizations/[id]/defaults/route.ts
  • apps/web/src/app/api/organizations/[id]/defaults/route.test.ts
  • apps/web/src/lib/organizations/organization-group-policy-context.server.ts
  • apps/web/src/lib/organizations/group-policies/organization-group-policies.ts
  • apps/web/src/lib/organizations/group-policies/registry.server.ts
  • apps/web/src/lib/organizations/group-policies/mcp-server-access/mcp-server-access.schema.ts (deleted)
  • apps/web/src/lib/organizations/group-policies/mcp-server-access/mcp-server-access.server.ts (deleted)
  • apps/web/src/lib/organizations/organization-groups.test.ts
  • apps/web/src/routers/organizations/organization-groups-router.ts
  • apps/web/src/components/organizations/groups/drawer/PolicyTypePickerPanel.tsx
  • apps/web/src/components/organizations/groups/policies/registry.client.ts
  • apps/web/src/components/organizations/groups/policies/mcp-server-access/McpServerAccessPolicyEditor.tsx (deleted)
  • apps/web/src/components/organizations/groups/policies/mcp-server-access/McpServerAccessPolicyListItem.tsx (deleted)
  • apps/web/src/components/organizations/groups/policies/mcp-server-access/mcp-server-access.definition.client.tsx (deleted)
  • packages/db/src/schema-types.ts

Scope: PR-only incremental diff 444706c4..b7bd102b (the branch was rebased onto main@22a125a4, so dd2deab3..HEAD also contains unrelated upstream commits; those were excluded). Assumptions stated for transparency: this was a read-only review, so no Jest suite or typecheck was executed — the removal's type consistency was verified by reading every exhaustive registry/union site and by a repository-wide search for residual identifiers. Earlier remarks the author declined with a stated rationale (defaultFreeModel: string | null, organizations.ts lock order, fail-open evaluator contract, no re-added preview gate) are unchanged here and are not re-raised.

Previous review (commit dd2deab)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The defaults endpoint now evaluates the policy against the already-authorized organization row (good), but the member subject it pairs with still turns Kilo-admin and parent-org inherited access into an uncaught UNAUTHORIZED and a 500.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/web/src/app/api/organizations/[id]/defaults/route.ts 41 subject: { type: 'member' } requires a direct membership row, so Kilo admins and parent-org owners/billing managers that getAuthorizedOrgContext explicitly authorizes hit the policy context's UNAUTHORIZED throw; the handler has no try/catch, so an authorized request that returned 200 on main now returns 500
Verified as fixed in this revision
  • Vercel BYOK fixture (providers/vercel/index.test.ts): the partial-ignore test now pairs Anthropic direct with Bedrock — both real serving endpoints for anthropic/claude-sonnet-4.5 — so the asserted only: ['bedrock'] is reachable in production. Verified the expected byok shape matches parseAwsCredentials / AwsCredentialsSchema (accessKeyId, secretAccessKey, region) and that bedrock is a member of VercelUserByokInferenceProviderIdSchema.
  • Policy/organization snapshot consistency (organization-group-policy-context.server.ts): the new resolveOrganization fails closed on the caller-supplied row — it rejects an id mismatch and a non-null deleted_at, matching the isNull(deleted_at) filter of the read it replaces, and still runs the membership and settings/group queries inside the repeatable read transaction. The fallback DB read path is unchanged.
Files Reviewed (3 files)
  • apps/web/src/app/api/organizations/[id]/defaults/route.ts - 1 issue
  • apps/web/src/lib/organizations/organization-group-policy-context.server.ts - reviewed (fail-closed checks verified against the replaced query and against getAuthorizedOrgContext / getOrganizationById, both of which always return a row whose id matches and whose deleted_at is null)
  • apps/web/src/lib/ai-gateway/providers/vercel/index.test.ts - reviewed (fixture verified against applyVercelSettings, getVercelInferenceProviderConfigForUserByok, and AwsCredentialsSchema)

Scope: incremental diff 9c57da61..dd2deab3 (3 code files, ~57 changed lines; .plans/groups*.md and their .gitignore allowlist entries were removed and have no remaining references in the repo). Earlier remarks the author declined with a stated rationale (defaultFreeModel: string | null contract, organizations.ts lock order) are unchanged in this revision and are not re-raised. No commands were run against application code (read-only review), so the Jest suites and typecheck were not executed; the BYOK fixture and the new policy-context branch were verified by reading the production code paths they exercise.

Fix these issues in Kilo Cloud

Previous review (commit 9c57da6)

Status: 1 Issue Found | Recommendation: Safe to merge after optional cleanup

Executive Summary

This revision fixes all four previously-flagged issues it targets (BYOK pinning, Slack default model, prefetch rejection handling, inert type assertions); the only remaining remark is a Vercel BYOK test fixture that asserts a provider/model pairing production can never produce.

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
apps/web/src/lib/ai-gateway/providers/vercel/index.test.ts 149 Fixture pairs an openai credential with anthropic/claude-sonnet-4.5; checkVercelBYOK only returns providers that serve the requested model, so the test locks in an unroutable only: ['openai'] expectation
Verified as fixed in this revision
  • BYOK pinning (providers/vercel/index.ts): provider.ignore can no longer empty the map — retainedByokProviders falls back to allByokProviders, so only/byok always carry a credential. Confirmed the retained set is always a subset of the model's own BYOK endpoints (getModelUserByokProviders), so partial-ignore cannot strand a request on a non-serving provider.
  • Slack default model (slack-bot/model-allow-list.ts): the unrestricted fast path returns globalDefault, byte-for-byte matching main's hasActiveModelRestrictions branch; settings.default_model is now only consulted on the restricted path after isAllowed. The unrestricted predicate (unrestricted grant + empty deny list + no provider ceiling) is equivalent to main's check, including provider_allow_list: [] still counting as restrictive.
  • Prefetch rejection (api/openrouter/[...path]/route.ts:480): void organizationGroupPolicyPromise?.catch(() => {}) attaches a handler to the original promise, so early returns and bypassAccessCheck paths cannot raise an unhandled rejection while the later await still observes the original error.
  • Type assertions (model-access.schema.ts, mcp-server-access.schema.ts): AssertTrue<T extends true> with false branches now makes schema/DB drift a typecheck error instead of silently resolving to never.
  • MCP copy (mcp-server-access.definition.client.tsx): the policy-type picker now states the grant is not enforced at the gateway yet, matching the editor's caveat.
Files Reviewed (7 files)
  • apps/web/src/lib/ai-gateway/providers/vercel/index.test.ts - 1 issue
  • apps/web/src/lib/ai-gateway/providers/vercel/index.ts - reviewed (BYOK fallback verified against getModelUserByokProviders and shouldRouteToVercel)
  • apps/web/src/lib/slack-bot/model-allow-list.ts - reviewed (diffed against main behavior)
  • apps/web/src/app/api/openrouter/[...path]/route.ts - reviewed
  • apps/web/src/lib/organizations/group-policies/model-access/model-access.schema.ts - reviewed
  • apps/web/src/lib/organizations/group-policies/mcp-server-access/mcp-server-access.schema.ts - reviewed
  • apps/web/src/components/organizations/groups/policies/mcp-server-access/mcp-server-access.definition.client.tsx - reviewed

Scope: incremental diff 6fc2efc2..9c57da61 (7 files, ~103 changed lines). Earlier remarks the author declined with a stated rationale (model-access fail-open contract, organizations.ts lock order, defaultFreeModel: string | null) are in files untouched by this revision and are not re-raised. No commands were run against application code (read-only review); typecheck was not executed here, so the new AssertTrue assertions were verified by reading only.

Fix these issues in Kilo Cloud

Previous review (commit 6fc2efc)

Status: 9 Issues Found | Recommendation: Address before merge

Executive Summary

The rebase onto main plus the policy-gate rewrite removed the enable/preview safeguard, so Enterprise model-access enforcement now flips implicitly and removing the default model_access policy fails open to the full catalog; six findings from the previous review are still unresolved.

Overview

Severity Count
CRITICAL 0
WARNING 4
SUGGESTION 5
Issue Details (click to expand)

WARNING

File Line Issue
apps/web/src/lib/organizations/group-policies/model-access/model-access.server.ts 64 Enforcement is now implicit; removing the last model_access policy silently grants the full catalog instead of tightening access
apps/web/src/lib/ai-gateway/providers/vercel/index.ts 275 Caller provider.ignore can empty byokProviders, sending only: [] / byok: {} so BYOK pinning is dropped
apps/web/src/lib/slack-bot/model-allow-list.ts 31 Unrestricted fast path now returns settings.default_model; pre-PR it returned globalDefault, changing bot defaults silently
apps/web/src/app/api/organizations/[id]/defaults/route.ts 20 defaultFreeModel widened to string | null; shipped clients have never received a null for this field

SUGGESTION

File Line Issue
apps/web/src/components/organizations/groups/drawer/PolicyEditorPanel.tsx 102 Default-policy save is an org-wide lockout with no impact estimate now that previewPolicy is deleted
apps/web/src/components/organizations/groups/policies/mcp-server-access/mcp-server-access.definition.client.tsx 18 Picker copy promises MCP access control that the gateway does not enforce yet
apps/web/src/app/api/openrouter/[...path]/route.ts 469 Prefetched policy promise is never awaited on early-return and bypassAccessCheck paths — unhandled rejection plus a discarded read-only transaction
apps/web/src/lib/organizations/organizations.ts 344 Policy advisory lock is taken after the membership insert, inverting lock order against removeUserFromOrganization
apps/web/src/lib/organizations/group-policies/model-access/model-access.schema.ts 31 _AssertModelAccessMatchesDb resolves to never instead of erroring, so schema/DB-type drift never fails typecheck (the new mcp-server-access.schema.ts copies the same inert pattern)
Verified as fixed in this revision
  • The enabled_policy_types column, setPolicyTypeEnabled mutation, previewOrganizationGroupPolicy, and the preview/confirm UI are fully removed, with no dangling references or unused imports; the snapshot and schema.ts agree.
  • Migration renumbered to 0201_organization_groups.sql after the rebase; _journal.json and 0201_snapshot.json are consistent, and all DDL targets newly created tables.
  • PolicyEditorPanel now guards on !policies before find, reports save and remove failures via toast, and only exposes onDelete for a persisted policy.
  • registry.client.ts widening (organizationGroupPolicyDefinition) keeps per-policy definitions strictly typed while shared drawer/page code passes union values; runtime pairing is guaranteed by the key.
  • buildEligibleCatalog resolves the org catalog with { type: 'defaultAccess' }, which is the correct subject for organization-level pool configuration.
Files Reviewed (incremental diff since `7d8535a4`, rebase-normalized)
  • apps/web/src/lib/organizations/group-policies/model-access/model-access.server.ts - 2 issues
  • apps/web/src/components/organizations/groups/drawer/PolicyEditorPanel.tsx - 1 issue
  • apps/web/src/components/organizations/groups/policies/mcp-server-access/mcp-server-access.definition.client.tsx - 1 issue
  • apps/web/src/lib/organizations/group-policies/mcp-server-access/mcp-server-access.schema.ts - reviewed (new policy type, strict union, uuid config ids)
  • apps/web/src/lib/organizations/group-policies/mcp-server-access/mcp-server-access.server.ts - reviewed (normalization, enterprise-gated editor data)
  • apps/web/src/components/organizations/groups/policies/mcp-server-access/{McpServerAccessPolicyEditor,McpServerAccessPolicyListItem}.tsx - reviewed
  • apps/web/src/components/organizations/groups/policies/{PolicyEditorFooter.tsx,registry.client.ts,types.ts} - reviewed
  • apps/web/src/components/organizations/groups/{OrganizationGroupsPage.tsx,drawer/GroupDetailsPanel.tsx,drawer/GroupPoliciesPanel.tsx,drawer/PolicyTypePickerPanel.tsx,drawer/renderOrganizationGroupsDrawerContent.tsx} - reviewed
  • apps/web/src/components/organizations/groups/policies/model-access/{ModelAccessPolicyEditor,model-access.definition.client}.tsx - reviewed
  • apps/web/src/lib/organizations/{organization-groups.ts,organization-group-policy-context.server.ts,organization-groups.test.ts,effective-model-access.server.test.ts} - reviewed
  • apps/web/src/lib/organizations/group-policies/{organization-group-policies.ts,registry.server.ts} - reviewed
  • apps/web/src/routers/organizations/organization-groups-router.ts - reviewed
  • apps/web/src/lib/ai-gateway/auto-routing-pool-validation.ts - reviewed (rebase adaptation)
  • apps/web/src/app/api/organizations/[id]/defaults/route.ts, apps/web/src/lib/slack-bot/model-allow-list.ts - reviewed
  • packages/db/src/{schema.ts,schema-types.ts,migrations/0201_organization_groups.sql,migrations/meta/*} - reviewed
  • .gitignore, .plans/groups.md, .plans/groups-handoff.md - reviewed (no HTML <img> tags)

Assumptions: the branch was rebased onto a463a51e, so the incremental scope was computed by diffing the previous PR patch against the current one; upstream-only changes carried in by the rebase (the Vercel laguna guard, skipCustomerSourceSurveyForOrgJoin, cli_sessions_v2.last_activity_at) were excluded. No commands were run against the codebase (read-only review).

Fix these issues in Kilo Cloud

Previous review (commit 7d8535a)

Status: 6 Issues Found | Recommendation: Address before merge

Executive Summary

Incremental review of 7d8535a4: all 13 previous findings are fixed, but the Vercel BYOK fix now lets a caller's provider.ignore empty the BYOK map, sending only: [] / byok: {} while the request still bypasses the balance check.

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 3
Issue Details (click to expand)

WARNING

File Line Issue
apps/web/src/lib/ai-gateway/providers/vercel/index.ts 270 New provider.ignore filter can empty byokProviders, so BYOK is no longer pinned while the request still skips the balance <= 0 rejection
apps/web/src/lib/slack-bot/model-allow-list.ts 31 Unrestricted fast path returns settings.default_model unvalidated; pre-PR it returned globalDefault, so bot integration defaults change silently
apps/web/src/app/api/organizations/[id]/defaults/route.ts 20 defaultFreeModel widened to string | null; shipped clients have never received a null for this field

SUGGESTION

File Line Issue
apps/web/src/app/api/openrouter/[...path]/route.ts 469 Prefetched policy promise is never awaited on early-return and bypassAccessCheck paths — unhandled rejection plus a discarded read-only transaction
apps/web/src/lib/organizations/organizations.ts 328 Policy advisory lock now taken after the membership insert, inverting lock order against removeUserFromOrganization
apps/web/src/lib/organizations/group-policies/model-access/model-access.schema.ts 31 _AssertModelAccessMatchesDb resolves to never instead of erroring, so schema/DB-type drift never fails typecheck
Previous findings re-verified as fixed
  • Preview N+1: previewOrganizationGroupPolicy now loads members, groups, and group memberships in 3 queries, memoizes per distinct group set, and short-circuits on the first allowed model.
  • Revision bump now runs only inside if (added), so no-op SSO re-login inserts no longer bump policy_revision or take the org policy lock.
  • embedding-models, transcription-models, models-by-provider, and defaults resolve the member policy once and loop with getEffectiveModelDecision.
  • Rename conflicts: assertGroupNameAvailable (with excludeGroupId) is applied in create, updateOrganizationGroupMetadata, and updateOrganizationGroupDetails, and its lower(btrim(name)) predicate matches UQ_organization_groups_organization_id_canonical_name; the check runs under the org policy advisory lock.
  • Vercel BYOK: the throw and the org/group provider.only constraint are gone (consistent with ai-gateway/AGENTS.md).
  • PolicyEditorPanel.onSave wraps both mutations in try/catch with a toast.error.
  • Integration updateModel uses isOrganizationModelUpdateAllowed, which maps the policy-context NOT_FOUND back to the historical allow-and-return-{ success } behavior.
  • models-by-provider returns private, no-store for the per-caller body and private, max-age=0, must-revalidate otherwise; manager-view special-casing removed, and the inline decision check is equivalent to isModelRouteAllowed.
  • setPolicyTypeEnabled now uses organizationOwnerMutationProcedure.
  • Groups page distinguishes loading / error / zero-affected states and disables the switch until settingsQuery.data is present.
  • Module move: no remaining references to @kilocode/db/organization-group-policies; the export was removed from packages/db/package.json and the app-side barrel is no longer index.ts.
Files Reviewed (32 files in the incremental diff)
  • apps/web/src/lib/ai-gateway/providers/vercel/index.ts - 1 issue
  • apps/web/src/lib/slack-bot/model-allow-list.ts - 1 issue
  • apps/web/src/app/api/organizations/[id]/defaults/route.ts - 1 issue
  • apps/web/src/app/api/openrouter/[...path]/route.ts - 1 issue
  • apps/web/src/lib/organizations/organizations.ts - 1 issue
  • apps/web/src/lib/organizations/group-policies/model-access/model-access.schema.ts - 1 issue
  • apps/web/src/lib/organizations/organization-groups.ts - reviewed (preview rewrite, name-conflict helper)
  • apps/web/src/lib/organizations/group-policies/model-access/model-access.server.ts - reviewed
  • apps/web/src/lib/organizations/group-policies/organization-group-policies.ts - reviewed
  • apps/web/src/lib/organizations/group-policies/registry.server.ts - reviewed
  • apps/web/src/lib/organizations/organization-group-policy-context.server.ts - reviewed
  • apps/web/src/app/api/gateway/{embedding,transcription}-models/route.ts - reviewed
  • apps/web/src/app/api/openrouter/models-by-provider/route.ts - reviewed
  • apps/web/src/lib/integrations/{discord,slack,linear,github-apps}-service.ts - reviewed
  • apps/web/src/routers/organizations/organization-groups-router.ts - reviewed
  • apps/web/src/components/organizations/groups/** (page, drawer panels, policy registry/definitions, types) - reviewed
  • apps/web/src/lib/organizations/organization-groups.test.ts - reviewed
  • packages/db/src/schema-types.ts, schema-types.test.ts, package.json - reviewed

Fix these issues in Kilo Cloud

Additional previous summary content was truncated to keep this comment within platform limits.


Reviewed by kimi-k3 · Input: 67.5K · Output: 12.4K · Cached: 756.7K

Review guidance: REVIEW.md from base branch main

Comment thread apps/web/src/app/api/openrouter/[...path]/route.ts
Comment thread apps/web/src/lib/ai-gateway/providers/vercel/index.ts Outdated
Comment thread apps/web/src/lib/slack-bot/model-allow-list.ts Outdated
Comment thread apps/web/src/app/api/organizations/[id]/defaults/route.ts
Comment thread apps/web/src/app/api/openrouter/[...path]/route.ts
Comment thread apps/web/src/lib/organizations/organizations.ts
Comment thread apps/web/src/lib/organizations/group-policies/model-access/model-access.schema.ts Outdated
Comment thread apps/web/src/lib/ai-gateway/providers/vercel/index.test.ts Outdated
Comment thread apps/web/src/app/api/organizations/[id]/defaults/route.ts Outdated
jrf0110 added 8 commits August 3, 2026 08:32
Add Enterprise-only member groups that carry composable, strictly
discriminated policies, plus an organization-level set of default policies
applied to every direct member.

- model access policy (all, none, or selected models and providers) enforced
  at the gateway and reflected in catalogs, defaults, and integrations
- MCP server access policy for granting organization MCP Gateway servers;
  configuration only until gateway enforcement lands
- absence of an applicable policy preserves existing organization-wide access;
  only an explicit none mode grants nothing
- organization model and provider restrictions remain a hard ceiling that
  group grants cannot exceed
- DrawerStack-based management UI backed by exhaustive client and server
  policy registries, so a new policy type must declare every entry
- keep BYOK pinning when `provider.ignore` covers every provider the caller
  holds keys for; an empty BYOK map sent `only: []` with no credential, so the
  request billed Kilo's Vercel account while still skipping the zero-balance
  rejection as a BYOK request
- restore pre-policy parity in the Slack/Discord/Linear default-model helper:
  unrestricted organizations get `globalDefault` again instead of a possibly
  virtual `settings.default_model`
- mark the gateway group-policy prefetch handled so an early return or a
  bypassed request cannot turn a policy-context failure into an unhandled
  rejection
- make the schema/DB compatibility assertions real build errors via
  `AssertTrue`; the previous conditional types could resolve to `never`, which
  satisfies `extends true` and never failed typecheck
The policy type picker is where an owner commits to adding the policy, so the
caveat that MCP Gateway still resolves connections from per-member assignments
belongs there rather than only in a footnote inside the editor.
…rganization

`getOrganizationGroupPolicyContext` re-read the organization row, so the defaults
endpoint evaluated model access against a second read instead of the organization
it had already loaded and authorized. Callers holding the row now pass it in,
which also removes a duplicate read of the same row.

This restores the endpoint's deny-list fallback and its 409 "all models blocked"
response, both covered by the existing route tests.
…xture

`userByok` only contains providers that serve the requested model, so pairing an
OpenAI key with a Claude request pinned an unroutable expectation. Anthropic plus
Bedrock is the reachable partial-ignore case.
The groups product contract and session handoff now live in the local docs
directory, so the `.plans` allowlist entries for them are no longer needed.
That product is being retired, so the policy type is removed from the DB shape,
the runtime schemas, both registries, the editor UI, and the policy editor data
query. Model access is again the only policy type.

The type picker gains a non-interactive "More policies coming soon" row so it
still reads as a list of choices once model access is configured.
…non-members

Kilo admins and parent-organization owners are authorized for an organization
without holding a membership row, so the member policy subject rejected them with
an UNAUTHORIZED TRPCError that escaped this route as a 500. They belong to no
group, so they now resolve against organization-level policy via an explicit
`allowNonMember` subject flag.

Enforcement paths leave the flag unset and still fail closed for callers without
direct access.

@pandemicsyn pandemicsyn left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

lgtm

…ders

`ensureOrganizationAccess` grants Kilo admins and parent-organization owners
access without a membership row, so the member policy subject rejected them from
`listAvailableModels` and the model-preferences allow-list. Both are read paths
behind an authorization check, so they now pass `allowNonMember` and resolve
organization-level policy, matching pre-groups behavior.

The gateway and token-authenticated catalog routes keep the strict subject.

Adds a parity suite covering organizations that predate groups: legacy
model/provider restrictions in `organizations.settings` with no group and no
`organization_group_policy_settings` row must produce exactly the decisions the
pre-groups predicate produced, and reading policy must not materialize a row.
@jrf0110
jrf0110 merged commit af22820 into main Aug 3, 2026
50 checks passed
@jrf0110
jrf0110 deleted the feature/groups branch August 3, 2026 20:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants