feat(accounts): add feature flags with negative-balance opt-in - #1817
Conversation
Add a database-only allow_negative_balance policy on user and organization billing accounts, defaulting to false. Apply the policy to regular and composite model key eligibility, batch admission, and insufficient-credit error enrichment while retaining usage accounting and key spending caps. Notify onwards when the policy changes, document its operation, refresh SQLx query metadata, and cover eligibility, accounting, organization scope, and access restrictions with regression tests.
Deploying control-layer with
|
| Latest commit: |
a31f7e4
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://6c16725e.control-layer.pages.dev |
| Branch Preview URL: | https://peter-credit-check-bypass.control-layer.pages.dev |
There was a problem hiding this comment.
1 issue found across 13 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="dwctl/src/db/handlers/credits.rs">
<violation number="1" location="dwctl/src/db/handlers/credits.rs:460">
P3: In `get_balance_for_admission`, the flag and the balance are read with two separate queries, so a flag flip committed between them yields a stale decision for one admission check. Since this method gates batch admission, onwards eligibility, and error enrichment, fold both reads into a single statement so the flag and balance come from one snapshot.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| /// permits debt. This policy never changes the balance used for accounting. | ||
| #[instrument(skip(self), fields(user_id = %abbrev_uuid(&user_id)), err)] | ||
| pub async fn get_balance_for_admission(&mut self, user_id: UserId) -> Result<Option<Decimal>> { | ||
| let allow_negative_balance = sqlx::query_scalar!( |
There was a problem hiding this comment.
P3: In get_balance_for_admission, the flag and the balance are read with two separate queries, so a flag flip committed between them yields a stale decision for one admission check. Since this method gates batch admission, onwards eligibility, and error enrichment, fold both reads into a single statement so the flag and balance come from one snapshot.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At dwctl/src/db/handlers/credits.rs, line 460:
<comment>In `get_balance_for_admission`, the flag and the balance are read with two separate queries, so a flag flip committed between them yields a stale decision for one admission check. Since this method gates batch admission, onwards eligibility, and error enrichment, fold both reads into a single statement so the flag and balance come from one snapshot.</comment>
<file context>
@@ -453,6 +453,24 @@ impl<'c> Credits<'c> {
+ /// permits debt. This policy never changes the balance used for accounting.
+ #[instrument(skip(self), fields(user_id = %abbrev_uuid(&user_id)), err)]
+ pub async fn get_balance_for_admission(&mut self, user_id: UserId) -> Result<Option<Decimal>> {
+ let allow_negative_balance = sqlx::query_scalar!(
+ "SELECT allow_negative_balance FROM users WHERE id = $1 AND is_deleted = false",
+ user_id
</file context>
There was a problem hiding this comment.
🟡 Changes recommended
Deleted-key filtering is still needed in the deployment lookup exemption branches.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds a database-only account opt-in allowing negative balances while preserving usage accounting, permissions, and spending caps.
Changes:
- Applies the opt-in to model eligibility, deployment lookup, batch admission, and error enrichment.
- Adds migration, reload notification, documentation, SQLx metadata, and regression tests.
- Review note (nit, 3 votes): deployment lookup needs deleted-key predicates in the exemption branches.
File summaries
| File | Summary |
|---|---|
dwctl/src/sync/onwards_config/tests.rs |
Tests eligibility toggling and restrictions. |
dwctl/src/sync/onwards_config/mod.rs |
Updates regular and composite model eligibility. |
dwctl/src/error_enrichment.rs |
Adjusts insufficient-credit error enrichment. |
dwctl/src/db/handlers/credits.rs |
Adds balance-admission policy handling and tests. |
dwctl/src/db/handlers/api_keys.rs |
Updates deployment-key eligibility. |
dwctl/src/api/handlers/batches.rs |
Allows opted-in negative-balance batches. |
dwctl/migrations/147_allow_negative_balance.sql |
Adds the account flag and reload trigger. |
docs/onwards-sync-notification-system.md |
Documents operator configuration. |
.sqlx/query-d5be88341f10e2b20ca435b2ec6ffa0831e96183fec04f4ba1ed600fbade86f0.json |
Updates SQLx query metadata. |
.sqlx/query-d04d6a6b66259e75a9b18118eb139031d9c66398f447d148abb5e42c255d4394.json |
Adds admission-query metadata. |
.sqlx/query-3161d003a16d0634a6d1b6e69460eeb7f6a07d29a042cb8e77f98512fc139eb8.json |
Updates composite-query metadata. |
.sqlx/query-2704727661d6058708a8f3297121b230fbd8377a3845c1e9f0afdeb183855eab.json |
Updates deployment-query metadata. |
.sqlx/query-0fdbda77ac8a89b72d3c1c218b7013442942604e7a172d7024fa1e708ebc0ba1.json |
Updates regular-query metadata. |
Review details
Files not reviewed (3)
- .sqlx/query-3161d003a16d0634a6d1b6e69460eeb7f6a07d29a042cb8e77f98512fc139eb8.json: Generated file
- .sqlx/query-d04d6a6b66259e75a9b18118eb139031d9c66398f447d148abb5e42c255d4394.json: Generated file
- .sqlx/query-d5be88341f10e2b20ca435b2ec6ffa0831e96183fec04f4ba1ed600fbade86f0.json: Generated file
- Files reviewed: 10/13 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| WHERE dg.deployment_id = $1 | ||
| AND ( | ||
| ak.user_id = $2 -- System user always has access | ||
| OR EXISTS ( | ||
| SELECT 1 FROM users u | ||
| WHERE u.id = ak.user_id AND u.allow_negative_balance AND NOT u.is_deleted | ||
| ) |
Replace the unreleased negative-balance column with account-scoped feature storage and a shared SQL and typed Rust lookup. Notify routing consumers when flags are inserted, updated, or removed while preserving usage accounting and key restrictions. Check batch file-owner admission independently of organization context and cover flag lifecycle, account isolation, and organization-owned admission with regression tests.
There was a problem hiding this comment.
All reported issues were addressed across 15 files
Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.
Fix all with cubic | Re-trigger cubic
Expose the enabled-account lookup to the PostgreSQL planner in both regular and composite routing queries so it can reuse a hashed set instead of evaluating a SQL function per model and API key. Preserve the outer deleted-user guard and existing balance, permission, and spending-cap checks. Regenerate SQLx query metadata and update the query-shape regression guard.
…keys Check the resolved batch execution key owner independently from the active organization and file owner, preventing an organization debt opt-in from covering a capped personal key. Use the execution creditor for verification status. Exclude soft-deleted API keys from all deployment lookup branches and their bulk access queries. Add regressions for cross-account admission and revoked-key lookup, regenerate SQLx metadata, and clarify soft deletion and zero-balance batch behavior in the feature flag documentation.
There was a problem hiding this comment.
All reported issues were addressed across 7 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
Read verification status from the active account used by the outstanding-volume limiter. Keep the resolved execution account balance check independent so a cross-account key cannot inherit a negative-balance opt-in without changing existing quota semantics.
Integrate the analytics checkpoint changes from main with the account feature flag and admission fixes so required checks validate the current merge base.
🤖 I have created a release *beep* *boop* --- ## [12.1.0](v12.0.1...v12.1.0) (2026-09-18) ### Features * **accounts:** add feature flags with negative-balance opt-in ([#1817](#1817)) ([dc3bb30](dc3bb30)) ### Bug Fixes * **analytics:** checkpoint records before enrichment ([#1789](#1789)) ([7ed4000](7ed4000)) * **fusillade:** fence reclaimed request ownership ([#1804](#1804)) ([14a53f9](14a53f9)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
Summary
Accounts normally lose paid-model access at zero balance and batch admission below zero. Add a reusable
user_feature_flagstable and useALLOW_NEGATIVE_BALANCEto let opted-in billing accounts accrue debt while usage continues to be charged.The database enforces one row per account and flag. Missing or disabled rows mean disabled. A typed Rust lookup and SQL
user_has_featurefunction share the same account-scoped behavior, including denying flags on soft-deleted accounts. Insert, update, and delete notifications refresh onwards eligibility for regular and composite models. Bulk onwards reloads use inlineEXISTSlookups so PostgreSQL can reuse an enabled-account set instead of invoking a SQL function for each model/key pair; single-account admission checks retain the shared helper.Organization-owned keys use the organization's flag; personal keys use their owner's flag. Batch admission checks the active account, uploaded file owner, and resolved execution-key billing account independently, including capped personal keys used in organization context. Deployment-key lookups exclude soft-deleted keys in every access branch. Model permissions, key deletion, and explicit key spending caps remain enforced. Flags are managed directly in the database, with enable/disable SQL documented in the onwards sync guide.
This is the first consumer of the shared table. Existing ZDR, invoicing, and auto-join storage is unchanged; config seeding and authenticated-user flag enrichment are outside this change. The migration is additive relative to the released schema and replaces the unreleased boolean-column migration in this PR.
Validation
Full CI on the latest commit passed, including security, Open Responses, and pooled-database end-to-end checks.
Rust library suite: 3,298 passed, 4 ignored, 0 failed across all workspace crates.
Rust clippy with all features, formatting, and SQLx prepared-query validation passed.
Fresh-schema migration and direct PostgreSQL checks passed for flag defaults, uniqueness, timestamps, account isolation, soft deletion, and regular/composite routing toggles.
Regression coverage includes capped personal execution keys with organization context, revoked-key deployment lookup, organization versus personal billing, personal file submission from organization context, flag insertion/update/deletion notifications, usage accounting, and existing access/spending restrictions.
Independent code review completed; findings addressed.