Skip to content

feat!: scope chat model configs to organizations - #27955

Merged
ethanndickson merged 4 commits into
ethan/codagt-709-model-config-tx-readsfrom
ethan/codagt-709-model-config-org-schema
Aug 24, 2026
Merged

feat!: scope chat model configs to organizations#27955
ethanndickson merged 4 commits into
ethan/codagt-709-model-config-tx-readsfrom
ethan/codagt-709-model-config-org-schema

Conversation

@ethanndickson

@ethanndickson ethanndickson commented Aug 10, 2026

Copy link
Copy Markdown
Member

Summary

Scope chat model configurations and runtime model resolution to organizations. Each model configuration now belongs to exactly one organization, each organization can have its own default model, and Chatd only uses models that belong to the chat's organization.

Depends on #27968.

Problem

Chat model configurations are currently deployment-wide even though chats belong to organizations. A default model, explicitly selected model, or configured model override can therefore reference the same model configuration from every organization. This prevents model catalogs and defaults from being isolated by organization and allows a chat to retain or select a model that its organization does not own.

Existing model configurations also have no organization ownership or organization-scoped ACL data, and the database enforces a single live default across the entire deployment.

Fix

Add required organization ownership and ACL storage to chat model configurations. The migration assigns every existing configuration to the default organization, seeds organization-wide read access, and replaces the deployment-wide default constraint with a per-organization constraint.

Scope Chatd lookup, admission, fallback, and queue-promotion behavior to the chat's organization:

  • New chats use the usable default model from their organization.
  • Explicit model selections from another organization are rejected.
  • Deployment or personal overrides that point to a foreign model are not used for the chat.
  • Historical cross-organization model references remain unchanged in the database, but runtime generation falls back to a usable local default.
  • Queue promotion resolves unavailable, disabled, missing, or foreign stored models to the usable local default and fails when no usable local default exists.

The existing model configuration management and availability routes remain compatibility routes for the default organization in this layer. A later layer in the stack adds organization-scoped management routes and updates clients to use them.

Breaking change

This changes chat model behavior from deployment-wide to organization-scoped. Existing configurations are migrated only to the default organization; they are not copied into every organization.

As a result:

  • A non-default organization no longer inherits the deployment's existing default model.
  • Creating a chat in an organization without a usable local default fails with 400: No default chat model config is configured.
  • Explicitly selecting a configuration owned by another organization fails with 400: Model config does not belong to the specified organization.
  • Existing chats whose stored model belongs to another organization can generate only if a usable local default is available; otherwise generation fails.

Deployments using Agents in non-default organizations must configure a usable default chat model separately for each organization after the organization-scoped management layers in this stack are available.

This pull request description was generated by Coder Agents.

@linear-code

linear-code Bot commented Aug 10, 2026

Copy link
Copy Markdown

CODAGT-709

@ethanndickson
ethanndickson marked this pull request as ready for review August 10, 2026 03:56

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 425aa7af9a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread coderd/x/chatd/subagent.go Outdated
@ethanndickson

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-08-19 18:45 UTC by @ethanndickson

Review history
  • R1 (2026-08-10), 2 Nit, 1 Note, 2 P2, COMMENT. Review
  • R2 (2026-08-10): 23 reviewers, 8 Nit, 9 Note, 7 P2, 9 P3, 1 P4, COMMENT. Review
  • R3 (2026-08-18): 23 reviewers, 12 Nit, 12 Note, 2 P1, 9 P2, 16 P3, 1 P4, REQUEST_CHANGES. Review
  • R4 (2026-08-19): 22 reviewers, 22 Nit, 17 Note, 2 P1, 11 P2, 23 P3, 1 P4, REQUEST_CHANGES. Review

deep-review v0.9.0 | Round 4 | 033bf39..ad400e1

Last posted: Round 4, 76 findings (2 P1, 11 P2, 23 P3, 1 P4, 22 Nit, 17 Note), REQUEST_CHANGES. Review

Finding inventory

Finding inventory: PR #27955

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P2 Author fixed (ef9a1a7) coderd/x/chatd/subagent_internal_test.go:4936 Test name TestListSubagentModels_NonDefaultOrgSeesDefaultOrgConfigs claims the opposite of what the test asserts R1 Netero Yes
CRF-2 P2 Author fixed (ef9a1a7) coderd/x/chatd/configcache.go:307 Inline default-org fallback duplicates defaultOrgChatModelConfig (chatd.go:1685) in the same package R1 Netero Yes
CRF-3 Nit Author contested; panel closed R2 (unanimous accept: Netero, Bisky, Mafu-san, Pariston, Gon, Kurapika, Killua, Chopper, Zoro, Razor, Nami) coderd/database/queries/chatmodelconfigs.sql:18 GetDefaultChatModelConfig / UnsetDefaultChatModelConfigs gained an org filter without the ByOrganization suffix R1 Netero Yes
CRF-4 Nit Author fixed (ef9a1a7) coderd/database/migrations/migrate_test.go:2769 Migration 000565 test/fixture identifiers suffixed 552 (renumbering artifact) R1 Netero Yes
CRF-5 Note Author accepted R2 (fix lands in #27959; correct today under per-org lock) coderd/exp_chats.go:7133 ensureDefaultChatModelConfig fetches all orgs' configs and filters by org in Go R1 Netero Yes
CRF-6 Nit Author fixed (7a713c7) coderd/exp_chats.go:6705 Default-org resolution block (AsChatd call, rationale comment, 500 response) duplicated between createChatModelConfig and defaultCreateChatModelConfigID R2 Netero, Gon Yes
CRF-7 Note Author contested; panel closed R3 (3/4 accept: throughput not correctness; note the cited tests prove data isolation, not lock independence) coderd/exp_chats.go:6600 Per-org GenLockID switch has no test proving cross-org lock independence; property only load-bearing after cutover PRs R2 Netero Yes
CRF-8 Note Author accepted R3 (org-scoped availability belongs to the later API cutover) coderd/x/chatd/chatd.go:2784 Model resolution is org-scoped but getUserChatProviderAvailability still lists deployment-wide; seam if cutover lands before API PR R2 Netero Yes
CRF-9 P2 Author fixed (7a713c7) coderd/exp_chats.go:7136 Org isolation of default-config write path has zero discriminating coverage; deleting either org filter keeps all tests green R2 Bisky Yes
CRF-10 Note Author fixed (commit message rewritten) coderd/exp_chats.go:7088 Commit message attributes the locked-read rewrite (now in #27968) to this commit R2 Mafu-san Yes
CRF-11 P3 Author fixed (constant restored with Deprecated comment) coderd/database/lock.go:19 LockIDChatModelConfigWrites deleted instead of kept deprecated; frees ID 11 for silent reuse against the list's own rule R2 Mafuuu P3, Takumi Nit, Knov Nit Yes
CRF-12 Note Author contested; panel closed R3 (4/5 accept: deploy contract stands; documentation ask tracked as CRF-44) coderd/exp_chats.go:6600 Rolling upgrade: old/new replicas serialize on disjoint lock IDs; old-binary inserts fail NOT NULL; transient, self-healing, needs human sign-off R2 Hisoka, Mafuuu, Pariston, Takumi, Knov Yes
CRF-13 P2 Author fixed (7a713c7) coderd/exp_chats.go:4483 AsChatd escalation rationale comment is inaccurate (claims request carries no org; caller validates req.OrganizationID) and opens with a clause restating the code R2 Kite P3, Gon P2 Yes
CRF-14 Note Author accepted R3 (org-scoped creation belongs to the later API cutover) coderd/exp_chats.go:4497 defaultCreateChatModelConfigID pins the default org while req.OrganizationID is in hand; sibling of CRF-8 seam R2 Mafuuu, Ryosuke, Razor Yes
CRF-15 P3 Author fixed (8c03537, single GetEffectiveChatModelConfigsByOrganization query) coderd/x/chatd/subagent.go:722 Fallback reads rows and ownership marker in separate unlocked queries; race with an org's first config write yields a transient empty list neither snapshot would return (empirically constructed) R2 Komugi Yes
CRF-16 P2 Author fixed (grant removed from chatd subject) coderd/database/dbauthz/dbauthz.go:800 Org-read grant comment names one consumer of three and promises "removed in M3" that would break the exp_chats handlers; bare M3 token unanchored R2 Gon P2, Leorio P3 Yes
CRF-17 P2 Author fixed (comment removed) coderd/x/chatd/configcache.go:289 "the explosion migration" is plan jargon existing nowhere else in the repo; siblings say "org-scoping cutover" R2 Gon P2, Leorio Nit Yes
CRF-18 P2 Author fixed (7a713c7, SoleConfigRemainsDefaultWhenDemoted test) coderd/exp_chats.go:7118 ensureDefaultChatModelConfig maintains the load-bearing ownership invariant with no doc comment; inner comment misstates the all-excluded re-promotion; no test pins the demote-sole-config sequence R2 Gon P2, Takumi P3, Kite Nit, Pariston Note Yes
CRF-19 Nit Author fixed (helper removed in refactor) coderd/x/chatd/configcache_internal_test.go:88 Doc comment on totalDefaultModelConfigCalls restates the name R2 Gon Yes
CRF-20 Nit Author fixed (comment removed) coderd/database/migrations/migrate_test.go:2948 Doc comment on mustJSON restates the name and body R2 Gon Yes
CRF-21 Nit Author fixed (helper replaced by effectiveDefaultChatModelConfig) coderd/x/chatd/chatd.go:1683 defaultChatModelConfigForOrg name hides the fallback its sibling's name announces R2 Gon Yes
CRF-22 P3 Author fixed (7a713c7) coderd/exp_chats.go:4493 "Failed to resolve chat model config." covers three distinct 500 causes; identical failure reports differently in createChatModelConfig R2 Leorio Yes
CRF-23 Nit Author fixed (7a713c7) coderd/x/chatd/configcache_internal_test.go:41 Test stub call-counting uses sync.Once + syncmap + per-org atomics where zero-value syncmap or one mutex suffices R2 Ging-go, Zoro Yes
CRF-24 P3 Author fixed (casts dropped, typed ChatACL params) coderd/database/queries/chatmodelconfigs.sql:117 ::jsonb casts on @group_acl/@user_acl defeat the ChatACL sqlc override; every writer hand-marshals; wrong-shaped raw JSON passes the CHECK and poisons reads R2 Knuckle, Meruem, Robin Yes
CRF-25 Note Author fixed (immutability documented) coderd/exp_chats.go:6895 Advisory lock keyed off pre-lock read is safe only because organization_id is immutable; assumption undocumented R2 Meruem, Knuckle Yes
CRF-26 P3 Author fixed (grant removed; lookups via shared system-restricted helper) coderd/database/dbauthz/dbauthz.go:801 Chatd subject gains site-wide org read; purpose-built subjectDefaultOrgReader would avoid handlers escalating to full chatd authority (6 reviewers deem the grant itself acceptable; disagreement flagged) R2 Kurapika P3; Pariston/Meruem/Chopper/Razor/Kite/Knov Notes Yes
CRF-27 P4 Author fixed (8c03537, org-binding checks in chatd resolution) coderd/x/chatd/chatd.go:1605 Explicit model-config IDs validated for existence/enabled, never org binding; unexploitable pre-cutover; enforcement must land no later than cutover R2 Kurapika P4, Kite Note Yes
CRF-28 Nit Author fixed (TODO added) coderd/database/dbauthz/dbauthz.go:3747 GetEnabledChatModelConfigsByOrganization authorizes deployment-wide without the TODO(CODAGT-161) marker its sibling carries R2 Ryosuke, Kurapika Note Yes
CRF-29 Note Author fixed (single effective-configs query) coderd/x/chatd/subagent.go:707 Pre-cutover, non-default-org list paths pay 5 sequential DB round trips where 1 sufficed (common case until cutover); quantified, deleted at M3 R2 Killua Yes
CRF-30 P3 Author fixed (8c03537, helper owns the query) coderd/x/chatd/subagent.go:706 enabledChatModelConfigsWithDefaultOrgFallback takes rows as input, so both callers duplicate the query-then-fallback pair; absorb the query and co-locate with its sibling R2 Meruem P3, Robin P3, Zoro P3, Luffy Nit, Ryosuke Nit Yes
CRF-31 P3 Author fixed (cloneChatACL clones maps and Permissions slices) coderd/x/chatd/configcache.go:54 cloneModelConfig does not clone the new GroupACL/UserACL maps; cache and callers alias the same maps, load-bearing once the RBAC PR reads them R2 Razor, Nami Yes
CRF-32 P3 Author fixed (helper removed structurally) coderd/x/chatd/chatd.go:1695 defaultChatModelConfigForOrg propagates GetDefaultOrganization failure without step context; the only branch with zero coverage R2 Chopper Yes
CRF-33 Note Panel re-raised R3; author silent through R4 (three inline ACL copies, no shared owner) coderd/database/dbgen/dbgen.go:187 dbgen seeds group_acl {} while every production path seeds the everyone-in-org read entry; RBAC-PR tests will exercise unrepresentative fixtures; org/ACL defaulting also duplicated in insertChatModelConfigForTest R2 Chopper, Ryosuke, Robin Yes
CRF-34 P3 Author fixed (8c03537, effective-resolution query replaces inference) coderd/x/chatd/subagent.go:722 Fallback ownership decided by invariant inference; an EXISTS query on live configs makes the bad state impossible; M2 cutover migration is a write path bypassing ensureDefaultChatModelConfig R2 Ryosuke P3, Pariston Note Yes
CRF-35 Note Dropped by orchestrator (default org immutable: no query flips is_default after insert, verified in organizations.sql; premise unreachable via app code and 10s TTL is within the cache's stated contract) coderd/x/chatd/configcache.go:302 Cached per-org default depends on organizations.is_default with no invalidation event R2 Komugi No
CRF-36 P3 Merged into review body as process observation (code change is CRF-6) coderd/exp_chats.go:6705 Round-1 CRF-2 fix did not generalize: the sibling duplicated default-org block in exp_chats.go survived the fix commit R2 Mafu-san No
CRF-37 P2 Author fixed (duplicate query and Row-conversion helper removed) coderd/database/queries/chatmodelconfigs.sql:73 GetEffectiveChatModelConfigsByOrganization is a byte-identical duplicate of GetEnabledChatModelConfigsByOrganization added in the same PR (fallback OR-clause stripped in 8c03537); deriveEffectiveChatModelConfigs exists solely to convert between the two Row types R3 Netero Yes
CRF-38 Nit Author fixed (comment rewritten) coderd/x/chatd/configcache.go:431 InvalidateModelConfig comment still justifies coarse invalidation via the default-org fallback this PR removed (added a836568, removed 8c03537) R3 Netero Yes
CRF-39 P1 Author fixed (resolver takes req.OrganizationID; 400 names the missing per-org default) coderd/exp_chats.go:4535 Non-default-org chat creation unconditionally fails: resolver pins deployment-default org config on all three entry paths (omitted ID, explicit ID, personal override) while chatd's new org binding rejects it; no in-API remediation exists R3 Kite P1, Knuckle P1, Razor P1, Knov P1, Ryosuke P1, Mafuuu P1, Pariston P2, Luffy P2; codex P1 Yes
CRF-40 P1 Author fixed (fall-through); panel re-raised R4 (orgs without a local default, i.e. every non-default org in this PR, remain dead; human decision needed) coderd/x/chatd/chatd.go:1684 Pre-existing chats in non-default orgs are bricked: foreign LastModelConfigID is rejected, org owns no configs, every send/promotion/finish-turn/edit fails with ErrNoDefaultChatModelConfig R3 Hisoka P1, Kite P1 Yes
CRF-41 P2 Author fixed (SDK status+message asserted; positive and foreign-model siblings added) coderd/exp_chats_test.go:379 NonDefaultOrgDoesNotUseDeploymentDefault asserts bare require.Error, codifying the CRF-39 failure; assert status and message, add positive org-with-local-default sibling R3 Mafuuu P2, Bisky P3, Chopper P3, Kite Nit Yes
CRF-42 P3 Author fixed (Contains assertion added) coderd/exp_chats_test.go:4056 ManagementListExcludesOtherOrganizations asserts only absence; empty list passes vacuously R3 Chopper P3, Bisky Nit Yes
CRF-43 P2 Merged into review body (process observation) coderd/exp_chats_test.go:358 Commit "repair organization model migration CI" hides the behavior-test inversion behind a mechanical label, empty body R3 Mafu-san P2, Leorio Nit No
CRF-44 P3 Author contested; panel closed R4 (4/5 accept: standard schema-migration semantics, not a PR-specific contract; Meruem dissents at Nit for a doc anchor) coderd/exp_chats.go:6616 CRF-12's mixed-version deploy contract is asserted in a review reply only; no comment, doc, or release note anchors it R3 Mafu-san P3, Ryosuke Note Yes
CRF-45 P3 Author fixed (cache restored with post-cache org check) coderd/x/chatd/chatd.go:4461 resolveModelConfig swapped configCache for direct DB read on the per-turn hot path; sibling path still uses the cache R3 Killua P3, Kite P3, Takumi Note Yes
CRF-46 P3 Author fixed (helper removed; resolution moved into chatstate) coderd/x/chatd/chatd.go:1733 resolveQueuedMessageHeadModelConfigID runs CountChatQueuedMessages before GetChatQueuedMessageHead; the head query already returns ErrNoRows on empty R3 Killua P3, Ryosuke Nit Yes
CRF-47 P3 Author fixed (distinct foreign-org log lines at all consumers) coderd/x/chatd/chatd.go:264 Org-mismatch signal inconsistent across consumers: advisor logs "disabled or unavailable", personal override logs "unavailable", requireEnabledChatModelConfig logs nothing; only resolveConfiguredModelOverride names the cause R3 Leorio P3, Melody Nit, Zoro Note, Chopper Note Yes
CRF-48 P3 Author fixed (sentinel dispatched in both failure modes) coderd/x/chatd/subagent.go:221 errModelConfigOutsideOrganization early-return shadows the hard/soft failureMode dispatch; out-of-org is soft-only with nothing stating it R3 Melody Yes
CRF-49 Note Author fixed (transitions own resolution; field removed) coderd/x/chatd/chatstate/transitions.go:355 Promoting transitions consume caller-supplied PromotedMessageModelConfigID without enforcing that anyone resolved it; transition should own the resolution R3 Meruem Yes
CRF-50 Nit Author fixed (field removed) coderd/x/chatd/chatstate/transitions.go:355 PromotedMessageModelConfigID undocumented on four input structs; SendMessage's Error-only resolve gate uncommented R3 Leorio Yes
CRF-51 Nit Author fixed (comment removed) coderd/x/chatd/chatd.go:1830 enabledChatModelConfigsForOrganization doc comment restates the name and misdescribes the parameter as "the chat organization" R3 Gon P2, Leorio Nit Yes
CRF-52 P3 Author fixed (wrappers deleted; validator takes organizationID) coderd/exp_chats.go:1224 Three single-callsite DefaultOrg wrappers duplicate the response switch and spread the default-org policy across seven edit points; reviewers disagree on direction (delete and let chatd own the check vs enforce at the coderd perimeter) R3 Zoro P3, Luffy P3, Knov P3, Ryosuke Note Yes
CRF-53 Note Author fixed (rollback warning comment; migration now 000574) coderd/database/migrations/000571_chat_model_config_organization.down.sql:9 Down migration silently demotes every non-default org's default and re-up does not re-promote; warn operators in a leading comment R3 Knuckle Yes
CRF-54 Nit Author fixed coderd/x/chatd/ARCHITECTURE.md:22 "The usable local stored default" stacks three adjectives; pick the section's one phrasing R3 Leorio Yes
CRF-55 Note Author fixed (six log strings updated) coderd/x/chatd/subagent.go:341 Six unchanged slog messages say "using deployment default" where the fall-through now resolves the chat organization's local default R3 Pariston, Netero Yes
CRF-56 Note Merged into review body (PR description) coderd/x/chatd/ARCHITECTURE.md:20 PR description names the change, not the user need; also overpromises org-scoped "selection" that the create path does not deliver R3 Luffy Note, Mafu-san Nit No
CRF-57 Note Dropped by orchestrator (empirically disproved: TestQueuedPromotionResolvesOrganizationModel asserts the foreign-org swap on ExplicitPromotion and ErrorStateSendPromotion plus the NoLocalDefaultKeepsQueuedRow inversion) coderd/x/chatd/chatd.go:1706 Claimed zero direct coverage of foreign-org queued row substitution R3 Chopper No

| CRF-58 | P2 | Open (escalated: recurrence of the CRF-4 renumber-artifact class after correction) | coderd/database/migrations/testdata/fixtures/000574_chat_model_config_organization.up.sql:1 | Fixture header and seeded identifiers still say 571 after the 000574 renumber (same class as CRF-4's 552 artifact) | R4 | Mafu-san P2, Netero Nit | Yes |
| CRF-59 | Nit | Open | coderd/x/chatd/chatd.go:1752 | enabledChatModelConfigsForOrganization is a zero-value passthrough over the store method; inline or drop | R4 | Netero | Yes |
| CRF-60 | Note | Open | coderd/exp_chats.go:4397 | resolveCreateChatModelConfigID runs userCanUseChatModelConfig twice on every non-org failure of an explicit ModelConfigID | R4 | Netero | Yes |
| CRF-61 | Note | Open | coderd/x/chatd/chatd.go:1735 | "Load enabled configs, return the IsDefault row, else no-rows" duplicated across chatd (effectiveDefaultChatModelConfig) and chatstate (resolveQueuedMessageModelConfigID) | R4 | Netero | Yes |
| CRF-62 | P3 | Open | coderd/x/chatd/chatd.go:4428 | Warm-cache resolveModelConfig now pays an uncached GetAIProviderByID per turn, and the caller fetches the same provider again via resolveModelRouteForConfig; CRF-45's hot-path goal not met | R4 | Killua P3, Ryosuke P3, Knov P3, Hisoka Note | Yes |
| CRF-63 | P3 | Open | coderd/exp_chats.go:3189 | promoteChatQueuedMessage switch lacks the ErrNoDefaultChatModelConfig arm four sibling handlers gained; reachable path 500s instead of 400 | R4 | Melody | Yes |
| CRF-64 | P2 | Open | coderd/x/chatd/tasks.go:649 | normalizeTaskTransitionError classifies ErrNoDefaultChatModelConfig as retryable; FinishTurn/FinishInterruption loop forever on a durable admin-config failure; compounds with the CRF-40 residual population | R4 | Melody P3, orchestrator escalated on consequence chain | Yes |
| CRF-65 | P3 | Open | coderd/x/chatd/subagent.go:61 | errModelConfigOutsideOrganization wraps ErrNoRows undocumented, making four switches order-dependent; resolveExplicitSpawnOverrides (subagent.go:606) misses the arm entirely | R4 | Gon P3, Knov Note, Mafuuu Nit | Yes |
| CRF-66 | P3 | Open | coderd/exp_chats.go:1232 | validateUserChatModelConfigAvailable folds outside-org into "not found or disabled" while the create path returns the precise message; send/edit/override callers get the wrong diagnosis; test codifies it | R4 | Mafuuu P3, Leorio P3, Chopper P3+Nit; Kurapika notes info-hiding rationale | Yes |
| CRF-67 | P3 | Open | coderd/x/chatd/chatd.go:2828 | Foreign-org title override: manual path short-circuits with no log and a different fallback than every other unusable-override case; overrideSet rewrite is load-bearing for quickgen and dead for chatd with nothing saying so | R4 | Chopper P3, Zoro Note, Hisoka Note | Yes |
| CRF-68 | P3 | Open | coderd/x/chatd/chatstate/transitions.go:309 | Queue promotion silently substitutes a foreign-org model with the org default; no signal, log, or audit trail links the promoted row back to the requested model | R4 | Chopper | Yes |
| CRF-69 | P3 | Open | coderd/exp_chats.go:6887 | Five handlers copy the same seven-line default-org-or-500 block verbatim; two add the same three-line ownership guard | R4 | Robin P3, Zoro Nit | Yes |
| CRF-70 | Nit | Open | coderd/x/chatd/title_override.go:79 | modelOverrideConfigResolver signature omits the org, so three call sites bind it via identical closures; wrong-org capture cannot fail the compiler | R4 | Ryosuke, Robin | Yes |
| CRF-71 | Nit | Open | coderd/exp_chats.go:4538 | Post-lookup org check in defaultCreateChatModelConfigID is unreachable (SQL filter owns the guarantee); dead branch misleads | R4 | Razor, Kite | Yes |
| CRF-72 | Nit | Open | coderd/x/chatd/chatstate/transitions.go:305 | nolint justification still says "deployment model configuration" after the switch to org-scoped resolution | R4 | Mafuuu | Yes |
| CRF-73 | Nit | Open | coderd/x/chatd/chatstate/transitions.go:323 | Error wrap says "get default chat model config" but the failing call is GetEnabledChatModelConfigsByOrganization | R4 | Mafuuu, Leorio | Yes |
| CRF-74 | Nit | Open | coderd/x/chatd/chatstate/transitions.go:285 | messageFromQueuedRow lost its doc comment when it gained resolution-with-fallback and an error return; resolveQueuedMessageModelConfigID also undocumented | R4 | Leorio, Mafuuu | Yes |
| CRF-75 | Nit | Open | coderd/exp_chats.go:4453 | Empty-case comment dedented to switch column and no longer explains the fall-through mechanism | R4 | Leorio | Yes |
| CRF-76 | Note | Open | coderd/x/chatd/subagent.go:226 | Hard-failure log text now surfaces "sql: no rows in result set" storage noise via the wrapped sentinel | R4 | Leorio | Yes |
| CRF-77 | Nit | Open | coderd/x/chatd/chatd.go:1618 | requireEnabledChatModelConfig doc names only the enabled recheck, not the org rejection; validateCreateModelConfigID undocumented | R4 | Gon | Yes |
| CRF-78 | Note | Open | coderd/x/chatd/configcache.go:432 | Per-org default cache turns every model-config pubsub event into M refetches (one per active org); targeted invalidation options recorded for a follow-up issue | R4 | Killua | Yes |
| CRF-79 | Note | Open | coderd/exp_chats.go:6902 | Admin list branch fetches all orgs' configs and filters in Go; CRF-5 sibling, same deferral rationale, carry into the same follow-up | R4 | Chopper | Yes |
| CRF-80 | Nit | Open | coderd/x/chatd/subagent_internal_test.go:4919 | TestEnabledChatModelConfigsForOrganization duplicates TestGetEnabledChatModelConfigsByOrganization row-for-row through the CRF-59 passthrough | R4 | Bisky | Yes |
| CRF-81 | Nit | Open | coderd/x/chatd/chatstate/errors.go:52 | ErrNoDefaultChatModelConfig doc names one caller of four | R4 | Razor | Yes |

Law analysis

R3

  • Effective LOC: +2440 -359 (722 production, 1718 test, 271 generated; density 70.4%)
  • Head SHA: 2c7f10b
  • Verdict: Don't split
  • Enforcement: Advisory
  • Reasoning: single risk domain (chat model config org ownership); queued-message resolver (concern 4) is a required companion of the chatd cutover, not a follow-up (foreign queued IDs would fail whole turns after cutover); the only viable vertical seam (schema+handlers vs chatd) would re-defer the CRF-27 org-binding fix. Strongest split candidate is a horizontal two-step migration (nullable interim) as a CRF-12 remedy, deferred to the CRF-12 panel adjudication.

R1

  • Round: R1
  • Effective LOC: +1451 -258 (466 production, 985 test, 174 generated; density 67.9%)
  • Head SHA: 425aa7a
  • Verdict: Split
  • Enforcement: Advisory
  • Proposal: extract the PATCH/DELETE rewrite (concern 5, ~200 production lines in exp_chats.go, three user-visible API behavior changes: 412 on unconfigured/disabled provider, 404 on vanished row in DELETE, dropped compression-threshold re-validation) into its own PR landing first against the existing global lock. Keep migration/query/chatd/handler org-scoping (concerns 1-4) plus ACL columns (concern 6) together. Verified: the three behavior changes have zero dedicated tests in this diff (no PreconditionFailed, 412, 404-after-lock, or compression-threshold PATCH cases in the test diff). Law's floor if the split is declined: dedicated tests for those three paths in this PR.

Contested and acknowledged

CRF-3 (Nit, coderd/database/queries/chatmodelconfigs.sql:18) - ByOrganization suffix on modified queries

  • Finding: GetDefaultChatModelConfig and UnsetDefaultChatModelConfigs gained an organization_id filter without the ByOrganization suffix while the sibling new query uses it; rename or state why exempt.
  • Author defense (R2): The convention is not the repo majority (of 38 org-filtered queries, 24 omit the org token, e.g. GetAuditLogsOffset, GetGroups, AcquireProvisionerJob). In this file feat!: enforce organization scope for chat models #27959 uses the ByOrganization suffix for a different property (the sibling without @authorize_filter), so reusing it would give one suffix two meanings. Rename cost: 14 files, one bare string literal at dbauthz/setup_test.go:249 the compiler cannot catch, conflicts with feat: add chat model config RBAC resource #27957 and feat!: enforce organization scope for chat models #27959. If the convention matters, enforce it repo-wide with a lint rule, not a two-query rename in a schema PR.
  • Status: contested, no panel decision yet.

CRF-5 (Note, coderd/exp_chats.go:7133) - ensureDefaultChatModelConfig all-orgs scan

  • Finding: ensureDefaultChatModelConfig fetches every org's configs via GetChatModelConfigs and filters by org in Go; an org-scoped query would make the scan org-local in SQL.
  • Author accepted (R2): Correct today (small table, write path only, under the per-org advisory lock, granted by the finding itself). feat!: enforce organization scope for chat models #27959 makes exactly the proposed change (GetChatModelConfigsByOrganization) and feat: add chat model config RBAC resource #27957 rewrites the same call first (GetDefaultChatModelConfigCandidates to dodge the RBAC filter); fixing here would be reverted twice by stacked PRs.

CRF-3 panel closure (R2, unanimous)

  • Eleven panelists evaluated the defense; all accept. Representative reasoning (Chopper): a developer who greps ByOrganization misses the two queries, but the org filter is visible in every generated Go signature so the misread cannot survive into code; and feat!: enforce organization scope for chat models #27959 gives the suffix a second meaning in this file, so renaming would make the name lie. Mafu-san independently re-counted the convention claim (43 org-filtered queries, 29 without the token; same direction as the author's 38/24). Closed.

Orchestrator downgrades (R2)

  • CRF-19, CRF-20 (Gon P2 -> Nit). Keep-argument at P2: Gon's calibration treats restating comments as P2 because they erode reader trust in comments file-wide. Downgrade reason: both are isolated two-line test-helper comments with no behavioral or misleading content; consequence is seconds of reader time. The keep-argument's file-wide erosion claim does not hold for two occurrences in test files.
  • CRF-35 (Komugi Note, dropped). Keep-argument: direct SQL could still flip is_default and the cache would serve stale fallback resolution for up to 10s with no event. Drop reason: verified no query mutates is_default after insert; the 10s TTL staleness is within the cache's stated contract; Komugi's own conclusion was "nothing to change here."

CRF-7 (Note, coderd/exp_chats.go:6600) - cross-org lock independence untested

  • Finding: The per-org GenLockID switch has no test proving cross-org lock independence or lock-ID stability; the property becomes load-bearing after the cutover PRs.
  • Author defense (R3): "We removed the lock-ID and lock-independence tests because they pin implementation details. The behavior tests cover organization isolation for promotion and deletion."
  • Status: contested, no panel decision yet.

CRF-12 (Note, coderd/exp_chats.go:6600) - rolling-upgrade lock disjointness

  • Finding: Old and new replicas serialize chat model config writes on disjoint lock IDs during a rolling upgrade; old-binary inserts fail the new NOT NULL; transient and self-healing but needs a human decision.
  • Author defense (R3): "We intentionally do not support mixed-version lock compatibility. We removed the dual-lock workaround because consistent-version replicas are required for this migration."
  • Status: contested, no panel decision yet.

CRF-7 panel closure (R3, 3/4)

  • Finding: per-org GenLockID switch untested for cross-org lock independence.
  • Author defense: lock-ID tests pin implementation details; behavior tests cover org isolation.
  • Panel closure: Takumi: FNV-64 disjoint inputs are mechanically disjoint; a concurrency test would only demonstrate what the input string guarantees; regression surfaces as latency, not correctness. Pariston: the per-org unique index is the correctness backstop; the lock is a lost-update/throughput guard. Ryosuke concurs. Mafuuu dissents on the defense's accuracy (the cited tests prove data isolation, not lock independence) but agrees the consequence is throughput-only. Closed with the correction noted in the reply.

CRF-12 panel closure (R3, 4/5)

  • Finding: rolling-upgrade lock disjointness plus old-binary NOT NULL failures.
  • Author defense: mixed-version lock compatibility intentionally unsupported.
  • Panel closure: Pariston: old-binary writes fail on NOT NULL before the lock matters, identical to any NOT-NULL-adding schema PR; transient, self-healing, admin-only. Takumi and Mafuuu concur. Ryosuke accepts as a deploy contract but asks it be written down; Mafu-san finds the policy unanchored. The documentation ask is tracked as CRF-44.

Orchestrator notes (R3)

  • CRF-51 (Gon P2 -> Nit). Keep-argument: the comment misdescribes scope on a helper later stack PRs keep touching. Downgrade: the signature corrects the misread instantly, the same reasoning the panel used to close CRF-3 unanimously; consequence is reader-seconds.
  • CRF-57 (Chopper Note, dropped). Keep-argument: FinishTurn/FinishInterruption promotion paths lack direct foreign-org coverage. Drop: the central claim (zero coverage) is empirically false; the shared resolver is asserted on two of four paths with the exact swap assertion.

CRF-44 (P3, coderd/exp_chats.go:6616) - deploy contract documentation

  • Finding: The CRF-12 mixed-version deploy contract exists only in a review reply; anchor it at the lock site or in release notes.
  • Author defense (R4): "We did not add a mixed-version compatibility note. Schema-changing writes from old replicas during migration execution are not a supported contract, and this change does not introduce a special rolling-upgrade workaround."
  • Status: contested, no panel decision yet.

CRF-44 panel closure (R4, 4/5)

  • Author defense: schema-changing writes from old replicas during migration are not a supported contract; no special workaround was introduced.
  • Panel closure: Pariston: there is no deploy contract specific to this PR, only Coder's standard migration semantics; an operator researching in isolation reaches the author's conclusion. Ryosuke: a lock-site comment would repeat a general contract at a specific site. Knuckle and Mafu-san concur. Meruem dissents at Nit: docs/install/upgrade-best-practices.md never states the general contract either, so the next debugger repeats CRF-12's investigation. Closed; Meruem's doc suggestion recorded in the reply.

Orchestrator notes (R4)

  • CRF-58 escalated Nit -> P2 (Mafu-san P2 vs Netero Nit; higher wins): a repeated same-class mistake after correction (CRF-4, R1) is the process failure, not just the artifact.
  • CRF-64 escalated Melody P3 -> P2 on the consequence chain: the CRF-40 residual population (every pre-existing non-default-org chat with a queued head) multiplied by an unbounded 5s-backoff retry loop is a permanent, invisible resource drain. Verified against tasks.go:649 (no sentinel arm; sentinel does not wrap ErrNoRows) before escalating.
  • Hisoka P1 re-raise of CRF-40 vs Pariston explicit non-re-raise (design intent per ARCHITECTURE.md): adjudicated for the re-raise. Hisoka's argument is more specific (ran the codifying tests, traced all five consumer paths, proved no in-API remediation), and no-agent-accepted-permanence requires a human decision; posted as a re-raise reply on the CRF-40 thread with Pariston's counter-position recorded.
  • Chopper's PUTModelRejectsChatOrgModel Nit folded into CRF-66 (test prong of the same fix).

Round log

Round 1

Netero-only (P2 findings gate the panel). Law ran (advisory split). 2 P2, 2 Nit, 1 Note. Reviewed against 1e578a6..425aa7a.

Round 2 churn guard

PROCEED. CRF-1, CRF-2, CRF-4 author fixed (ef9a1a7). CRF-3 contested. CRF-5 acknowledged. PR restructured after Law's advisory split: now second layer of a 7-PR stack; base PR #27968 carries the PATCH/DELETE locked-read rewrite and its three behavior changes. Base is now e19560c (the #27968 branch), not main. Author corrected two round-1 report claims: the three 412 assertions pre-date this PR (exp_chats_test.go:4674/4690/4712 on main), and the dropped compression_threshold re-validation is unobservable (DB CHECK bounds 0..100). An external reviewer (codex) P2, fallback gated on len(rows) so an org with all configs disabled borrowed default-org models, was fixed in ef9a1a7 via ownership check (defaultConfig.OrganizationID == organizationID) plus two subtests.

Round 2

First full panel round: 23 reviewers (21 trigger-matched incl. always-on and Gon/Leorio floor, wildcards Luffy and Nami drawn from full pool after the distilled set was exhausted). Netero ran first (1 Nit, 2 Notes, CRF-6/7/8). Panel produced CRF-9 through CRF-36: 5 P2, 9 P3, 1 P4, 6 Nit, 8 Note posted; 1 dropped (CRF-35), 1 merged to body (CRF-36). CRF-3 closed by unanimous panel acceptance of the author defense. CRF-1/2/4 and the codex P2 fix independently verified by multiple reviewers (tests run against live Postgres). Orchestrator verified default-org immutability (organizations.sql) to settle Komugi/Meruem contradiction, and Bisky's coverage-gap claim (3 dbgen.ChatModelConfig calls in exp_chats_test.go, none in a second org). Reviewed against e19560c..ef9a1a7.

Round 3 churn guard

PROCEED. 25 addressed, 2 acknowledged (CRF-8, CRF-14), 2 contested (CRF-7, CRF-12), 0 silent. Head 2c7f10b (commits 7a713c7, 8c03537). Major restructure: fallback machinery replaced by single GetEffectiveChatModelConfigsByOrganization query; chatd org grant removed; org-binding checks added in chatd resolution (CRF-27). PR description regenerated; title now "scope chat models to organizations". New unresolved codex P1 at exp_chats.go:4535: omitted-model chat creation still resolves the deployment default org while chatd.CreateChat now requires the config to belong to req.OrganizationID. CI failing: test-go-pg, test-go-pg-17, test-go-race-pg.

Round 3

Full panel, 23 reviewers (wildcards Luffy, Melody). Netero advisory (CRF-37 P2, CRF-38 Nit); Law re-ran: Don't split (advisory). Dominant finding: the CRF-27 org-binding fix landed while the coderd resolvers still pin the deployment default org, turning the acknowledged CRF-14/CRF-8 seams into live P1 breakage (CRF-39 creation, CRF-40 pre-existing chats); 7 reviewers converged, codex P1 confirmed. CRF-7 and CRF-12 closed by panel vote; CRF-33 re-raised at root cause. New: 2 P1, 2 P2 (+CRF-37), 7 P3, 4 Nit, 3 Note posted; CRF-43/56 merged to body; CRF-57 dropped (disproved). CI failing on head (test-go-pg, test-go-pg-17, test-go-race-pg); review credentials could not read CI logs; reported in body. Event REQUEST_CHANGES. Reviewed against c0950c1..2c7f10b.

Round 4 churn guard

PROCEED. 17 addressed, 1 contested (CRF-44), 0 silent. Head ad400e1. CI green (19 passed). Migration renumbered 000571 -> 000574. Both P1s fixed per author: resolver org-threading plus foreign-LastModelConfigID fall-through to local default. Queued-head resolution moved into chatstate transitions (CRF-46/49/50 fixed structurally).

Round 4

Full panel, 22 reviewers (wildcards Zoro, Razor). CI green. Both R3 P1 fixes verified structurally (Kurapika chain-verified CRF-26/27/31; Knuckle verified every schema-domain fix; Bisky verified CRF-41/42; Komugi/Takumi/Ging-go clean). CRF-44 closed by panel (4/5). CRF-40 re-raised at P1: the fall-through fix rescues only orgs owning a local default, which no non-default org can have in this PR; zombie chats persist with clearer errors, codified by four tests; needs a human decision (seed at migration, land write-path cutover with this PR, or explicit sign-off). New: 2 P2, 7 P3, 10 Nit, 5 Note. CRF-33 remains re-raised and author-silent. Event REQUEST_CHANGES. Reviewed against 033bf39..ad400e1.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@coder-agents-review coder-agents-review Bot 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.

First-pass review only: these are mechanical findings from Netero; the full review panel has not yet reviewed this PR and will review after these findings are addressed.

The change is well built: the migration ships a dedicated up/down/re-up test with fixture, test density is 67.9%, dbauthz entries cover all three changed queries, the removed LockIDChatModelConfigWrites was the last iota entry so prior lock values are unchanged, and ARCHITECTURE.md was kept current. Findings: 2 P2, 2 Nit, 1 Note.

Because effective additions exceed 1000 lines, a decomposition analysis (Law) also ran. Verdict: split, advisory. Proposal: extract the PATCH/DELETE rewrite in exp_chats.go (~200 production lines) into its own PR that lands first against the existing global lock; it is independent of the schema change, and it carries three user-visible API behavior changes (412 on unconfigured/disabled provider, 404 on vanished row in DELETE, dropped compression-threshold re-validation) that currently have zero dedicated tests in this diff. As Law put it, three contract changes are "buried in a schema PR whose title says "schema"." The split is advisory, but if the PR stays whole, the minimum acceptable outcome is dedicated tests for those three paths in this PR; the panel will hold that line.

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/x/chatd/subagent_internal_test.go Outdated
Comment thread coderd/x/chatd/configcache.go Outdated
Comment thread coderd/database/queries/chatmodelconfigs.sql
Comment thread coderd/database/migrations/migrate_test.go
Comment thread coderd/exp_chats.go
@ethanndickson
ethanndickson force-pushed the ethan/codagt-709-model-config-org-schema branch from 425aa7a to ef9a1a7 Compare August 10, 2026 06:57
@ethanndickson
ethanndickson changed the base branch from main to ethan/codagt-709-model-config-tx-reads August 10, 2026 06:58
@ethanndickson

Copy link
Copy Markdown
Member Author

Review round 1 addressed

Head is now ef9a1a7, force-pushed. Four findings fixed, two declined with reasoning in their threads. All six threads have replies and are resolved.

Structural change since the last review: the review flagged this PR as a decomposition candidate, and I agreed. It is now the second layer of a 7 PR stack (#27969) rather than the first of 6. A new base PR #27968 carries the locked-read fix for the PATCH and DELETE handlers, which was tangled into this PR before. The three handler behaviour changes previously described here (the 412 for an unconfigured or disabled provider, the DELETE 404, and the dropped compression_threshold re-validation) all moved down into #27968. Please review this diff against #27968, not against main.

Fixed

ID Finding Fix
C1 (P2) enabledChatModelConfigsWithDefaultOrgFallback gated on len(rows) > 0, so an org whose configs were all disabled borrowed the default org's models Ownership now decided by defaultConfig.OrganizationID == organizationID. Two new subtests cover the disabled-config and disabled-provider cases
CRF-1 (P2) TestListSubagentModels_NonDefaultOrgSeesDefaultOrgConfigs asserted the opposite of its name Renamed to NonDefaultOrgListIsOrgLocal
CRF-2 (P2) Default-org fallback logic duplicated in DefaultModelConfig Three copies consolidated into defaultChatModelConfigForOrg
CRF-4 (Nit) Migration 000565 test and fixture identifiers suffixed 552 16 occurrences renamed to 565

Declined

Full reasoning is in each thread.

Two corrections to the round 1 report

  • The three 412 assertions cited as new tests pre-date this PR. They are at coderd/exp_chats_test.go:4674, :4690, and :4712 on main.
  • The dropped compression_threshold re-validation is unobservable. The DB CHECK constraint bounds the column to 0..100, so the removed check could only reject requests that did not touch the field.

Generated by mux

@ethanndickson

Copy link
Copy Markdown
Member Author

@codex review

@ethanndickson

Copy link
Copy Markdown
Member Author

/coder-agents-review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ef9a1a7537

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread coderd/exp_chats.go

@coder-agents-review coder-agents-review Bot 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.

First full panel round: 23 reviewers across database, Go, concurrency, auth, and testing domains, following round 1's Netero-only first pass.

This PR holds up well under that much scrutiny. The migration earned repeated independent praise: correct expand ordering (nullable, backfill, SET NOT NULL), a down migration that demotes foreign defaults before re-creating the global index, and a test that round-trips up/down/up including the soft-deleted row, both unique-index rejections, and the ACL CHECK. Multiple reviewers verified ARCHITECTURE.md claim-by-claim against the code with no divergence, and the codex round-1 fallback fix was independently re-verified by nine reviewers running the new subtests against live Postgres. Round 1 fixes (CRF-1, CRF-2, CRF-4) all verified.

Findings: 5 P2, 9 P3, 1 P4, 6 Nit, 8 Note. No P0/P1. The recurring theme is the ownership invariant ("an org that owns configs owns a default"): it is load-bearing for the fallback (codex fix), maintained only by write-path discipline in another package, undocumented at its maintainer (CRF-18), untested for cross-org isolation (CRF-9), and inferable rather than structural (CRF-34). The M2 cutover migration is a write path that can bypass it entirely; several findings converge on making it either documented and pinned, or structural via an existence check.

CRF-3 (ByOrganization suffix) is closed: the panel unanimously accepts the author's defense, on the evidence that the convention is a repo minority (independently re-counted) and #27959 gives the suffix a conflicting meaning in the same file.

One flagged disagreement: on the chatd subject's site-wide org read (CRF-26), Kurapika argues for a purpose-built narrow subject while six reviewers deem the grant acceptable as the narrowest static option; both positions are in the comment.

Process observations: restructuring the stack to move the locked-read rewrite into #27968 was the right call and made this diff reviewable as one concern. One process miss (Mafu-san): the round-1 CRF-2 fix consolidated the duplicated fallback in configcache.go but did not generalize to the sibling duplicated default-org block in exp_chats.go (both copies existed at the round-1 head); a grep for GetDefaultOrganization(dbauthz.AsChatd at fix time would have caught both. That code change is tracked as CRF-6. Schema side effects were enumerated: the new ON DELETE CASCADE on organization_id is a dead path today since organizations are only ever soft-deleted, and the backfill cannot fail NOT NULL because migration 000198 guarantees the default org.

As Robin put it about the duplicated test-helper defaulting: "If the two copies drift, one of them will be quietly wrong with no one to mourn it."


coderd/x/chatd/chatd.go:1605

P4 [CRF-27] Explicit model-config IDs are validated for existence and enabled-ness, never for organization binding. (Kurapika P4, Kite Note)

Kurapika: resolveFallbackModelConfigID (preserved-ID branch), requireEnabledChatModelConfig (chatd.go:1560), and validateCreateModelConfigID (chatd.go:1580) all accept any config UUID ... Nothing checks config.OrganizationID == chat.OrganizationID. Today this is unexploitable: the only producers put every config in the default org ... but the moment the cutover PR copies configs into every org, a user in org B who learns a config UUID from org A can pin their chat to it and burn org A's provider credentials.

The enforcement must land no later than the cutover migration; the API/RBAC PRs (#27957, #27959) should be verified to add this binding check. Recorded here because this PR creates the data model in which configs become org property.

🤖

coderd/x/chatd/configcache.go:54

P3 [CRF-31] cloneModelConfig claims to isolate cached state but does not clone the two map fields this PR adds to ChatModelConfig. (Razor, Nami)

Razor: ChatModelConfig now carries GroupACL and UserACL of type ChatACL = map[string]ChatACLEntry (models.go:5155), and cloneModelConfig copies the struct shallowly, so the cache entry and every caller alias the same maps (and the Permissions slices inside them). Today nothing in chatd reads or writes these fields ... The RBAC PR later in the stack makes them load-bearing: any in-place mutation of an ACL on a config obtained from the cache would corrupt the shared cache entry, and a concurrent mutation racing a reader is a map-access panic.

The fix belongs here because this PR introduces both the fields and the aliasing. Note (Nami): ChatACLEntry carries a Permissions slice, so a shallow maps.Clone still shares the slices; copy those too or document that entries are immutable.

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/exp_chats.go Outdated
Comment thread coderd/exp_chats.go
Comment thread coderd/x/chatd/chatd.go Outdated
Comment thread coderd/exp_chats.go
Comment thread coderd/exp_chats.go
Comment thread coderd/x/chatd/subagent.go Outdated
Comment thread coderd/x/chatd/subagent.go Outdated
Comment thread coderd/x/chatd/chatd.go Outdated
Comment thread coderd/database/dbgen/dbgen.go
Comment thread coderd/x/chatd/subagent.go Outdated
@ethanndickson

Copy link
Copy Markdown
Member Author

Follow-up from the automated review of stacked PR #27957: the chatd subject comment in coderd/database/dbauthz/dbauthz.go says (removed in M3), which depends on stack plan shorthand rather than a durable repository referent. Please replace that phrase with a ticketed TODO such as TODO(mafredri): remove after CODAGT-709 M3 (org-scoping cutover).

Generated by mux on behalf of @ethanndickson.

@ethanndickson
ethanndickson force-pushed the ethan/codagt-709-model-config-org-schema branch from 46e2aaa to 38f5109 Compare August 11, 2026 00:14
@ethanndickson
ethanndickson force-pushed the ethan/codagt-709-model-config-org-schema branch 2 times, most recently from ddd0964 to c3e41d2 Compare August 17, 2026 13:25
@ethanndickson ethanndickson changed the title feat(coderd): org-scope chat model config schema feat(coderd): scope chat models to organizations Aug 18, 2026
@ethanndickson
ethanndickson force-pushed the ethan/codagt-709-model-config-org-schema branch from 0615d01 to 2c7f10b Compare August 18, 2026 08:17

Copy link
Copy Markdown
Member Author

@codex review

Copy link
Copy Markdown
Member Author

/coder-agents-review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2c7f10b3a8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread coderd/exp_chats.go Outdated
@ethanndickson ethanndickson changed the title feat(coderd): scope chat models to organizations feat: scope chat models to organizations Aug 18, 2026

@coder-agents-review coder-agents-review Bot 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.

Round 3, full panel (23 reviewers). The response to round 2 was excellent in volume and mostly in substance: 25 of 29 findings addressed with commit-linked evidence, and the structural rewrites are real improvements. Multiple reviewers verified the fixes: the single effective-configs query replaced the three-query fallback (CRF-15/29/30/32/34), the chatd subject's site-wide org grant is gone (CRF-16/26), cloneChatACL deep-clones including Permissions slices (CRF-31, now pinned by a mutation test), the org-binding checks landed everywhere (CRF-27, verified across six resolution sites), and LockIDChatModelConfigWrites is reserved again (CRF-11). Komugi, Kurapika, and Ging-go filed clean reports after real verification, including a race-clean run of the queued-promotion ordering test.

But this round the panel found the PR's first release-blocking defects. Findings: 2 P1, 2 P2, 7 P3, 4 Nit, 3 Note. REQUEST_CHANGES on the P1s.

The P1 story in one paragraph: the CRF-27 org-binding fix landed in this PR while every coderd resolver still pins the deployment default org. The seams that CRF-8 and CRF-14 acknowledged as "later API cutover" stopped being latent the moment chatd started enforcing config.OrganizationID == chat.OrganizationID. Seven panel reviewers plus codex converged: non-default-org chat creation now fails unconditionally on every entry path (CRF-39), and, worse, every pre-existing chat in a non-default org is bricked at runtime, sends, queue promotions, turn finishes, and edits all error (CRF-40). There is no in-API remediation because createChatModelConfig hardcodes the default org. As Hisoka put it: "the trap sprung on every chat that walked through the door before it was set."

CI is failing on this head (test-go-pg, test-go-pg-17, test-go-race-pg). The review environment's GitHub credentials could not read the CI logs, so the panel could not diagnose whether these failures share a root cause with the P1s; reviewers' targeted local runs of the new tests all pass. Failing checks are bugs: please fix them or state the cause.

Contested findings adjudicated: CRF-7 closed (3/4 accept; the lock property is throughput, not correctness, though the defense's claim that the behavior tests cover lock independence is inaccurate; they prove data isolation). CRF-12 closed (4/5 accept; the mixed-version stance is a legitimate deploy contract), with the one actionable residue filed as CRF-44: the contract exists only in a review reply, so write it down. CRF-33's fix converged three ACL literal copies without giving them an owner; re-raised on its thread at root cause.

Process observations. [CRF-43] The head commit 2c7f10b3 titled "repair organization model migration CI" bundles a genuine migration renumber with a load-bearing test inversion (SuccessNonDefaultOrgUsesDeploymentDefault became NonDefaultOrgDoesNotUseDeploymentDefault, assertion flipped, LastModelConfigID claim deleted) behind a mechanical label and an empty body; that inversion is exactly where the P1 hides, and a log reader scanning subjects will skip it. Split semantic test flips into their own honestly-titled commits. [CRF-56] The PR description says the change "scopes ... Chatd model selection ... to each chat organization" while the CRF-14 acknowledgment says creation stays deployment-default; one story, please. It also never says who needs per-org catalogs; a middle PR in a 7-PR stack that ships zero net user-visible behavior (and currently a regression) should carry its own justification.

A human decision is needed on CRF-39/CRF-40's direction: resolve per-org at creation now, or hold the chatd strict binding until the API cutover. Both are viable; shipping neither is not.


coderd/x/chatd/subagent.go:341

Note [CRF-55] Six unchanged slog messages in resolvePersonalSubagentModelConfigID (lines 341, 367, 393, 400, 408, 426) say "using deployment default" where the fall-through now resolves the chat organization's local default. (Pariston, Netero)

Pariston: the strings say the wrong thing about which default the caller will land on, which is the same premise gap the creation-path P1 catches on a different path (author's mental model still says "deployment default"). One fix line: replace "deployment default" with "chat organization default" across the six sites.

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/exp_chats.go Outdated
Comment thread coderd/x/chatd/chatd.go
Comment thread coderd/exp_chats_test.go Outdated
Comment thread coderd/exp_chats_test.go
Comment thread coderd/exp_chats.go
Comment thread coderd/exp_chats.go Outdated
Comment thread coderd/x/chatd/ARCHITECTURE.md Outdated
Comment thread coderd/database/queries/chatmodelconfigs.sql
Comment thread coderd/x/chatd/configcache.go Outdated
@ethanndickson
ethanndickson force-pushed the ethan/codagt-709-model-config-org-schema branch from 4e42463 to 82110a6 Compare August 19, 2026 16:55

Copy link
Copy Markdown
Member Author

$@codex review\n\nThis review request was generated by Coder Agents.

Copy link
Copy Markdown
Member Author

/coder-agents-review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ad400e18d9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread coderd/x/chatd/ARCHITECTURE.md Outdated
Comment thread coderd/x/chatd/ARCHITECTURE.md Outdated

@coder-agents-review coder-agents-review Bot 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.

Round 4, full panel (22 reviewers). The fix quality this round is high and the panel verified it hands-on: Kurapika chain-verified the org-binding enforcement end to end (CRF-26/27/31 all hold, including the new dbauthz test asserting the chatd subject is denied org read), Knuckle verified every schema-domain fix at head, Bisky confirmed the test-quality fixes (exact SDK message, positive siblings) and called the queued-promotion suite "real gems," and Komugi, Takumi, Ging-go, and Pariston filed clean reports after real verification. Moving the queued-head resolution inside the chatstate transaction closed CRF-46/49/50 at the root. CI is green.

Findings: 2 P2, 7 P3, 10 Nit, 5 Note, plus one P1 re-raise. REQUEST_CHANGES on the re-raise.

The re-raise (on the CRF-40 thread): the fall-through fix rescues chats only when the chat's org owns a local default, and after this migration no non-default organization can own one; the API writes configs exclusively to the default org. Every pre-existing chat in a non-default org still dies on send, promotion, and turn finish, now with a clearer error, and four new tests codify that state as intended. As Hisoka put it: "The R3 fix reads as though it rescued the described customer. It rescued the tests. The customer is untouched." The panel is not unanimous: Pariston reads ARCHITECTURE.md's "the default organization does not provide runtime fallback models" as a deliberate design with the write-path cutover legitimately deferred to its stack PR. Both positions are in the thread. What the panel agrees on: this cannot be closed by an agent's acknowledgment. A human must pick one: seed each org's default at migration time, land the org-scoped write path with this PR, gate the strict enforcement until the cutover, or explicitly sign off (with a ticket) on multi-org deployments being degraded between this merge and the cutover PR. The new CRF-64 raises the stakes: the same population triggers unbounded task-runner retry loops, not just clean 400s.

Contested finding adjudicated: CRF-44 closed (4/5 accept; standard schema-migration semantics, not a PR-specific contract; Meruem's dissent that the general contract is documented nowhere is recorded in the thread).

Still open with no author response: the CRF-33 re-raise from round 3 (three inline copies of the everyone-in-org ACL with no shared owner). Two rounds without a response; fix it, file a ticket, or explain why not.

Process observations (Mafu-san, Leorio): the 000571 -> 000574 renumber repeated the exact CRF-4 class from round 1, updating identifiers in migrate_test.go while leaving nine 571 artifacts in the fixture it loads; that recurrence after correction is why CRF-58 is P2. Eight of nine round-4 commits have empty bodies, including the two structural fixes for the R3 P1s, and two commits share the identical subject "fix(coderd): enforce organization-local chat models"; the CRF-43 signal from round 3 was applied to the flagged commit and not retained as a practice. Commit bodies naming the finding being addressed would have made this round's churn audit mechanical.


coderd/x/chatd/tasks.go:649

P2 [CRF-64] normalizeTaskTransitionError does not classify chatstate.ErrNoDefaultChatModelConfig, so async task loops retry a durable admin-configuration failure forever. (Melody P3; orchestrator escalated to P2 on the consequence chain)

Melody: chatstate.ErrNoDefaultChatModelConfig falls into the else arm, and runTaskWithRetry loops on taskRetryableError with defaultTaskRetryMaxBackoff = 5 * time.Second until the context is canceled. When an org's local default is gone and a chat has a queued row, FinishTurn/FinishInterruption produce an admin-durable error.

Orchestrator verified: the sentinel is xerrors.New (does not wrap ErrNoRows), so no existing arm catches it; it is retryable today. The escalation: combined with the CRF-40 residual (every pre-existing non-default-org chat has no usable local default after this migration), each such chat with a queued head becomes a permanent 5s-backoff retry loop across FinishTurn and FinishInterruption. That is invisible resource drain at exactly the population the P1 re-raise names. Classify the sentinel as non-retryable (the ErrTransitionNotAllowed/ErrInvalidState arm is the right shape).

🤖

coderd/exp_chats.go:3189

P3 [CRF-63] promoteChatQueuedMessage's error switch is missing the chatd.ErrNoDefaultChatModelConfig arm that four sibling handlers gained this round. (Melody)

Consumer side, four other handlers were updated to pair with the new sentinel: postChatMessages (2883), patchChatMessage (3048), regenerateChatTitle (3609), and proposeChatTitle (3658) each map it to 400 "No default chat model config is configured." ... transitions_test.go:172 asserts PromoteQueuedMessage returns exactly this sentinel from the "missing local default" path, so the case is reachable from this endpoint.

Orchestrator verified the switch at head: ErrChatArchived, ErrQueuedMessageNotFound, ErrChatNotFound, invalid-state, ErrTransitionNotAllowed, default. The reachable no-default path 500s here and 400s everywhere else. Add the arm.

🤖

🤖 This review was automatically generated with Coder Agents.

@@ -0,0 +1,91 @@
-- Fixture for 000571 (org-scope chat model configs). Fixtures apply at

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.

P2 [CRF-58] The 000571 -> 000574 renumber updated identifiers in migrate_test.go and left the fixture it loads untouched: the header and nine seeded identifiers (fixture-openai-571, Fixture Default 571, 571c000{1,2,3}-... UUIDs) still say 571. (Mafu-san P2, Netero Nit)

Mafu-san: CRF-4 (R1, "Migration 000565 test/fixture identifiers suffixed 552") corrected exactly this pattern in Round 1. Not searching for siblings during a renumber recurred in Round 4, and the fix commit that renumbers eight identifiers in one file demonstrates that the pattern was in the author's hands at that moment.

P2 because it is a repeated same-class mistake after correction, not because of the artifact's own weight. Fix: sed -i 's/571/574/g' on the fixture (Netero verified all nine matches are renumber artifacts) and grep the tree for remaining 571 before landing.

🤖

Comment thread coderd/x/chatd/chatd.go
}
if !xerrors.Is(err, sql.ErrNoRows) {
if modelConfig.Enabled && modelConfig.OrganizationID == chat.OrganizationID && modelConfig.AIProviderID.Valid {
provider, providerErr := p.db.GetAIProviderByID(

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.

P3 [CRF-62] The warm-cache path of resolveModelConfig now pays an uncached GetAIProviderByID every turn, and the caller immediately fetches the same provider again; CRF-45's hot-path goal is not met. (Killua P3, Ryosuke P3, Knov P3, Hisoka Note)

Knov: The fix restored p.configCache.ModelConfigByID, but immediately below it added p.db.GetAIProviderByID(...) to test provider.Enabled. The only caller in production is resolveChatModel, which on success does resolveModelRouteForConfig -> gatewayProviderForConfig -> enabledAIProviderByID -> p.db.GetAIProviderByID on the same provider ID a handful of lines later. Pre-diff, resolveChatModel paid one provider read per turn; post-diff it pays two.

Four reviewers converged with the same two remedies: read provider enabled-state from configCache.EnabledProviders(ctx) (already cached under the same TTL and invalidation events), or return the provider from resolveModelConfig and thread it into resolveModelRouteForConfig so the read happens once. Ryosuke's framing: one decision ("is this config still usable") made twice at two layers; pick one owner.

🤖

var errInvalidModelOverrideMetadata = xerrors.New("invalid model override metadata")
var (
errInvalidModelOverrideMetadata = xerrors.New("invalid model override metadata")
errModelConfigOutsideOrganization = xerrors.Errorf("%w: model config belongs to another organization", sql.ErrNoRows)

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.

P3 [CRF-65] errModelConfigOutsideOrganization silently wraps sql.ErrNoRows, making four switch sites order-dependent with nothing stating the coupling, and one site misses the arm entirely. (Gon P3, Knov Note, Mafuuu Nit)

Gon: subagent.go:249 (soft mode): the errModelConfigOutsideOrganization case must come before the sql.ErrNoRows case. Reorder the switch and the outside-org log message disappears; chatd.go:2828 and title_override.go:87 have the same ordering dependency. TestResolveConfiguredModelOverride_OrganizationMismatch.Soft only exercises the current ordering, so a reorder passes tests and silently loses the log.

Mafuuu: resolveExplicitSpawnOverrides (subagent.go:606) checks sql.ErrNoRows without a preceding sentinel case, so the subagent tool returns "not found or is disabled" for a model that is neither.

Fix per Gon/Knov: either a one-line comment at the sentinel definition stating the wrap and the check-me-first rule, or drop the wrap and make each site chain both checks explicitly; and add the missing arm at subagent.go:606 either way.

🤖

Comment thread coderd/exp_chats.go
case chatModelConfigAvailable:
return modelConfig, 0, nil
case chatModelConfigUnavailableModelNotFoundOrDisabled:
case chatModelConfigUnavailableModelNotFoundOrDisabled,

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.

P3 [CRF-66] validateUserChatModelConfigAvailable folds chatModelConfigUnavailableOutsideOrganization into the "model config not found or disabled" 400 while the create path returns the precise diagnosis, so the same condition gets two different answers by endpoint. (Mafuuu P3, Leorio P3, Chopper P3)

Leorio: The user posts to a chat with a model_config_id that lives in a sibling org. Create-chat says "does not belong". Send-message and edit-message say "not found or disabled". They then load the model list, see the model there, and cannot reconcile the two answers.

Affects postChatMessages, patchChatMessage, and putUserChatPersonalModelOverride. Kurapika's counter (folding hides foreign-org existence by design) is undercut by the create path already disclosing the precise message at exp_chats.go:4405; pick one policy. If the precise message wins, PUTModelRejectsChatOrgModel (exp_chats_test.go:15259) currently asserts the wrong substring and must move with it (Chopper).

🤖

Comment thread coderd/x/chatd/chatd.go
modelOpts,
)
if overrideErr != nil {
if errors.Is(overrideErr, errModelConfigOutsideOrganization) {

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.

P3 [CRF-67] The foreign-org title-override path diverges from every other unusable-override case three ways, silently. (Chopper P3, Zoro Note, Hisoka Note)

  1. Chopper: resolveManualTitleModel catches the sentinel and returns with no log, while the async path Debug-logs the same class; an admin asking "why didn't my override run on Regenerate Title?" gets nothing.
  2. Zoro: the shortcut returns the org default via resolveDefaultManualTitleModel, while other unusable-override cases fall through to the preferred-short-text scan; user-visible divergence whenever the org default is not the preferred short-text model, codified by the test without commentary.
  3. Hisoka: title_override.go:88's silent overrideSet=false rewrite is load-bearing for quickgen.go:301 and dead code for chatd.go:2828; a caller reorder flips semantics silently.

Either unify the foreign-org case with the other unusable-override fall-through (deleting the one-caller resolveDefaultManualTitleModel), or keep the shortcut and state the rationale at all three sites. A Debug log on the manual path is the floor.

🤖

})
}

func TestEnabledChatModelConfigsForOrganization(t *testing.T) {

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.

Nit [CRF-80] TestEnabledChatModelConfigsForOrganization is TestGetEnabledChatModelConfigsByOrganization (querier_test.go) rebuilt in a second file: same 3-org fixture, same four configs, same four assertions, exercised through the one-line passthrough CRF-59 flags. (Bisky)

Delete the subagent test; the querier test already proves the query row-for-row. Two settings, one stone.

If CRF-59's inline-the-wrapper lands, this test loses its subject entirely.

🤖

// wraps this sentinel.
ErrMessageQueueFull = xerrors.New("chat message queue is full")

// ErrNoDefaultChatModelConfig is returned when queued-message

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.

Nit [CRF-81] The doc on ErrNoDefaultChatModelConfig says it is returned by queued-message promotion, which is one of its four producers. (Razor)

The sentinel is also returned by resolveFallbackModelConfigID (chatd.go:1696), resolveDefaultManualTitleModel (chatd.go:2895), and resolveModelConfig (chatd.go:4454).

Align with the generic phrasing the chatd alias already carries.

🤖

Comment thread coderd/x/chatd/chatd.go
return database.ChatModelConfig{}, sql.ErrNoRows
}

func enabledChatModelConfigsForOrganization(

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.

Nit [CRF-59] enabledChatModelConfigsForOrganization is a zero-value passthrough over store.GetEnabledChatModelConfigsByOrganization: no logging, no error context, no cache, no policy. (Netero)

Every call site passes exactly ctx, store, organizationID. Inline it or drop it; CRF-80 (the duplicated test built on it) goes with it.

🤖

Comment thread coderd/exp_chats.go
}
}
if _, status, resp := api.validateUserChatModelConfigAvailable(ctx, userID, *req.ModelConfigID); resp != nil {
_, reason, err := api.userCanUseChatModelConfig(ctx, userID, req.OrganizationID, *req.ModelConfigID)

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.

Note [CRF-60] resolveCreateChatModelConfigID runs userCanUseChatModelConfig twice for every non-org failure of an explicit req.ModelConfigID: the inline call computes reason, then validateUserChatModelConfigAvailable re-invokes it with identical arguments to map the same reason to a response. (Netero)

Extract the reason -> (status, response) switch and pass the already-computed reason in. Interacts with CRF-66: that fix edits the same switch.

🤖

Comment thread coderd/x/chatd/chatd.go
return nil
}

func effectiveDefaultChatModelConfig(

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.

Note [CRF-61] The load-bearing definition of "the org's usable default" (load GetEnabledChatModelConfigsByOrganization, return the IsDefault row, else no-rows) now exists twice: effectiveDefaultChatModelConfig here and chatstate.Tx.resolveQueuedMessageModelConfigID (transitions.go:319). (Netero)

Package boundary (chatstate cannot import chatd) makes a direct shared helper awkward, but the pattern is now the load-bearing definition ... call it out before the third copy shows up.

A helper in a package both can import (database or chatstate) retires the copy.

🤖

@ethanndickson ethanndickson changed the title feat: scope chat models to organizations feat: scope chat model configs to organizations Aug 20, 2026
@ethanndickson
ethanndickson force-pushed the ethan/codagt-709-model-config-org-schema branch from ad400e1 to 419208d Compare August 20, 2026 12:03
@ethanndickson
ethanndickson force-pushed the ethan/codagt-709-model-config-org-schema branch from 419208d to b346208 Compare August 21, 2026 18:24
@ethanndickson
ethanndickson force-pushed the ethan/codagt-709-model-config-org-schema branch from 4453eb1 to 87c91ab Compare August 23, 2026 10:39

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 87c91abb2f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

func TestMigration000574MCPServerConfigsOrganizationID(t *testing.T) {
func TestMigration000580ChatModelConfigOrganization(t *testing.T) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restore the migration 574 and 576 regression tests

Keep this new migration 580 test in addition to the existing TestMigration000574MCPServerConfigsOrganizationID and TestMigration000576MCPServerConfigACL tests rather than replacing them. This hunk removes both prior tests, so the suite no longer verifies migration 574's organization backfill and rollback cleanup or migration 576's ACL backfill and rollback behavior; regressions in those migration paths can now reach upgrade or rollback testing undetected.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 87c91abb2f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ethanndickson ethanndickson changed the title feat: scope chat model configs to organizations feat!: scope chat model configs to organizations Aug 24, 2026
@github-actions github-actions Bot added the release/breaking This label is applied to PRs to detect breaking changes as part of the release process label Aug 24, 2026
@ethanndickson
ethanndickson force-pushed the ethan/codagt-709-model-config-org-schema branch from 87c91ab to 18c6eff Compare August 24, 2026 09:04
@ethanndickson
ethanndickson merged commit f32592e into main Aug 24, 2026
28 checks passed
@ethanndickson
ethanndickson deleted the ethan/codagt-709-model-config-org-schema branch August 24, 2026 09:30
nothingtosurprise pushed a commit to nothingtosurprise/coder that referenced this pull request Aug 24, 2026
…#27956)

Include the owning organization ID for each chat model configuration in
telemetry snapshots.

Depends on coder#27955

_This pull request description was generated by Coder Agents._
aslilac pushed a commit that referenced this pull request Aug 24, 2026
## Summary

Scope chat model configurations and runtime model resolution to
organizations. Each model configuration now belongs to exactly one
organization, each organization can have its own default model, and
Chatd only uses models that belong to the chat's organization.

Depends on #27968.

## Problem

Chat model configurations are currently deployment-wide even though
chats belong to organizations. A default model, explicitly selected
model, or configured model override can therefore reference the same
model configuration from every organization. This prevents model
catalogs and defaults from being isolated by organization and allows a
chat to retain or select a model that its organization does not own.

Existing model configurations also have no organization ownership or
organization-scoped ACL data, and the database enforces a single live
default across the entire deployment.

## Fix

Add required organization ownership and ACL storage to chat model
configurations. The migration assigns every existing configuration to
the default organization, seeds organization-wide read access, and
replaces the deployment-wide default constraint with a per-organization
constraint.

Scope Chatd lookup, admission, fallback, and queue-promotion behavior to
the chat's organization:

- New chats use the usable default model from their organization.
- Explicit model selections from another organization are rejected.
- Deployment or personal overrides that point to a foreign model are not
used for the chat.
- Historical cross-organization model references remain unchanged in the
database, but runtime generation falls back to a usable local default.
- Queue promotion resolves unavailable, disabled, missing, or foreign
stored models to the usable local default and fails when no usable local
default exists.

The existing model configuration management and availability routes
remain compatibility routes for the default organization in this layer.
A later layer in the stack adds organization-scoped management routes
and updates clients to use them.

## Breaking change

This changes chat model behavior from deployment-wide to
organization-scoped. Existing configurations are migrated only to the
default organization; they are not copied into every organization.

As a result:

- A non-default organization no longer inherits the deployment's
existing default model.
- Creating a chat in an organization without a usable local default
fails with `400: No default chat model config is configured.`
- Explicitly selecting a configuration owned by another organization
fails with `400: Model config does not belong to the specified
organization.`
- Existing chats whose stored model belongs to another organization can
generate only if a usable local default is available; otherwise
generation fails.

Deployments using Agents in non-default organizations must configure a
usable default chat model separately for each organization after the
organization-scoped management layers in this stack are available.

_This pull request description was generated by Coder Agents._
aslilac pushed a commit that referenced this pull request Aug 24, 2026
Include the owning organization ID for each chat model configuration in
telemetry snapshots.

Depends on #27955

_This pull request description was generated by Coder Agents._
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release/breaking This label is applied to PRs to detect breaking changes as part of the release process

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants