Part of #60327 — implements RFC #1103.
Phase 1. Depends on #60329 (foundation + /authorize enforcement).
Scope
Drop _validate_scopes (ee/api/agentic_provisioning/views.py:2130) and ALLOWED_PROVISIONING_SCOPES (:2108, already contains llm_gateway:read at :2118). Apply request ∩ application.scopes (reject outside-ceiling) on all three issuance paths:
- Consent path — builds an
/authorize URL (_build_authorize_url), already flows through I1's override. ✓
- Direct-mint —
_exchange_authorization_code creates the token by hand (views.py:995-1003, scope_str = " ".join(scopes) if scopes else StripeIntegration.SCOPES). Never calls OAuthValidator; needs explicit intersection here.
- Provisioning refresh —
_exchange_refresh_token re-creates the token with scope=old_scope and no intersection; add narrowing.
account_requests reads scopes at views.py:321.
Reconcile
The in-flight worktree-scope-update-service / scope-update-followup branches hydrate token scope at issuance/refresh (scope pinned to consent org). Reconcile this slice with that work before splitting code.
Validation
- Rewrite/remove
ee/api/agentic_provisioning/test_scope_validation.py (tests _validate_scopes).
- New cases in
test_account_requests.py: in/out-of-ceiling on consent + direct-mint; refresh narrowing on the provisioning refresh path.
Direct-mint bypass (load-bearing). PR #60477 enforces the ceiling at /authorize via an OAuthValidator.validate_scopes override. That override does NOT reach _exchange_authorization_code at ee/api/agentic_provisioning/views.py:997-1003, which creates the token by hand with OAuthAccessToken.objects.create(scope=scope_str, ...). The intersection (request ∩ application.scopes) has to land inline here just before the create() call, with the same OIDC carve-out (OIDC_SCOPES ∪ {"introspection"} always allowed). Same applies to _exchange_refresh_token. Three issuance paths total: consent path (already covered by /authorize override), direct-mint, refresh.
Part of #60327 — implements RFC #1103.
Phase 1. Depends on #60329 (foundation +
/authorizeenforcement).Scope
Drop
_validate_scopes(ee/api/agentic_provisioning/views.py:2130) andALLOWED_PROVISIONING_SCOPES(:2108, already containsllm_gateway:readat:2118). Applyrequest ∩ application.scopes(reject outside-ceiling) on all three issuance paths:/authorizeURL (_build_authorize_url), already flows through I1's override. ✓_exchange_authorization_codecreates the token by hand (views.py:995-1003,scope_str = " ".join(scopes) if scopes else StripeIntegration.SCOPES). Never callsOAuthValidator; needs explicit intersection here._exchange_refresh_tokenre-creates the token withscope=old_scopeand no intersection; add narrowing.account_requestsreadsscopesatviews.py:321.Reconcile
The in-flight
worktree-scope-update-service/scope-update-followupbranches hydrate token scope at issuance/refresh (scope pinned to consent org). Reconcile this slice with that work before splitting code.Validation
ee/api/agentic_provisioning/test_scope_validation.py(tests_validate_scopes).test_account_requests.py: in/out-of-ceiling on consent + direct-mint; refresh narrowing on the provisioning refresh path.Direct-mint bypass (load-bearing). PR #60477 enforces the ceiling at
/authorizevia anOAuthValidator.validate_scopesoverride. That override does NOT reach_exchange_authorization_codeatee/api/agentic_provisioning/views.py:997-1003, which creates the token by hand withOAuthAccessToken.objects.create(scope=scope_str, ...). The intersection (request ∩ application.scopes) has to land inline here just before thecreate()call, with the same OIDC carve-out (OIDC_SCOPES ∪ {"introspection"}always allowed). Same applies to_exchange_refresh_token. Three issuance paths total: consent path (already covered by /authorize override), direct-mint, refresh.