feat(AGX1-272): dual-write agent_api_keys to spark-authz behind FGAC flag#248
Merged
Conversation
4 tasks
dm36
added a commit
that referenced
this pull request
May 26, 2026
Closes the parent_agent cascade gap surfaced on scaleapi/agentex#354. The api_key dual-write (AGX1-272, PR #248) currently calls grant() which writes the owner edge in SpiceDB but NOT the parent_agent edge. The agent_api_key schema requires `read = ... & parent_agent->read & ...`, so every downstream read/update fails closed without that edge. This PR adds register_resource/deregister_resource (Port + adapter + service) and swaps the api_keys use case from grant→register_resource with parent=AgentexResource.agent(agent_id). Now the owner edge and parent_agent edge are written atomically. Stack: - scaleapi/scaleapi#144657 — sgp-authz 0.7.0 (parent_resource kwarg). - scaleapi/agentex#355 — agentex-auth Port + adapter + HTTP routes. - #248 — original AGX1-272 dual-write (this stacks on it). - THIS PR — extends #248 to use the parent-aware path. Changes: - Port: abstract register_resource(resource, parent=None) and deregister_resource(resource). - Adapter proxy: POST /v1/authz/register and /v1/authz/deregister. - Service: mirror existing grant/revoke pattern (principal_context override, _bypass support, parent in log line for cascade debugging). - Use case: swap grant→register_resource passing parent=agent; swap revoke→deregister_resource. except Exception wrappers preserved (fail-closed on register, best-effort on deregister). - Tests: rename mocks to register_resource/deregister_resource; assert the parent edge is passed correctly. Test plan: - pytest agentex/tests/integration/services/test_agent_api_key_service_dual_write.py → 8 / 8 pass. - New test ``test_create_api_key_calls_grant_when_flag_on`` asserts parent.type == AgentexResourceType.agent and parent.selector == agent.id. Other resource types' grant→register_resource swap is out of scope. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…AGENT_API_KEYS_DUAL_WRITE flag Mirrors the AGX1-274 task dual-write pattern (PR #246) for agent_api_keys. - Adds creator_user_id / creator_service_account_id / spark_authz_zedtoken columns to agent_api_keys, with CHECK constraint and concurrent indexes. - On create, when FGAC_AGENT_API_KEYS_DUAL_WRITE is enabled for the caller's account, calls authorization_service.grant(AgentexResource.api_key(id)) BEFORE the Postgres write. Grant failure aborts the create. - On delete, best-effort revoke after the Postgres delete. Failures are logged but do not block the delete. - Adds AgentexResourceType.api_key and AgentexResource.api_key(...) factory. - Creates src/utils/feature_flags.py with both FGAC_TASKS_DUAL_WRITE and FGAC_AGENT_API_KEYS_DUAL_WRITE (file does not exist on main yet; if PR #246 lands first this becomes a rebase concern). Structural divergence from tasks: agent_api_keys have no service layer, so the dual-write logic lives in AgentAPIKeysUseCase rather than a separate service. This keeps the call site simple and avoids inventing a new layer. Route layer (read-side auth checks) is out of scope; that's PR B (AGX1-273). agentex-auth spark_mapping.py update is a sibling-repo concern. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes the parent_agent cascade gap surfaced on scaleapi/agentex#354. The api_key dual-write (AGX1-272, PR #248) currently calls grant() which writes the owner edge in SpiceDB but NOT the parent_agent edge. The agent_api_key schema requires `read = ... & parent_agent->read & ...`, so every downstream read/update fails closed without that edge. This PR adds register_resource/deregister_resource (Port + adapter + service) and swaps the api_keys use case from grant→register_resource with parent=AgentexResource.agent(agent_id). Now the owner edge and parent_agent edge are written atomically. Stack: - scaleapi/scaleapi#144657 — sgp-authz 0.7.0 (parent_resource kwarg). - scaleapi/agentex#355 — agentex-auth Port + adapter + HTTP routes. - #248 — original AGX1-272 dual-write (this stacks on it). - THIS PR — extends #248 to use the parent-aware path. Changes: - Port: abstract register_resource(resource, parent=None) and deregister_resource(resource). - Adapter proxy: POST /v1/authz/register and /v1/authz/deregister. - Service: mirror existing grant/revoke pattern (principal_context override, _bypass support, parent in log line for cascade debugging). - Use case: swap grant→register_resource passing parent=agent; swap revoke→deregister_resource. except Exception wrappers preserved (fail-closed on register, best-effort on deregister). - Tests: rename mocks to register_resource/deregister_resource; assert the parent edge is passed correctly. Test plan: - pytest agentex/tests/integration/services/test_agent_api_key_service_dual_write.py → 8 / 8 pass. - New test ``test_create_api_key_calls_grant_when_flag_on`` asserts parent.type == AgentexResourceType.agent and parent.selector == agent.id. Other resource types' grant→register_resource swap is out of scope. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
9668f1a to
e72df68
Compare
…L_WRITE
Per team discussion: rather than maintain a parallel env-var flag system
in scale-agentex, route api_key dual-write flag checks through
egp-api-backend's existing flag service. One source of truth across services,
single flip surface for ops, fewer per-env env-var allowlists to keep in sync.
Changes:
- EnvVarKeys.EGP_API_BACKEND_URL — new env var for the egp-api-backend
base URL. Used by the new HTTP-backed flag provider.
- FeatureFlagProvider rewritten as an HTTP client of egp-api-backend's
GET /feature-flag/{id} endpoint:
* Forwards x-api-key / x-user-id / x-service-account-id /
x-selected-account-id from the caller's principal_context so the
endpoint's REQUIRE_IDENTITY_AND_OPTIONAL_ACCOUNT policy admits the
request.
* Coerces the response's `value` field to bool.
* Fails closed to False on any error (config missing, no identity,
non-2xx, transport failure, JSON parse failure) — the legacy
no-Spark code path is the safe default.
* `is_enabled` is now async (HTTP call). Signature is
`is_enabled(name, *, principal_context, account_id)`.
- AgentAPIKeysUseCase: both call sites now await is_enabled and pass
principal_context. _deregister grabs principal_context from
self.authorization_service.
- Test fixtures: mock FeatureFlagProvider directly (Mock with
is_enabled = AsyncMock(return_value=flag_on)) so dual-write tests stay
hermetic. The pre-existing FeatureFlagProvider() no-arg constructions
in test_agents_api_keys_use_case.py and integration_client.py now pass
egp_api_backend_url=None (provider returns False without it, matching
the prior "flag never enabled in unit tests" behavior).
Out of scope:
- Migrating Asher's FGAC_TASKS_DUAL_WRITE flag check off env vars.
That's task-team-owned and we leave their existing pattern alone per
the team discussion (new-work-only).
- Caching the flag response. Each is_enabled is a fresh HTTP call.
Egp-api-backend's flag endpoint is fast and the caller paths are
already crossing the network for the actual register/deregister, so
one extra round-trip is acceptable for now. Add caching later if
load profiling shows it matters.
Test plan:
- uv run pytest agentex/tests/integration/services/test_agent_api_key_service_dual_write.py — 8/8 pass.
- Existing 4 unrelated test_agents_api_keys_use_case.py docker-fixture
errors predate this commit (verified via `git stash`).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
harvhan
reviewed
May 29, 2026
Three rounds of Harvey's review feedback together pointed at the same concern: scale-agentex (OSS) should not be coupled to egp-api-backend (proprietary) feature-flag service or Spark-specific schema concepts. Changes: - Drop FeatureFlagProvider and the egp-api-backend HTTP query entirely. scale-agentex now calls register_resource/deregister_resource unconditionally. agentex-auth's per-account routing (FGAC_AGENTEX_AUTH_SPARK, scaleapi/agentex#353) decides whether the call lands on Spark AuthZ or falls back to legacy SGP for the caller's account. - Drop the creator_user_id / creator_service_account_id columns from the agent_api_keys table. They were only used as a runtime guard inside the auth-registration helper. Moved that check inline. - Drop the spark_authz_zedtoken column. The name leaked SpiceDB-specific terminology into the OSS schema, and the column was always None. - Delete the Alembic migration entirely. migration_history.txt re-pointed to head=6c942325c828. - Rename helper methods: _register_api_key_in_spark_authz -> _register_api_key_in_auth, same for deregister. Test plan: 6/6 dual-write integration tests pass. Pre-existing docker-fixture errors in test_agents_api_keys_use_case.py unrelated. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
jenniechung
reviewed
May 29, 2026
Per @jenniechung review: delete(id) used to call deregister unconditionally, which wastes an auth round-trip when the id doesn't exist. The two delete_by_* methods already use pre-fetch + `if existing is not None`; delete() now matches that pattern via try/except ItemDoesNotExist. Also tightened the unconditional-register comment from 5 lines to 2. Tests: - Fixture now defaults repo.get() to a stub entity so existing delete tests flow through the deregister path; new helper `_stub_api_key`. - New test_delete_api_key_skips_deregister_when_row_does_not_exist asserts deregister.assert_not_called() when repo.get raises ItemDoesNotExist. - 7/7 dual-write tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…t fixture Per reviewer: the agent_api_keys_use_case fixture had `grant` and `revoke` set up as AsyncMocks but not `register_resource` / `deregister_resource`. The deregister path silently swallowed the resulting TypeError via its `except Exception` handler, so delete tests passed without exercising the auth-side call. Matches the integration_client.py fixture shape. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
harvhan
approved these changes
May 29, 2026
| api_key_type=api_key_type, | ||
| api_key=api_key, | ||
| ) | ||
| return await self.agent_api_key_repo.create(item=agent_api_key) |
Contributor
There was a problem hiding this comment.
Should we follow the egp-api-backend's pattern and add compensating deregister if repo.create fails?
Contributor
Author
There was a problem hiding this comment.
followed asher's approach here with tasks with not compensating deregister if repo.create fails for consistency- i can update tho
4 tasks
agentex-auth derives account_id from the authenticated principal on its side, so threading it through routes and use-case calls in OSS scale-agentex is dead weight — and leaks the SGP-specific field name. Drop: - account_id extraction (getattr on principal_context) in all three api_keys route handlers; drop the now-unused DAuthorizationService injection from create_api_key/delete_agent_api_key. - account_id parameter on create, delete, delete_by_agent_id_and_key_name, delete_by_agent_name_and_key_name, _register_api_key_in_auth, and _deregister_api_key_from_auth. - account_id entries from log extras (it was only ever log context — no auth call ever consumed it). - account_id kwargs at use-case call sites in the dual-write test. Keep the _principal(account_id=...) test helper — it models the principal shape and Harvey's feedback is about production OSS surface, not test mocks. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per Harvey's nit: when the pre-fetch raises ItemDoesNotExist, skip both the repo.delete (was issuing a wasted DB round-trip for a no-op) and the deregister. Same HTTP response from the route either way. Test renamed and updated: now asserts neither repo.delete nor deregister is called when the row doesn't exist (previously asserted repo.delete was called). Note: the delete_by_* variants have the symmetric pattern (unconditional repo.delete_by_* even when the pre-fetch returns None), but they predate this PR and Harvey's comment was scoped to delete. Leaving those for now. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
danielmillerp
approved these changes
Jun 2, 2026
rpatel-scale
approved these changes
Jun 2, 2026
dm36
added a commit
that referenced
this pull request
Jun 2, 2026
…+ two-factor) (#252) ## Summary Wires Spark AuthZ into all six `agent_api_keys` routes. Mirrors Asher's task-route pattern from [#249](#249). - All denials on the api_key resource collapse to `404` so callers can't probe cross-tenant existence. - `GET` list filters to api_keys the caller can read (id filter pushed into the repo for correct pagination). - `POST` does an explicit `agent.update` check on the parent — only enforcement surface at create time, since no api_key resource exists yet. - Mutations (`delete`) rely on SpiceDB's transitive `api_key.delete = editor & parent_agent->update & tenant_gate` expansion. The `parent_agent` edge is populated by [#248](#248 `register_resource(parent=agent)`. ## Stack | Repo | PR | State | |---|---|---| | scaleapi/scaleapi | [#144657](scaleapi/scaleapi#144657) — sgp-authz 0.7.0 | ✅ Merged | | scaleapi/agentex | [#354](scaleapi/agentex#354) — `api_key` mapping | ✅ Merged | | scaleapi/scale-agentex | [#248](#248) — dual-write | ✅ Merged | | scaleapi/scale-agentex | this PR — route-layer FGAC | Ready | Linear: [AGX1-263](https://linear.app/scale-epd/issue/AGX1-263). ## Tests - `tests/unit/api/test_agent_api_keys_authz.py` — 12 tests covering the collapse helper, `DAuthorizedId` routing, name-route inline collapse, list filtering, and create-parent-check. ## Out of scope - Restoring the 403/404 split once api_keys carry tenant scope ([AGX1-290](https://linear.app/scale-epd/issue/AGX1-290)). <!-- greptile_comment --> <h3>Greptile Summary</h3> This PR wires Spark AuthZ (FGAC) into all six `agent_api_keys` routes, mirroring the task-route pattern from #249. All authorization denials collapse to `404` to prevent cross-tenant key-name probing, and the delete-by-name route now resolves name→id before checking auth (closing the TOCTOU window flagged in a prior review). - **All six routes gated**: `POST` checks `agent.update` on the parent agent (no api_key resource exists yet); `GET /{id}` and `DELETE /{id}` use `DAuthorizedId`; `GET /name/{name}` and `DELETE /name/{name}` use the inline `_check_api_key_or_collapse_to_404` helper; `GET /` uses `DAuthorizedResourceIds` with the id list pushed into the SQL filter for correct pagination. - **404 collapse**: Both \"row absent\" and \"row present but denied\" paths on name routes now raise `ItemDoesNotExist(API_KEY_NOT_FOUND_MESSAGE)`, making the bodies byte-for-byte identical and closing the existence-leak vector. - **`AgentAPIKeysUseCase.list`** gains an optional `id` filter with an explicit empty-list short-circuit to prevent the base repo from converting `id=[]` into an unfiltered query. <details><summary><h3>Confidence Score: 5/5</h3></summary> The FGAC wiring is correct across all six routes; the 404-collapse and TOCTOU fixes flagged in prior reviews have been addressed. The authorization logic is sound: denials are consistently collapsed to 404 with identical bodies, delete-by-name now deletes by resolved ID rather than by name, and the empty-list short-circuit prevents an accidental unfiltered query. The only findings are a misleading (but vacuous) test assertion and an untyped parameter — neither affects runtime behavior. The test assertion in test_delete_by_name_handler_collapses_denial_to_404 checks a removed method rather than the one now used for deletion; worth a quick fix before the tests are relied upon for regression coverage. </details> <h3>Important Files Changed</h3> | Filename | Overview | |----------|----------| | agentex/src/api/routes/agent_api_keys.py | All six routes now gated by FGAC; denials collapse to 404; delete-by-name resolves name→id before auth check, closing the TOCTOU window; absent and denied 404 bodies are byte-for-byte identical via API_KEY_NOT_FOUND_MESSAGE. | | agentex/src/utils/agent_api_key_authorization.py | New collapse helper correctly catches AuthorizationError and raises ItemDoesNotExist with a fixed message; minor: authorization parameter is untyped. | | agentex/src/utils/authorization_shortcuts.py | DAuthorizedId gains an api_key branch that delegates to the collapse helper; formatting-only change to DAuthorizedBodyId; no logic issues. | | agentex/src/domain/use_cases/agent_api_keys_use_case.py | list() gains optional id filter pushed to repo layer; empty-list short-circuit correctly prevents unfiltered query when caller has no authorized keys. | | agentex/tests/unit/api/test_agent_api_keys_authz.py | 12 tests covering all major FGAC paths; test_delete_by_name_handler_collapses_denial_to_404 asserts the removed delete_by_agent_id_and_key_name is not called rather than the new delete method, making the delete-not-invoked assertion vacuous. | | agentex/src/api/schemas/authorization_types.py | Formatting-only change to the schedule() method signature; no logic changes. | </details> <details><summary><h3>Sequence Diagram</h3></summary> ```mermaid sequenceDiagram participant Client participant Route as agent_api_keys route participant SpiceDB as SpiceDB (AuthZ) participant DB as Postgres (repo) Note over Route,DB: GET /{id} / DELETE /{id} — DAuthorizedId dep Client->>Route: "GET /agent_api_keys/{id}" Route->>SpiceDB: "check(api_key:{id}, read)" alt denied SpiceDB-->>Route: AuthorizationError Route-->>Client: 404 ItemDoesNotExist else allowed SpiceDB-->>Route: OK Route->>DB: get(id) DB-->>Route: entity Route-->>Client: 200 AgentAPIKey end Note over Route,DB: GET /name/{name} — inline collapse Client->>Route: "GET /agent_api_keys/name/{name}" Route->>DB: get_by_agent_id_and_name(...) alt absent DB-->>Route: None Route-->>Client: 404 Agent api_key not found. else present DB-->>Route: entity (with id) Route->>SpiceDB: "check(api_key:{id}, read)" alt denied SpiceDB-->>Route: AuthorizationError Route-->>Client: 404 Agent api_key not found. else allowed SpiceDB-->>Route: OK Route-->>Client: 200 AgentAPIKey end end Note over Route,DB: DELETE /name/{name} — resolve→check→delete Client->>Route: "DELETE /agent_api_keys/name/{name}" Route->>DB: get_by_agent_id_and_name(...) alt absent DB-->>Route: None Route-->>Client: 404 Agent api_key not found. else present DB-->>Route: entity (with id) Route->>SpiceDB: "check(api_key:{id}, delete)" alt denied SpiceDB-->>Route: AuthorizationError Route-->>Client: 404 Agent api_key not found. else allowed SpiceDB-->>Route: OK Route->>DB: "delete(id=entity.id)" Route-->>Client: 200 deleted end end Note over Route,DB: POST / — parent agent.update check Client->>Route: POST /agent_api_keys Route->>DB: agent_use_case.get(...) Route->>SpiceDB: "check(agent:{id}, update)" alt denied SpiceDB-->>Route: AuthorizationError 403 Route-->>Client: 403 else allowed SpiceDB-->>Route: OK Route->>DB: create api_key Route-->>Client: 201 CreateAPIKeyResponse end ``` </details> <!-- greptile_failed_comments --> <details open><summary><h3>Comments Outside Diff (2)</h3></summary> 1. `agentex/src/api/routes/agent_api_keys.py`, line 163-179 ([link](https://github.com/scaleapi/scale-agentex/blob/4335ca721893fc121ba8209b05ee1dcc6ef9d7b5/agentex/src/api/routes/agent_api_keys.py#L163-L179)) <a href="#"><img alt="P1" src="https://greptile-static-assets.s3.amazonaws.com/badges/p1.svg?v=9" align="top"></a> <a href="#"><img alt="security" src="https://greptile-static-assets.s3.amazonaws.com/badges/Security.svg?v=2" align="top"></a> **Distinguishable 404 bodies defeat the existence-leak collapse** The name routes produce two structurally different 404 responses depending on whether the key exists in the database. If the key is absent, `HTTPException` is raised with `"Agent api_key '{name}' not found for agent ID {agent.id}"`. If the key exists but the caller is denied, `ItemDoesNotExist` is raised with `"Item with id '{api_key_id}' does not exist."`. Both return HTTP 404, but a cross-tenant caller who can resolve an `agent_id` can probe for key-name existence by comparing the response body — exactly the information leak the collapse is meant to prevent. The same issue exists in `delete_agent_api_key_by_name` (line ~264). To close it, both "not found" paths should produce an identical detail string (e.g. both raise `ItemDoesNotExist` with the same generic message). <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: agentex/src/api/routes/agent_api_keys.py Line: 163-179 Comment: **Distinguishable 404 bodies defeat the existence-leak collapse** The name routes produce two structurally different 404 responses depending on whether the key exists in the database. If the key is absent, `HTTPException` is raised with `"Agent api_key '{name}' not found for agent ID {agent.id}"`. If the key exists but the caller is denied, `ItemDoesNotExist` is raised with `"Item with id '{api_key_id}' does not exist."`. Both return HTTP 404, but a cross-tenant caller who can resolve an `agent_id` can probe for key-name existence by comparing the response body — exactly the information leak the collapse is meant to prevent. The same issue exists in `delete_agent_api_key_by_name` (line ~264). To close it, both "not found" paths should produce an identical detail string (e.g. both raise `ItemDoesNotExist` with the same generic message). How can I resolve this? If you propose a fix, please make it concise. ````` </details> <a href="https://app.greptile.com/api/ide/cursor?prompt=This%20is%20a%20comment%20left%20during%20a%20code%20review.%0APath%3A%20agentex%2Fsrc%2Fapi%2Froutes%2Fagent_api_keys.py%0ALine%3A%20163-179%0A%0AComment%3A%0A**Distinguishable%20404%20bodies%20defeat%20the%20existence-leak%20collapse**%0A%0AThe%20name%20routes%20produce%20two%20structurally%20different%20404%20responses%20depending%20on%20whether%20the%20key%20exists%20in%20the%20database.%20If%20the%20key%20is%20absent%2C%20%60HTTPException%60%20is%20raised%20with%20%60%22Agent%20api_key%20'%7Bname%7D'%20not%20found%20for%20agent%20ID%20%7Bagent.id%7D%22%60.%20If%20the%20key%20exists%20but%20the%20caller%20is%20denied%2C%20%60ItemDoesNotExist%60%20is%20raised%20with%20%60%22Item%20with%20id%20'%7Bapi_key_id%7D'%20does%20not%20exist.%22%60.%20Both%20return%20HTTP%20404%2C%20but%20a%20cross-tenant%20caller%20who%20can%20resolve%20an%20%60agent_id%60%20can%20probe%20for%20key-name%20existence%20by%20comparing%20the%20response%20body%20%E2%80%94%20exactly%20the%20information%20leak%20the%20collapse%20is%20meant%20to%20prevent.%20The%20same%20issue%20exists%20in%20%60delete_agent_api_key_by_name%60%20%28line%20~264%29.%20To%20close%20it%2C%20both%20%22not%20found%22%20paths%20should%20produce%20an%20identical%20detail%20string%20%28e.g.%20both%20raise%20%60ItemDoesNotExist%60%20with%20the%20same%20generic%20message%29.%0A%0AHow%20can%20I%20resolve%20this%3F%20If%20you%20propose%20a%20fix%2C%20please%20make%20it%20concise.&pr=252&platform=github"><picture><source media="(prefers-color-scheme: dark)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixInCursorDark.svg?v=3"><source media="(prefers-color-scheme: light)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixInCursor.svg?v=3"><img alt="Fix in Cursor" src="https://greptile-static-assets.s3.amazonaws.com/badges/FixInCursor.svg?v=3" height="20"></picture></a> <a href="https://app.greptile.com/ide/claude-code?prompt=This%20is%20a%20comment%20left%20during%20a%20code%20review.%0APath%3A%20agentex%2Fsrc%2Fapi%2Froutes%2Fagent_api_keys.py%0ALine%3A%20163-179%0A%0AComment%3A%0A**Distinguishable%20404%20bodies%20defeat%20the%20existence-leak%20collapse**%0A%0AThe%20name%20routes%20produce%20two%20structurally%20different%20404%20responses%20depending%20on%20whether%20the%20key%20exists%20in%20the%20database.%20If%20the%20key%20is%20absent%2C%20%60HTTPException%60%20is%20raised%20with%20%60%22Agent%20api_key%20'%7Bname%7D'%20not%20found%20for%20agent%20ID%20%7Bagent.id%7D%22%60.%20If%20the%20key%20exists%20but%20the%20caller%20is%20denied%2C%20%60ItemDoesNotExist%60%20is%20raised%20with%20%60%22Item%20with%20id%20'%7Bapi_key_id%7D'%20does%20not%20exist.%22%60.%20Both%20return%20HTTP%20404%2C%20but%20a%20cross-tenant%20caller%20who%20can%20resolve%20an%20%60agent_id%60%20can%20probe%20for%20key-name%20existence%20by%20comparing%20the%20response%20body%20%E2%80%94%20exactly%20the%20information%20leak%20the%20collapse%20is%20meant%20to%20prevent.%20The%20same%20issue%20exists%20in%20%60delete_agent_api_key_by_name%60%20%28line%20~264%29.%20To%20close%20it%2C%20both%20%22not%20found%22%20paths%20should%20produce%20an%20identical%20detail%20string%20%28e.g.%20both%20raise%20%60ItemDoesNotExist%60%20with%20the%20same%20generic%20message%29.%0A%0AHow%20can%20I%20resolve%20this%3F%20If%20you%20propose%20a%20fix%2C%20please%20make%20it%20concise.&repo=scaleapi%2Fscale-agentex&pr=252&platform=github"><picture><source media="(prefers-color-scheme: dark)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixInClaudeDark.svg?v=3"><source media="(prefers-color-scheme: light)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixInClaude.svg?v=3"><img alt="Fix in Claude Code" src="https://greptile-static-assets.s3.amazonaws.com/badges/FixInClaude.svg?v=3" height="20"></picture></a> <a href="https://chatgpt.com/codex/deeplink?prompt=IMPORTANT%3A%20Work%20in%20the%20repository%20%22scaleapi%2Fscale-agentex%22%20on%20the%20existing%20branch%20%22dhruv%2Fagx1-263-agent-api-keys-route-migration%22.%20Checkout%20that%20branch%20%E2%80%94%20do%20NOT%20create%20a%20new%20branch%20or%20open%20a%20new%20PR.%20Push%20your%20changes%20to%20%22dhruv%2Fagx1-263-agent-api-keys-route-migration%22.%0A%0AThis%20is%20a%20comment%20left%20during%20a%20code%20review.%0APath%3A%20agentex%2Fsrc%2Fapi%2Froutes%2Fagent_api_keys.py%0ALine%3A%20163-179%0A%0AComment%3A%0A**Distinguishable%20404%20bodies%20defeat%20the%20existence-leak%20collapse**%0A%0AThe%20name%20routes%20produce%20two%20structurally%20different%20404%20responses%20depending%20on%20whether%20the%20key%20exists%20in%20the%20database.%20If%20the%20key%20is%20absent%2C%20%60HTTPException%60%20is%20raised%20with%20%60%22Agent%20api_key%20'%7Bname%7D'%20not%20found%20for%20agent%20ID%20%7Bagent.id%7D%22%60.%20If%20the%20key%20exists%20but%20the%20caller%20is%20denied%2C%20%60ItemDoesNotExist%60%20is%20raised%20with%20%60%22Item%20with%20id%20'%7Bapi_key_id%7D'%20does%20not%20exist.%22%60.%20Both%20return%20HTTP%20404%2C%20but%20a%20cross-tenant%20caller%20who%20can%20resolve%20an%20%60agent_id%60%20can%20probe%20for%20key-name%20existence%20by%20comparing%20the%20response%20body%20%E2%80%94%20exactly%20the%20information%20leak%20the%20collapse%20is%20meant%20to%20prevent.%20The%20same%20issue%20exists%20in%20%60delete_agent_api_key_by_name%60%20%28line%20~264%29.%20To%20close%20it%2C%20both%20%22not%20found%22%20paths%20should%20produce%20an%20identical%20detail%20string%20%28e.g.%20both%20raise%20%60ItemDoesNotExist%60%20with%20the%20same%20generic%20message%29.%0A%0AHow%20can%20I%20resolve%20this%3F%20If%20you%20propose%20a%20fix%2C%20please%20make%20it%20concise."><picture><source media="(prefers-color-scheme: dark)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixInCodexDark.svg?v=3"><source media="(prefers-color-scheme: light)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixInCodex.svg?v=3"><img alt="Fix in Codex" src="https://greptile-static-assets.s3.amazonaws.com/badges/FixInCodex.svg?v=3" height="20"></picture></a> 2. `agentex/src/api/routes/agent_api_keys.py`, line 260-278 ([link](https://github.com/scaleapi/scale-agentex/blob/4335ca721893fc121ba8209b05ee1dcc6ef9d7b5/agentex/src/api/routes/agent_api_keys.py#L260-L278)) <a href="#"><img alt="P2" src="https://greptile-static-assets.s3.amazonaws.com/badges/p2.svg?v=9" align="top"></a> **TOCTOU window between auth check and delete-by-name** The auth check resolves the key to an `id` and passes that ID to SpiceDB, but the subsequent delete is issued by name (`delete_by_agent_id_and_key_name`). If the key is deleted and a new key with the same name is created between the lookup and the delete, the auth check will have evaluated the old key's ID (which the caller may have legitimately had delete rights on) but the mutation will affect the new key. The practical risk is low, but it is a real race between a non-atomic read-auth-mutate sequence. Deleting by the resolved `id` instead of by name would close it. <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: agentex/src/api/routes/agent_api_keys.py Line: 260-278 Comment: **TOCTOU window between auth check and delete-by-name** The auth check resolves the key to an `id` and passes that ID to SpiceDB, but the subsequent delete is issued by name (`delete_by_agent_id_and_key_name`). If the key is deleted and a new key with the same name is created between the lookup and the delete, the auth check will have evaluated the old key's ID (which the caller may have legitimately had delete rights on) but the mutation will affect the new key. The practical risk is low, but it is a real race between a non-atomic read-auth-mutate sequence. Deleting by the resolved `id` instead of by name would close it. How can I resolve this? If you propose a fix, please make it concise. ````` </details> <a href="https://app.greptile.com/api/ide/cursor?prompt=This%20is%20a%20comment%20left%20during%20a%20code%20review.%0APath%3A%20agentex%2Fsrc%2Fapi%2Froutes%2Fagent_api_keys.py%0ALine%3A%20260-278%0A%0AComment%3A%0A**TOCTOU%20window%20between%20auth%20check%20and%20delete-by-name**%0A%0AThe%20auth%20check%20resolves%20the%20key%20to%20an%20%60id%60%20and%20passes%20that%20ID%20to%20SpiceDB%2C%20but%20the%20subsequent%20delete%20is%20issued%20by%20name%20%28%60delete_by_agent_id_and_key_name%60%29.%20If%20the%20key%20is%20deleted%20and%20a%20new%20key%20with%20the%20same%20name%20is%20created%20between%20the%20lookup%20and%20the%20delete%2C%20the%20auth%20check%20will%20have%20evaluated%20the%20old%20key's%20ID%20%28which%20the%20caller%20may%20have%20legitimately%20had%20delete%20rights%20on%29%20but%20the%20mutation%20will%20affect%20the%20new%20key.%20The%20practical%20risk%20is%20low%2C%20but%20it%20is%20a%20real%20race%20between%20a%20non-atomic%20read-auth-mutate%20sequence.%20Deleting%20by%20the%20resolved%20%60id%60%20instead%20of%20by%20name%20would%20close%20it.%0A%0AHow%20can%20I%20resolve%20this%3F%20If%20you%20propose%20a%20fix%2C%20please%20make%20it%20concise.&pr=252&platform=github"><picture><source media="(prefers-color-scheme: dark)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixInCursorDark.svg?v=3"><source media="(prefers-color-scheme: light)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixInCursor.svg?v=3"><img alt="Fix in Cursor" src="https://greptile-static-assets.s3.amazonaws.com/badges/FixInCursor.svg?v=3" height="20"></picture></a> <a href="https://app.greptile.com/ide/claude-code?prompt=This%20is%20a%20comment%20left%20during%20a%20code%20review.%0APath%3A%20agentex%2Fsrc%2Fapi%2Froutes%2Fagent_api_keys.py%0ALine%3A%20260-278%0A%0AComment%3A%0A**TOCTOU%20window%20between%20auth%20check%20and%20delete-by-name**%0A%0AThe%20auth%20check%20resolves%20the%20key%20to%20an%20%60id%60%20and%20passes%20that%20ID%20to%20SpiceDB%2C%20but%20the%20subsequent%20delete%20is%20issued%20by%20name%20%28%60delete_by_agent_id_and_key_name%60%29.%20If%20the%20key%20is%20deleted%20and%20a%20new%20key%20with%20the%20same%20name%20is%20created%20between%20the%20lookup%20and%20the%20delete%2C%20the%20auth%20check%20will%20have%20evaluated%20the%20old%20key's%20ID%20%28which%20the%20caller%20may%20have%20legitimately%20had%20delete%20rights%20on%29%20but%20the%20mutation%20will%20affect%20the%20new%20key.%20The%20practical%20risk%20is%20low%2C%20but%20it%20is%20a%20real%20race%20between%20a%20non-atomic%20read-auth-mutate%20sequence.%20Deleting%20by%20the%20resolved%20%60id%60%20instead%20of%20by%20name%20would%20close%20it.%0A%0AHow%20can%20I%20resolve%20this%3F%20If%20you%20propose%20a%20fix%2C%20please%20make%20it%20concise.&repo=scaleapi%2Fscale-agentex&pr=252&platform=github"><picture><source media="(prefers-color-scheme: dark)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixInClaudeDark.svg?v=3"><source media="(prefers-color-scheme: light)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixInClaude.svg?v=3"><img alt="Fix in Claude Code" src="https://greptile-static-assets.s3.amazonaws.com/badges/FixInClaude.svg?v=3" height="20"></picture></a> <a href="https://chatgpt.com/codex/deeplink?prompt=IMPORTANT%3A%20Work%20in%20the%20repository%20%22scaleapi%2Fscale-agentex%22%20on%20the%20existing%20branch%20%22dhruv%2Fagx1-263-agent-api-keys-route-migration%22.%20Checkout%20that%20branch%20%E2%80%94%20do%20NOT%20create%20a%20new%20branch%20or%20open%20a%20new%20PR.%20Push%20your%20changes%20to%20%22dhruv%2Fagx1-263-agent-api-keys-route-migration%22.%0A%0AThis%20is%20a%20comment%20left%20during%20a%20code%20review.%0APath%3A%20agentex%2Fsrc%2Fapi%2Froutes%2Fagent_api_keys.py%0ALine%3A%20260-278%0A%0AComment%3A%0A**TOCTOU%20window%20between%20auth%20check%20and%20delete-by-name**%0A%0AThe%20auth%20check%20resolves%20the%20key%20to%20an%20%60id%60%20and%20passes%20that%20ID%20to%20SpiceDB%2C%20but%20the%20subsequent%20delete%20is%20issued%20by%20name%20%28%60delete_by_agent_id_and_key_name%60%29.%20If%20the%20key%20is%20deleted%20and%20a%20new%20key%20with%20the%20same%20name%20is%20created%20between%20the%20lookup%20and%20the%20delete%2C%20the%20auth%20check%20will%20have%20evaluated%20the%20old%20key's%20ID%20%28which%20the%20caller%20may%20have%20legitimately%20had%20delete%20rights%20on%29%20but%20the%20mutation%20will%20affect%20the%20new%20key.%20The%20practical%20risk%20is%20low%2C%20but%20it%20is%20a%20real%20race%20between%20a%20non-atomic%20read-auth-mutate%20sequence.%20Deleting%20by%20the%20resolved%20%60id%60%20instead%20of%20by%20name%20would%20close%20it.%0A%0AHow%20can%20I%20resolve%20this%3F%20If%20you%20propose%20a%20fix%2C%20please%20make%20it%20concise."><picture><source media="(prefers-color-scheme: dark)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixInCodexDark.svg?v=3"><source media="(prefers-color-scheme: light)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixInCodex.svg?v=3"><img alt="Fix in Codex" src="https://greptile-static-assets.s3.amazonaws.com/badges/FixInCodex.svg?v=3" height="20"></picture></a> </details> <!-- /greptile_failed_comments --> <a href="https://app.greptile.com/api/ide/cursor?prompt=Fix%20the%20following%202%20code%20review%20issues.%20Work%20through%20them%20one%20at%20a%20time%2C%20proposing%20concise%20fixes.%0A%0A---%0A%0A%23%23%23%20Issue%201%20of%202%0Aagentex%2Ftests%2Funit%2Fapi%2Ftest_agent_api_keys_authz.py%3A195-213%0A**Vacuous%20assertion%20on%20removed%20method**%0A%0A%60api_key_use_case.delete_by_agent_id_and_key_name.assert_not_called%28%29%60%20is%20dead%20code%20%E2%80%94%20%60delete_agent_api_key_by_name%60%20no%20longer%20calls%20%60delete_by_agent_id_and_key_name%60%3B%20it%20calls%20%60agent_api_key_use_case.delete%28id%3D...%29%60.%20The%20comment%20says%20%22the%20delete%20is%20NOT%20invoked%20when%20the%20check%20fails%2C%22%20but%20the%20assertion%20doesn't%20test%20that.%20If%20someone%20accidentally%20removed%20the%20%60_check_api_key_or_collapse_to_404%60%20guard%2C%20this%20assertion%20would%20still%20pass.%20Add%20%60api_key_use_case.delete%20%3D%20AsyncMock%28%29%60%20in%20the%20setup%20and%20assert%20%60api_key_use_case.delete.assert_not_called%28%29%60%20to%20test%20the%20intended%20safety%20property.%0A%0A%23%23%23%20Issue%202%20of%202%0Aagentex%2Fsrc%2Futils%2Fagent_api_key_authorization.py%3A13-17%0AMissing%20type%20annotation%20on%20%60authorization%60%20parameter%20%E2%80%94%20the%20other%20parameters%20are%20typed%20but%20this%20one%20isn't%2C%20so%20type-checkers%20won't%20catch%20callers%20passing%20the%20wrong%20type.%20The%20concrete%20type%20is%20%60DAuthorizationService%60%20%28matching%20every%20call%20site%20in%20this%20PR%29.%0A%0A%60%60%60suggestion%0Aasync%20def%20_check_api_key_or_collapse_to_404%28%0A%20%20%20%20authorization%3A%20%22DAuthorizationService%22%2C%0A%20%20%20%20api_key_id%3A%20str%2C%0A%20%20%20%20operation%3A%20AuthorizedOperationType%2C%0A%29%20-%3E%20None%3A%0A%60%60%60%0A%0A&pr=252&platform=github"><picture><source media="(prefers-color-scheme: dark)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCursorDark.svg?v=3"><source media="(prefers-color-scheme: light)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCursor.svg?v=3"><img alt="Fix All in Cursor" src="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCursor.svg?v=3" height="20"></picture></a> <a href="https://app.greptile.com/ide/claude-code?prompt=Fix%20the%20following%202%20code%20review%20issues.%20Work%20through%20them%20one%20at%20a%20time%2C%20proposing%20concise%20fixes.%0A%0A---%0A%0A%23%23%23%20Issue%201%20of%202%0Aagentex%2Ftests%2Funit%2Fapi%2Ftest_agent_api_keys_authz.py%3A195-213%0A**Vacuous%20assertion%20on%20removed%20method**%0A%0A%60api_key_use_case.delete_by_agent_id_and_key_name.assert_not_called%28%29%60%20is%20dead%20code%20%E2%80%94%20%60delete_agent_api_key_by_name%60%20no%20longer%20calls%20%60delete_by_agent_id_and_key_name%60%3B%20it%20calls%20%60agent_api_key_use_case.delete%28id%3D...%29%60.%20The%20comment%20says%20%22the%20delete%20is%20NOT%20invoked%20when%20the%20check%20fails%2C%22%20but%20the%20assertion%20doesn't%20test%20that.%20If%20someone%20accidentally%20removed%20the%20%60_check_api_key_or_collapse_to_404%60%20guard%2C%20this%20assertion%20would%20still%20pass.%20Add%20%60api_key_use_case.delete%20%3D%20AsyncMock%28%29%60%20in%20the%20setup%20and%20assert%20%60api_key_use_case.delete.assert_not_called%28%29%60%20to%20test%20the%20intended%20safety%20property.%0A%0A%23%23%23%20Issue%202%20of%202%0Aagentex%2Fsrc%2Futils%2Fagent_api_key_authorization.py%3A13-17%0AMissing%20type%20annotation%20on%20%60authorization%60%20parameter%20%E2%80%94%20the%20other%20parameters%20are%20typed%20but%20this%20one%20isn't%2C%20so%20type-checkers%20won't%20catch%20callers%20passing%20the%20wrong%20type.%20The%20concrete%20type%20is%20%60DAuthorizationService%60%20%28matching%20every%20call%20site%20in%20this%20PR%29.%0A%0A%60%60%60suggestion%0Aasync%20def%20_check_api_key_or_collapse_to_404%28%0A%20%20%20%20authorization%3A%20%22DAuthorizationService%22%2C%0A%20%20%20%20api_key_id%3A%20str%2C%0A%20%20%20%20operation%3A%20AuthorizedOperationType%2C%0A%29%20-%3E%20None%3A%0A%60%60%60%0A%0A&repo=scaleapi%2Fscale-agentex&pr=252&platform=github"><picture><source media="(prefers-color-scheme: dark)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInClaudeDark.svg?v=3"><source media="(prefers-color-scheme: light)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInClaude.svg?v=3"><img alt="Fix All in Claude Code" src="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInClaude.svg?v=3" height="20"></picture></a> <a href="https://chatgpt.com/codex/deeplink?prompt=IMPORTANT%3A%20Work%20in%20the%20repository%20%22scaleapi%2Fscale-agentex%22%20on%20the%20existing%20branch%20%22dhruv%2Fagx1-263-agent-api-keys-route-migration%22.%20Checkout%20that%20branch%20%E2%80%94%20do%20NOT%20create%20a%20new%20branch%20or%20open%20a%20new%20PR.%20Push%20your%20changes%20to%20%22dhruv%2Fagx1-263-agent-api-keys-route-migration%22.%0A%0AFix%20the%20following%202%20code%20review%20issues.%20Work%20through%20them%20one%20at%20a%20time%2C%20proposing%20concise%20fixes.%0A%0A---%0A%0A%23%23%23%20Issue%201%20of%202%0Aagentex%2Ftests%2Funit%2Fapi%2Ftest_agent_api_keys_authz.py%3A195-213%0A**Vacuous%20assertion%20on%20removed%20method**%0A%0A%60api_key_use_case.delete_by_agent_id_and_key_name.assert_not_called%28%29%60%20is%20dead%20code%20%E2%80%94%20%60delete_agent_api_key_by_name%60%20no%20longer%20calls%20%60delete_by_agent_id_and_key_name%60%3B%20it%20calls%20%60agent_api_key_use_case.delete%28id%3D...%29%60.%20The%20comment%20says%20%22the%20delete%20is%20NOT%20invoked%20when%20the%20check%20fails%2C%22%20but%20the%20assertion%20doesn't%20test%20that.%20If%20someone%20accidentally%20removed%20the%20%60_check_api_key_or_collapse_to_404%60%20guard%2C%20this%20assertion%20would%20still%20pass.%20Add%20%60api_key_use_case.delete%20%3D%20AsyncMock%28%29%60%20in%20the%20setup%20and%20assert%20%60api_key_use_case.delete.assert_not_called%28%29%60%20to%20test%20the%20intended%20safety%20property.%0A%0A%23%23%23%20Issue%202%20of%202%0Aagentex%2Fsrc%2Futils%2Fagent_api_key_authorization.py%3A13-17%0AMissing%20type%20annotation%20on%20%60authorization%60%20parameter%20%E2%80%94%20the%20other%20parameters%20are%20typed%20but%20this%20one%20isn't%2C%20so%20type-checkers%20won't%20catch%20callers%20passing%20the%20wrong%20type.%20The%20concrete%20type%20is%20%60DAuthorizationService%60%20%28matching%20every%20call%20site%20in%20this%20PR%29.%0A%0A%60%60%60suggestion%0Aasync%20def%20_check_api_key_or_collapse_to_404%28%0A%20%20%20%20authorization%3A%20%22DAuthorizationService%22%2C%0A%20%20%20%20api_key_id%3A%20str%2C%0A%20%20%20%20operation%3A%20AuthorizedOperationType%2C%0A%29%20-%3E%20None%3A%0A%60%60%60%0A%0A"><picture><source media="(prefers-color-scheme: dark)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodexDark.svg?v=3"><source media="(prefers-color-scheme: light)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodex.svg?v=3"><img alt="Fix All in Codex" src="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodex.svg?v=3" height="20"></picture></a> <details><summary>Prompt To Fix All With AI</summary> `````markdown Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes. --- ### Issue 1 of 2 agentex/tests/unit/api/test_agent_api_keys_authz.py:195-213 **Vacuous assertion on removed method** `api_key_use_case.delete_by_agent_id_and_key_name.assert_not_called()` is dead code — `delete_agent_api_key_by_name` no longer calls `delete_by_agent_id_and_key_name`; it calls `agent_api_key_use_case.delete(id=...)`. The comment says "the delete is NOT invoked when the check fails," but the assertion doesn't test that. If someone accidentally removed the `_check_api_key_or_collapse_to_404` guard, this assertion would still pass. Add `api_key_use_case.delete = AsyncMock()` in the setup and assert `api_key_use_case.delete.assert_not_called()` to test the intended safety property. ### Issue 2 of 2 agentex/src/utils/agent_api_key_authorization.py:13-17 Missing type annotation on `authorization` parameter — the other parameters are typed but this one isn't, so type-checkers won't catch callers passing the wrong type. The concrete type is `DAuthorizationService` (matching every call site in this PR). ```suggestion async def _check_api_key_or_collapse_to_404( authorization: "DAuthorizationService", api_key_id: str, operation: AuthorizedOperationType, ) -> None: ``` ````` </details> <sub>Reviews (4): Last reviewed commit: ["refactor(AGX1-263): close delete-by-name..."](29a822a) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=34413167)</sub> <!-- /greptile_comment --> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
asherfink
added a commit
that referenced
this pull request
Jun 3, 2026
…strings (#268) Scrubs internal references (Linear ticket IDs, internal service names, internal flag names) from comments and docstrings across already-landed authorization code. Fixes issues introduced by #248, #252, #255, #257, #259, and #260. No behavior changes. <!-- greptile_comment --> <h3>Greptile Summary</h3> This PR is a pure housekeeping change that scrubs internal references — Linear ticket IDs (e.g. `AGX1-263`, `AGX1-290`), internal service names (`SpiceDB`, `Spark`, `agentex-auth`), and internal flag/PR references — from comments and docstrings across 13 authorization-related source and test files. No executable code is touched. - Comments and docstrings across `port.py`, `authorization_service.py`, `agent_api_keys.py`, and related utilities have internal service names replaced with generic terms like \"authorization service\" or \"authorization schema\". - TODO comments in `agent_api_key_authorization.py`, `agent_authorization.py`, and `authorization_shortcuts.py` have internal ticket IDs stripped, with file-level context substituted where the ticket reference previously identified the work location. - Module docstrings in all six affected test files have ticket-prefixed deliverable descriptions replaced with plain behavioral descriptions. <details><summary><h3>Confidence Score: 5/5</h3></summary> All changes are confined to comments and docstrings; no executable code is modified. Every diff hunk touches only comment text, docstrings, or module-level strings. There are no logic, control-flow, or interface changes anywhere in the PR, making regression risk essentially zero. No files require special attention. </details> <h3>Important Files Changed</h3> | Filename | Overview | |----------|----------| | agentex/src/adapters/authorization/port.py | Docstring-only: replaced "SpiceDB" with "authorization" in register_resource docstring. | | agentex/src/api/routes/agent_api_keys.py | Comment-only: removed "SpiceDB" references from two inline comments in create/delete route handlers. | | agentex/src/domain/services/authorization_service.py | Docstring-only: replaced "SpiceDB schema" with "authorization schema" in register_resource docstring. | | agentex/src/domain/use_cases/agent_api_keys_use_case.py | Comment-only: removed unconditional-routing comment referencing internal service names and ticket numbers; also cleaned docstring referencing internal routing logic. | | agentex/src/utils/agent_api_key_authorization.py | Comment-only: replaced TODO(AGX1-290) with plain TODO, removing the Linear ticket reference. | | agentex/src/utils/agent_authorization.py | Comment-only: replaced TODO(AGX1-290) with plain TODO, removing the Linear ticket reference. | | agentex/src/utils/authorization_shortcuts.py | Comment-only: updated TODO to reference the file name instead of internal ticket/PR numbers. | | agentex/tests/integration/services/test_agent_api_key_service_dual_write.py | Docstring-only: removed references to "Spark AuthZ", internal ticket numbers, and internal service names throughout module docstring and inline comments. | | agentex/tests/integration/services/test_schedule_service_dual_write.py | Docstring-only: removed "Spark AuthZ" and "SpiceDB" references across the module docstring and inline comments. | | agentex/tests/integration/api/checkpoints/test_checkpoints_authz_api.py | Docstring-only: removed AGX1-302 ticket reference and internal terminology from module docstring. | | agentex/tests/integration/api/events/test_events_authz_api.py | Docstring-only: replaced "AGX1-244" ticket reference in module docstring with a plain description. | | agentex/tests/integration/api/messages/test_messages_authz_api.py | Docstring-only: removed AGX1-277 ticket reference from module docstring. | | agentex/tests/unit/api/test_agent_api_keys_authz.py | Docstring-only: removed AGX1-263 ticket reference and "Spark AuthZ" mention from module docstring. | </details> <details><summary><h3>Flowchart</h3></summary> ```mermaid %%{init: {'theme': 'neutral'}}%% flowchart TD A["PR #268: Scrub internal references"] --> B["Source files\n(port.py, agent_api_keys.py,\nauthorization_service.py,\nagent_api_keys_use_case.py,\n3 utils files)"] A --> C["Test files\n(6 integration + unit tests)"] B --> D["Replace: SpiceDB → authorization service/schema\nRemove: agentex-auth, Spark, internal PR refs\nStrip: AGX1-xxx ticket IDs from TODOs"] C --> E["Replace: AGX1-xxx deliverable labels → plain descriptions\nRemove: Spark AuthZ, scaleapi/agentex#353, etc."] D --> F["No executable code changed"] E --> F ``` </details> <sub>Reviews (2): Last reviewed commit: ["chore(authz): scrub internal project ref..."](62581f7) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=35250665)</sub> <!-- /greptile_comment -->
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
Wires
register_resource/deregister_resourceintoAgentAPIKeysUseCase.create/deleteso api_keys get a SpiceDB tuple (withparent_agentedge) on create and have that tuple removed on delete. Companion to the route-migration PR (#252).scale-agentex is intentionally decoupled from egp-api-backend. The dual-write call is unconditional from this repo. agentex-auth's per-account routing (scaleapi/agentex#353,
FGAC_AGENTEX_AUTH_SPARK) decides whether the call lands on Spark AuthZ or falls back to the legacy SGP backend for the caller's account.Linear ticket: https://linear.app/scale-epd/issue/AGX1-272/fgac-agent-api-key-dual-write-registerderegister-on-createdelete
Cross-repo stack
parent_resourcekwarg/v1/authz/register,/v1/authz/deregister) +api_keymappingFGAC_AGENTEX_AUTH_SPARKflagregister_resource(parent=agent)Changes
Port + adapter
src/adapters/authorization/port.py— abstractregister_resource(resource, parent=None)andderegister_resource(resource)onAuthorizationGateway.src/adapters/authorization/adapter_agentex_authz_proxy.py— POST/v1/authz/registerand/v1/authz/deregisterto agentex-auth (endpoints exposed by #354).Service layer
src/domain/services/authorization_service.py—register_resource/deregister_resourceservice methods mirror the grant/revoke pattern (principal_contextoverride,_bypasssupport, parent identity in log line).Use case
src/domain/use_cases/agent_api_keys_use_case.py:createcalls_register_api_key_in_authunconditionally;parent=AgentexResource.agent(agent_id)is load-bearing for the SpiceDB cascade.delete(anddelete_by_*variants) call_deregister_api_key_from_authafter the Postgres row is gone.principal_contextat call time — no persisted creator columns.Tests
tests/integration/services/test_agent_api_key_service_dual_write.py— 6 cases. Mocks the authorization service; asserts the call sequencing inside the use case.tests/integration/fixtures/integration_client.py+tests/unit/use_cases/test_agents_api_keys_use_case.pyupdated for the new constructor signature.Test plan
test_create_api_key_calls_register_resource_with_parentassertsparent=AgentexResource.agent(agent.id)is passed on every register call.test_agents_api_keys_use_case.pypredate this PR (verified viagit stash).Diff scope
10 files changed, 55 insertions(+), 368 deletions(-)— net deletion. Earlier iterations had ~370 lines of OSS-coupling that Harvey's review pruned.Out of scope / follow-ups
spark_authz_zedtoken).task,build,deployment,schedule). Each owns its own follow-up.Linked: AGX1-272.
Greptile Summary
This PR wires
register_resource/deregister_resourceintoAgentAPIKeysUseCase.createand the three delete methods, so everyagent_api_keygets a SpiceDB tuple (with aparent_agentedge) on creation and has it removed on deletion. The dual-write is unconditional in this repo; per-account routing to Spark AuthZ vs. the legacy SGP backend is delegated to agentex-auth.AuthorizationGatewaygains abstractregister_resource/deregister_resource;AgentexAuthorizationProxyimplements them via POST to/v1/authz/registerand/v1/authz/deregister.AuthorizationService.register_resourceandderegister_resourcemirror the existinggrant/revokepattern —_bypass()guard,principal_contextoverride, structured log line.createcalls_register_api_key_in_auth(fail-closed, before the Postgres write) withparent=AgentexResource.agent(agent_id). All three delete paths call_deregister_api_key_from_auth(best-effort, after the DB delete) using the entity ID retrieved by a pre-fetch. A no-creator guard skips registration when neitheruser_idnorservice_account_idis resolvable on the principal.Confidence Score: 5/5
Safe to merge. The dual-write is unconditional and well-guarded: register fails closed (Postgres row never written on Spark failure), deregister is best-effort (delete completes regardless), and the no-creator guard prevents registration without an owner identity.
The create path correctly sequences auth registration before the DB write and re-raises on failure. The delete paths pre-fetch the entity ID, complete the DB delete, then attempt deregistration with swallowed errors. Bypass semantics and principal-context plumbing are consistent with the existing grant/revoke pattern.
No files require special attention. The integration test file has a minor coverage gap for
delete_by_agent_name_and_key_name, but the code itself is correct.Important Files Changed
_register_api_key_in_auth(fail-closed, pre-DB) and_deregister_api_key_from_auth(best-effort, post-DB) helpers; all three delete methods updated to pre-fetch the entity ID and call deregister. Logic is sound; no blocking issues.register_resourceandderegister_resourceservice methods following the established grant/revoke pattern; bypass and principal-override semantics are consistent with existing methods.register_resource(POST/v1/authz/register) andderegister_resource(POST/v1/authz/deregister) to the proxy adapter; payload serialization mirrors the existing grant/revoke pattern.register_resourceandderegister_resourcetoAuthorizationGateway; docstrings clearly explain the SpiceDB cascade contract.delete_by_agent_name_and_key_namepath.authorization_service(withAsyncMockfor bothregister_resourceandderegister_resource), matching the integration client setup.noop_authorization_servicemock with all requiredAsyncMockmethods and passes it toAgentAPIKeysUseCase; consistent with the unit test fixture pattern.Sequence Diagram
sequenceDiagram participant C as Caller participant UC as AgentAPIKeysUseCase participant AS as AuthorizationService participant GW as AgentexAuthorizationProxy participant DB as Postgres participant AX as agentex-auth Note over UC,AX: create() C->>UC: create(name, agent_id, api_key_type, api_key) UC->>DB: agent_repo.get(agent_id) DB-->>UC: AgentEntity UC->>UC: _register_api_key_in_auth(api_key_id, agent_id) UC->>AS: "register_resource(resource=api_key, parent=agent)" AS->>GW: register_resource(principal, resource, parent) GW->>AX: POST /v1/authz/register AX-->>GW: 200 OK GW-->>AS: None AS-->>UC: None UC->>DB: agent_api_key_repo.create(entity) DB-->>UC: AgentAPIKeyEntity UC-->>C: AgentAPIKeyEntity Note over UC,AX: delete() / delete_by_*() C->>UC: "delete(id) / delete_by_*(...)" UC->>DB: agent_api_key_repo.get(id) DB-->>UC: "AgentAPIKeyEntity (or ItemDoesNotExist -> return)" UC->>DB: agent_api_key_repo.delete(id) DB-->>UC: None UC->>UC: _deregister_api_key_from_auth(api_key_id) UC->>AS: "deregister_resource(resource=api_key)" AS->>GW: deregister_resource(principal, resource) GW->>AX: POST /v1/authz/deregister AX-->>GW: 200 OK (failure: logged, ignored) GW-->>AS: None AS-->>UC: None UC-->>C: NoneComments Outside Diff (1)
agentex/tests/integration/services/test_agent_api_key_service_dual_write.py, line 703-727 (link)The suite covers
grantfailure preventing the DB write, but not the reverse:grantsucceeding followed byrepo.createraising (e.g., a duplicate key or transient DB error). In that case a Spark tuple exists for anapi_key_idthat never lands in Postgres. Adding a test for this case would make the documented known-limitation explicit and guard against accidental regression if compensating logic is added later.Prompt To Fix With AI
Prompt To Fix All With AI
Reviews (9): Last reviewed commit: "Merge branch 'main' into dhruv/agx1-272-..." | Re-trigger Greptile