fix(identity): close the check-then-act race on max_agents - #942
Open
AmirF194 wants to merge 1 commit into
Open
Conversation
This was referenced Aug 28, 2026
EnforceAgentCreate counted existing agents and CreateAgent inserted the new one as two independent operations with nothing serializing them, so concurrent POST /v1/agents requests could all read the same pre-insert count and all pass. Reproduced against real Postgres: 8 concurrent requests against a cap of 1, all 8 succeeded. CreateAgentWithLimit takes a per-user advisory lock (keyspace 2, distinct from claimOrCreateDomain's keyspace 1) and re-checks the count inside the same transaction as the INSERT, mirroring tokencanopy#901's fix for max_domains. EnforceAgentCreate stays wired on the limit-exceeded path only, to attach the plan/upgrade-URL details a bare AgentLimitExceededError doesn't carry. Fixes tokencanopy#940
AmirF194
force-pushed
the
fix/agent-cap-concurrency-race
branch
from
August 28, 2026 21:41
49f252c to
70e3b1b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
EnforceAgentCreate(themax_agentspre-check) andCreateAgent(the insert) ran as two independent operations with nothing serializing them: concurrentPOST /v1/agentsrequests could all read the same pre-insert count and all pass. Reported with a live reproduction against real Postgres: 8 concurrent requests against a cap of 1, all 8 succeeded.CreateAgentWithLimitnow takes a per-user advisory lock (keyspace 2, distinct fromclaimOrCreateDomain's keyspace 1) and re-checks the count inside the same transaction as theINSERT, mirroring #901's fix formax_domains.handleCreateAgentcalls it directly instead ofEnforceAgentCreate+CreateAgent.EnforceAgentCreatestays wired, called only on the limit-exceeded path, to attach the plan/upgrade-URL details a bareAgentLimitExceededErrordoesn't carry; if it's unset or doesn't corroborate, the endpoint still answers 402 from the bare error.Operational risk
No schema change, no behavior change on the non-concurrent path or in the 402 response shape (still
limit_exceeded/ resourceagents, same fields). A request that previously slipped past the cap under a race now gets402 limit_exceeded, the same response the endpoint already returns for a non-racy over-cap request.internal/agent/oauth_handlers.go'sissueOAuthCodeWithNewAgent(the OAuth auto-provisioning path) also creates an agent, viaCreateAgentTx, and does not checkmax_agentsat all, before or after this change. Left alone here (different mechanism, and I don't have context on the intended cap behavior for auto-provisioning); flagged in the issue.Client surface checklist
Not applicable: internal concurrency fix, the request/response shape of
POST /v1/agentsis unchanged.Test plan
internal/e2e/agents_create_race_e2e_test.go,-tags integration): 8 concurrentPOST /v1/agentsagainstmax_agents=1. Fails onmain(8/8 succeed, cap blown 8x), passes on this branch (1 created, 7402 limit_exceeded), both against a real Postgres 16 container.go test ./internal/identity/...and./internal/httpapi/...green, including two new unit tests for theGetLimits-failure and enforcer-unwired error-mapping paths.go test ./...(theGo testsjob's own command) green.internal/httpapicoverage 87.0%, above the package's 73% floor.Fixes #940