Skip to content

feat(coderd): org-scope chat model config schema - #27955

Open
ethanndickson wants to merge 1 commit into
ethan/codagt-709-model-config-tx-readsfrom
ethan/codagt-709-model-config-org-schema
Open

feat(coderd): org-scope chat model config schema#27955
ethanndickson wants to merge 1 commit 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

Part of CODAGT-709. Second of a seven PR stack scoping chat model configs to organisations for Coder Agents GA.

This adds the schema and the Go plumbing. Migration 000565 gives chat_model_configs a cascading organization_id FK (backfilled to the default org), swaps the global single-default unique index for a per-organisation one, and adds group_acl/user_acl JSONB columns, seeding an everyone-in-org read entry so members keep read access once the RBAC PR later in the stack lands. Default and enabled model lookups now take an organisation, chatd caches configs per organisation, and model-config writes serialise on a per-org advisory lock (GenLockID) instead of the deployment-wide constant.

Chatd keeps a one-hop fallback to the default organisation's configs until the data cutover in the next PR copies configs into every org; the API PR removes the fallback entirely.

The handler behaviour changes this PR used to carry (the 412 for an unconfigured or disabled provider, the DELETE 404, and the dropped compression_threshold re-validation) now live in #27968 at the base of the stack.

@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-10 07:08 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

deep-review v0.9.0 | Round 2 | e19560c..ef9a1a7

Last posted: Round 2, 34 findings (7 P2, 9 P3, 1 P4, 8 Nit, 9 Note), COMMENT. 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 Open 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 Open 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 Open 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 Open 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 Open 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 Open 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 Open 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 Open 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 Open 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 Open 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 Open 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 Open 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 Open 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 Open (downgraded from Gon P2) coderd/x/chatd/configcache_internal_test.go:88 Doc comment on totalDefaultModelConfigCalls restates the name R2 Gon Yes
CRF-20 Nit Open (downgraded from Gon P2) coderd/database/migrations/migrate_test.go:2948 Doc comment on mustJSON restates the name and body R2 Gon Yes
CRF-21 Nit Open coderd/x/chatd/chatd.go:1683 defaultChatModelConfigForOrg name hides the fallback its sibling's name announces R2 Gon Yes
CRF-22 P3 Open 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 Open 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 Open 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 Open 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 Open 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 Open 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 Open 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 Open 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 Open 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 Open 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 Open 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 Open 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 Open 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

Law analysis

  • 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: org-scope the chat model config API #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: org-scope the chat model config API #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: org-scope the chat model config API #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: org-scope the chat model config API #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."

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.

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
Stage 1 of CODAGT-709 adds organization ownership and ACL storage to chat model configs. It scopes the live-default invariant, runtime model resolution, and cache entries by organization while retaining the temporary default-organization fallback.

Model-config writes use organization-derived advisory locks. Update and delete paths re-read mutable state after they acquire the lock. Rollback preserves all model configs and retains only the default organization's live default before restoring the global index.

Refs CODAGT-709

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
@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
) error {
return api.Database.InTx(func(tx database.Store) error {
if err := tx.AcquireLock(ctx, database.LockIDChatModelConfigWrites); err != nil {
lockID := database.GenLockID("chat_model_config_writes:" + organizationID.String())

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 Preserve the legacy advisory lock during upgrades

During a rolling upgrade, older replicas still acquire LockIDChatModelConfigWrites, while upgraded replicas acquire this unrelated organization hash for the same default-organization writes. The two versions can therefore execute the read/decide/write transaction concurrently; for example, concurrent first-config creates can both decide to self-promote, causing one request to fail on the unique index instead of being serialized. Keep the legacy ID reserved and coordinate default-org writes with it while mixed-version replicas can coexist.

Useful? React with 👍 / 👎.

@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
// authorized the operation, and a custom role holding only
// deployment-config permissions cannot read the organization object.
//nolint:gocritic // Internal default-org resolution, scoped to this call.
defaultOrg, err := api.Database.GetDefaultOrganization(dbauthz.AsChatd(ctx))

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-6] The default-org resolution block (AsChatd GetDefaultOrganization call, the "internal resolution step, not user-readable data" justification comment, and the 500 error response) is duplicated between createChatModelConfig (6698-6712) and defaultCreateChatModelConfigID (4483-4496). (Netero, Gon)

The two blocks carry the same rationale comment and the same //nolint:gocritic escape. When the M3 cutover removes this pre-cutover resolution, two sites must be found and deleted instead of one. Extract a small (api *API) defaultOrgForChatModelConfigs(ctx) helper carrying the comment once.

Gon adds that the same rationale ("cannot read the default organization object") appears five times across production and test files in this PR; when this duplication is resolved, the rationale should live once on the helper and call sites can drop to a reference.

🤖

Comment thread coderd/exp_chats.go
) error {
return api.Database.InTx(func(tx database.Store) error {
if err := tx.AcquireLock(ctx, database.LockIDChatModelConfigWrites); err != nil {
lockID := database.GenLockID("chat_model_config_writes:" + organizationID.String())

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-7] The switch from the deployment-wide LockIDChatModelConfigWrites to a per-org GenLockID has no test proving cross-org lock independence or lock-ID stability. (Netero)

Unverifiable cheaply and unobservable in this PR: every write path still resolves to the default org, so all writes contend on one lock exactly as before. The per-org property only becomes load-bearing after the cutover PRs add org-scoped routes; a contention test would belong there. Recording so the stack does not ship the property untested.

🤖

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

configs, err := store.GetEnabledChatModelConfigs(ctx)
configs, err := store.GetEnabledChatModelConfigsByOrganization(ctx, chat.OrganizationID)

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-8] Chat-time model resolution is now org-scoped with the default-org fallback, while getUserChatProviderAvailability (exp_chats.go:1010) still lists via the deployment-wide GetEnabledChatModelConfigs. (Netero)

The two views coincide pre-cutover (every config lives in the default org) and the PR description assigns the route/API changes to a later stack PR, so this is not a defect here. It is the seam to watch: if the cutover migration lands before the API PR, availability reporting and resolution diverge for non-default orgs.

Nami confirmed the seam is currently invisible to users: listChatModelConfigs and the availability endpoint see the same set as the resolver until the cutover lands. See CRF-14 for a sibling location of this seam.

🤖

Comment thread coderd/exp_chats.go
return xerrors.Errorf("list chat model configs: %w", err)
}
if len(modelConfigs) == 0 {
orgModelConfigs := make([]database.ChatModelConfig, 0, len(modelConfigs))

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-9] Org isolation of default-config management on the write path has zero discriminating coverage: delete the org filter here, or the organization_id filter from UnsetDefaultChatModelConfigs, and every test in the suite stays green. (Bisky)

The PR's central invariant is that a write in org A never touches org B's defaults. Two pieces of code carry it: the SQL filter in UnsetDefaultChatModelConfigs (chatmodelconfigs.sql:151) and the Go filter in ensureDefaultChatModelConfig (exp_chats.go:7136-7141). I grepped every caller and every fixture: no exp_chats_test.go write-path test ever seeds a chat model config in a second organization ... Every promotion test runs with all rows in one org, so both filters are no-ops in every execution.

The test isn't hard: via the raw store, seed a config with IsDefault: true in a second org (the migration test already proves the index allows it). Then via the API: (1) create a second config in the default org, PATCH it to default, assert the second org's default is still is_default = true; (2) delete the default org's default with a non-default candidate present, assert the promoted config belongs to the default org and the second org's rows are byte-for-byte untouched.

Orchestrator verified the fixture claim: only 3 dbgen.ChatModelConfig calls exist in exp_chats_test.go, none in a second org. #27957 and #27959 rewrite this same call next; without this coverage, a rewrite that drops either filter ships silently.

🤖

Comment thread coderd/exp_chats.go
@@ -7035,8 +7086,9 @@ func (api *API) deleteChatModelConfig(rw http.ResponseWriter, r *http.Request) {
return
}

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-10] The commit message sentence "Update and delete paths re-read mutable state after they acquire the lock" describes the locked-read rewrite that moved to base PR #27968 during the restack; this commit only captures and reuses the re-read results. (Mafu-san)

The re-read pattern pre-exists in the base (verified: git show e19560c9:coderd/exp_chats.go has the "Re-read the row under the write lock" comment). A future reader or bisect attributes the rewrite to this commit. One-sentence commit-message edit fixes it. The PR body, by contrast, correctly discloses that the handler behavior changes now live in #27968.

🤖

// TODO(mafredri): remove after CODAGT-709 M3 (org-scoping cutover);
// orgs list strictly within their own configs.
func enabledChatModelConfigsWithDefaultOrgFallback(
ctx context.Context,

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-29] Pre-cutover, every list-path call from a non-default org that owns no configs fans out to 5 sequential DB round trips where 1 sufficed; this is the common case, not the rare one, since configs only exist in the default org until the cutover PR. (Killua)

Derived from code, not estimated: GetEnabledChatModelConfigsByOrganization(org) (empty) + GetDefaultChatModelConfig(org) (miss) + GetDefaultOrganization + GetDefaultChatModelConfig(defaultOrg) + GetEnabledChatModelConfigsByOrganization(defaultOrg). ... All indexed point queries on a tiny table, per-user-action not per-token, and the whole helper carries a removal TODO for the M3 cutover in the same stack. This does not need fixing; it is the quantified cost the cutover PR deletes.

🤖

// exactly when it owns a default.
// TODO(mafredri): remove after CODAGT-709 M3 (org-scoping cutover);
// orgs list strictly within their own configs.
func enabledChatModelConfigsWithDefaultOrgFallback(

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-30] enabledChatModelConfigsWithDefaultOrgFallback makes every caller run the query it then judges; both call sites duplicate the same two-step dance, and the shape invites a third caller to run the query and forget the fallback. (Zoro P3, Meruem P3, Robin P3, Luffy Nit, Ryosuke Nit)

Zoro: The helper takes rows that the caller must have fetched via GetEnabledChatModelConfigsByOrganization for the same organizationID it also passes. It absorbs no work: both callers (subagent.go:633-640, chatd.go:2784-2787) perform the identical query-then-fallback sequence, and resolveManualTitleModel needs the awkward if err == nil { ... } chaining to compose the two steps.

Robin: The miss this shape permits is not hypothetical: exp_chats.go:1010 already lists enabled configs without the fallback (the CRF-8 seam).

Five reviewers converged. Absorb the query into the helper (enabledChatModelConfigsForOrg(ctx, db, orgID); Zoro's sketch keeps behavior and query count identical, verified only these two callers exist) and co-locate it with defaultChatModelConfigForOrg in chatd.go, which it wraps and which shares its M3 removal deadline. The M3 removal becomes one contiguous deletion, and Komugi's race fix (CRF-15) becomes natural inside the absorbed helper.

🤖

Comment thread coderd/x/chatd/chatd.go
if !errors.Is(err, sql.ErrNoRows) {
return database.ChatModelConfig{}, err
}
defaultOrg, err := store.GetDefaultOrganization(ctx)

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-32] defaultChatModelConfigForOrg propagates a GetDefaultOrganization failure without step context, and no test exercises that branch. (Chopper)

The helper runs up to three queries ... and returns non-ErrNoRows errors from all of them raw. Every caller wraps the result as one signal ... an operator sees a chat fail with "get default chat model config: " and starts debugging chat_model_configs queries, when the actual failure was the default-organization read inside the pre-cutover fallback, a step that exists only until M3. The misdirection costs exactly the time the temporary code was supposed to save.

Wrap the middle hop (xerrors.Errorf("get default organization: %w", err)); sql.ErrNoRows matching at callers is unaffected since that branch returns before the wrap. The uncovered branch is one stub error away from tested.

🤖

require.NoError(t, err, "get default organization")
organizationID = defaultOrg.ID
}
groupACL := seed.GroupACL

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-33] dbgen.ChatModelConfig seeds group_acl as {} while every production write path seeds the everyone-in-org read entry. (Chopper, Ryosuke, Robin)

Chopper: ACLs are inert in this PR, so nothing breaks now, but every dbgen-created fixture represents a config no member can read once the RBAC PR evaluates ACLs. The RBAC PR's tests will either flush this out loudly or quietly pass against unrepresentative fixtures.

Robin adds that the org-and-ACL defaulting is now taught to two test helpers in parallel (dbgen.ChatModelConfig and insertChatModelConfigForTest in querier_test.go:12269); if CRF-24 lands, the ACL half of both blocks disappears, and the default-org lookup remains written twice. Align the dbgen default with the production seed here or in the RBAC PR.

🤖

}
return nil, xerrors.Errorf("resolve default chat model config: %w", err)
}
if defaultConfig.OrganizationID == organizationID {

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-34] Fallback ownership is decided by default-config ownership, which encodes an invariant no schema, constraint, or test enforces; a direct existence check makes the bad state impossible instead of assumed away. (Ryosuke P3, Pariston Note)

Ryosuke: The fix works by inference: defaultConfig.OrganizationID == organizationID proves ownership only if "an org that owns live configs also owns a default" always holds. That invariant lives in write-path discipline (ensureDefaultChatModelConfig under the per-org lock) in package coderd, while the code that depends on it lives in package chatd. Nothing mechanical connects them ... and the M2 cutover migration in the next PR is a write path that bypasses ensureDefaultChatModelConfig entirely. If it copies configs without promoting per-org defaults, the codex bug returns silently.

Pariston: If you want the fallback to stop depending on the invariant entirely, the structural test is one query: decide ownership by EXISTS(SELECT 1 FROM chat_model_configs WHERE organization_id = $1 AND deleted = false) ... same query count on the fallback path.

This is a structural alternative, not a re-open of the closed codex P2: the EXISTS check costs one small query only when the org's enabled list is empty, eliminates the cross-package invariant dependence, and removes the M2 cutover's silent failure mode. If declined, the minimum is CRF-18's documentation plus an explicit check in the M2 cutover review that it promotes a default per org (Luffy independently flagged the cutover as exactly such a writer).

🤖

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

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.

1 participant