Skip to content

feat(ui): add scheduled task management - #354

Merged
jromualdez-scale merged 18 commits into
mainfrom
jerome/scheduled-tasks-ui
Jul 20, 2026
Merged

feat(ui): add scheduled task management#354
jromualdez-scale merged 18 commits into
mainfrom
jerome/scheduled-tasks-ui

Conversation

@jromualdez-scale

@jromualdez-scale jromualdez-scale commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add a Scheduled Tasks experience with direct creation, a consolidated cadence editor, active/paused controls, edit flows, irreversible-delete confirmation, and a disabled “Snooze — coming soon” affordance.
  • Add an occurrence-focused Upcoming timeline with current/all-agent scope, synchronized task-sidebar filtering, balanced previews for frequent schedules, run-now flows, and exact skip/unskip actions.
  • Use the generated TypeScript schedule SDK, label scheduled runs consistently in the task sidebar, and keep scheduled task titles stable without appending occurrence timestamps.

UX details

  • Create schedules immediately without a confirmation step; show animated pending/success feedback with Edit and auto-dismiss.
  • Group upcoming runs into Today, Tomorrow, and Later; show 10 by default with at most 3 per schedule, then allow expansion to the 10 occurrences returned per schedule.
  • Keep occurrence actions scoped to Skip/Unskip and Edit schedule; keep Pause/Resume, Delete, Run now, and Skip next run in the Schedules view.
  • Keep schedule names consistent with agent naming semantics: auto-generated kebab-case names are displayed and copied to scheduled task metadata.

Test plan

  • npm --prefix agentex-ui run typecheck
  • npm --prefix agentex-ui run lint
  • npm --prefix agentex-ui run test:run -- hooks/use-safe-search-params.test.tsx lib/agent-run-schedules.test.ts lib/schedule-utils.test.ts
  • Local smoke test with two registered agents, cross-agent schedule scope, upcoming schedule details, and schedule actions
  • Pre-commit Agentex UI lint hooks
Screen.Recording.2026-07-13.at.2.25.20.PM.mov

Greptile Summary

This PR delivers a full Scheduled Tasks UI surface: inline schedule creation with animated feedback, an Upcoming timeline grouped by Today/Tomorrow/Later, a Schedules list with pause/resume toggle, skip/unskip per-occurrence, run-now flows, edit and delete modals, and a feature flag (ENABLE_AGENT_RUN_SCHEDULES) that gates the entire experience.

  • New agentex-ui/components/scheduled-tasks/ module (~1 400 lines) provides the page, composer, cadence picker, upcoming list, schedules list, modals, and shared helpers; routing is handled entirely via URL search params (view, schedule_scope, agent_name).
  • lib/schedule-utils.ts owns cadence serialization, validation, display, and the scheduleToCadence round-trip used by the edit form.
  • agentex/src/temporal/activities/scheduled_agent_run_activities.py removes _format_fire_time and the per-occurrence timestamp from task_metadata.display_name, keeping task names stable across retries.

Confidence Score: 5/5

Safe to merge; the feature is flag-gated and all CRUD flows, cache invalidation, and error paths look correct.

Additive, flag-gated change. TanStack Query cache keys are well-structured, mutation error handling surfaces toasts via onError, and search-param routing clears stale VIEW values on task selection. The two findings only affect externally-created sub-minute or complex-cron schedules that the UI itself never produces.

agentex-ui/lib/schedule-utils.ts — specifically scheduleToCadence (sub-minute round-trip) and describeCadence (missing complex-cron fallback).

Important Files Changed

Filename Overview
agentex-ui/lib/schedule-utils.ts New utility module for cadence serialization, validation, and display. scheduleToCadence silently coerces sub-minute interval_seconds (e.g. 30 s → 1 min) and describeCadence lacks the raw-expression fallback its inline comment promises for complex external crons.
agentex-ui/hooks/use-agent-run-schedules.ts New TanStack Query hooks for schedule CRUD and actions. Cache key structure is consistent, invalidation logic is correct, and error toasts are surfaced via onError. Limit is 50 and is surfaced in the UI.
agentex-ui/components/scheduled-tasks/scheduled-tasks-page.tsx Main page orchestrating scope/agent/view state. Correctly wires search-param-driven scope selection and shows ScheduleComposer only for the current-agent scope.
agentex-ui/components/scheduled-tasks/all-schedules-list.tsx Schedule list with inline pause/resume toggle and overflow menu. Functionally correct; idempotent API calls handle the case where row-level and menu-level mutation pending states don't reflect each other.
agentex-ui/components/scheduled-tasks/upcoming-schedule-list.tsx Upcoming runs timeline with Today/Tomorrow/Later grouping. Run-now/skip sequential flow is handled correctly with .catch(() => undefined) to suppress uncaught-rejection noise while onError toasts surface failures.
agentex-ui/components/scheduled-tasks/schedule-modals.tsx Delete confirmation and edit modals. Name normalization/sanitization on blur, cadence round-trip via scheduleToCadence, and inline delete from the edit modal all look correct.
agentex/src/temporal/activities/scheduled_agent_run_activities.py Removes per-occurrence timestamp from display_name (now just schedule.name), removes the unused _format_fire_time helper, and keeps _extract_fire_time/regex for UI occurrence-time parsing.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Page Load] --> B{agentRunSchedulesEnabled?}
    B -->|false| C[Chat / Home only]
    B -->|true| D{view=scheduled_tasks?}
    D -->|no| E{taskID param?}
    E -->|yes| F[ChatView]
    E -->|no| G[HomeView]
    D -->|yes| H[ScheduledTasksPage]
    H --> I{scheduleScope?}
    I -->|ALL| J[useAgentRunSchedulesForAgents all agents]
    I -->|CURRENT| K{agent selected?}
    K -->|no| L[Empty State: Select an agent]
    K -->|yes| M[useAgentRunSchedules current agent]
    J --> N[useAgentRunScheduleDetailsForItems one req per schedule]
    M --> N
    N --> O{scheduleView?}
    O -->|upcoming| P[UpcomingScheduleList Today/Tomorrow/Later]
    O -->|all| Q[AllSchedulesList sorted by next run]
    M --> R[ScheduleComposer create new schedule]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Page Load] --> B{agentRunSchedulesEnabled?}
    B -->|false| C[Chat / Home only]
    B -->|true| D{view=scheduled_tasks?}
    D -->|no| E{taskID param?}
    E -->|yes| F[ChatView]
    E -->|no| G[HomeView]
    D -->|yes| H[ScheduledTasksPage]
    H --> I{scheduleScope?}
    I -->|ALL| J[useAgentRunSchedulesForAgents all agents]
    I -->|CURRENT| K{agent selected?}
    K -->|no| L[Empty State: Select an agent]
    K -->|yes| M[useAgentRunSchedules current agent]
    J --> N[useAgentRunScheduleDetailsForItems one req per schedule]
    M --> N
    N --> O{scheduleView?}
    O -->|upcoming| P[UpcomingScheduleList Today/Tomorrow/Later]
    O -->|all| Q[AllSchedulesList sorted by next run]
    M --> R[ScheduleComposer create new schedule]
Loading

Reviews (8): Last reviewed commit: "Merge branch 'main' into jerome/schedule..." | Re-trigger Greptile

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

✱ Stainless preview builds

This PR will update the agentex-sdk SDKs with the following commit messages.

openapi

feat(api): add skip/unskip/by-name methods, response fields to schedules

python

feat: Add scheduled tasks UI polish

typescript

feat(api): add num_tasks_created and skipped_action_times fields to schedules

Edit this comment to update them. They will appear in their respective SDK's changelogs.

agentex-sdk-openapi studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ❗

New diagnostics (2 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /agents/{agent_id}/schedules/{schedule_id}/skip`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /agents/{agent_id}/schedules/{schedule_id}/unskip`
agentex-sdk-typescript studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ❗build ✅lint ✅test ✅

npm install https://pkg.stainless.com/s/agentex-sdk-typescript/8cb79bd084af03a986befc8095d4a943d2c5301f/dist.tar.gz
New diagnostics (2 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /agents/{agent_id}/schedules/{schedule_id}/skip`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /agents/{agent_id}/schedules/{schedule_id}/unskip`
agentex-sdk-python studio · conflict

Your SDK build had at least one new note diagnostic, which is a regression from the base state.

New diagnostics (2 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /agents/{agent_id}/schedules/{schedule_id}/skip`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /agents/{agent_id}/schedules/{schedule_id}/unskip`

This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-07-09 17:13:15 UTC

@jromualdez-scale
jromualdez-scale force-pushed the jerome/scheduled-tasks-ui branch 2 times, most recently from 8445f04 to 0673c3d Compare July 9, 2026 17:05
jromualdez-scale and others added 6 commits July 10, 2026 13:00
Co-authored-by: Cursor <cursoragent@cursor.com>
Expose task-backed run counts and exact skip/unskip operations so schedule clients can reason about individual future occurrences.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add agent scoping, upcoming occurrence timelines, active toggles, run-now confirmation, and skipped occurrence controls to the scheduled tasks page.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use schedule metadata for scheduled-run task labels and add a calendar indicator so recurring runs are easier to recognize.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use the schedule name as the task display label and keep occurrence timing in metadata for views that need it.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@jromualdez-scale
jromualdez-scale force-pushed the jerome/scheduled-tasks-ui branch from 0673c3d to e36d4cd Compare July 10, 2026 17:40
Co-authored-by: Cursor <cursoragent@cursor.com>
@socket-security

socket-security Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatednpm/​agentex@​0.1.0-alpha.9 ⏵ 0.1.0-alpha.2078 -110099 +196 +5100

View full report

@jromualdez-scale jromualdez-scale changed the title Add scheduled tasks UI polish feat(ui): add scheduled task management Jul 13, 2026
@jromualdez-scale
jromualdez-scale marked this pull request as ready for review July 13, 2026 15:37
@jromualdez-scale
jromualdez-scale requested a review from a team as a code owner July 13, 2026 15:37
@jromualdez-scale
jromualdez-scale marked this pull request as draft July 13, 2026 15:38
Comment thread agentex-ui/components/scheduled-tasks/scheduled-tasks-page.tsx Outdated
Comment thread agentex-ui/hooks/use-agent-run-schedules.ts
Co-authored-by: Cursor <cursoragent@cursor.com>
@jromualdez-scale
jromualdez-scale marked this pull request as ready for review July 13, 2026 17:29
@jromualdez-scale
jromualdez-scale marked this pull request as draft July 13, 2026 17:31
jromualdez-scale and others added 2 commits July 13, 2026 14:09
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@jromualdez-scale
jromualdez-scale marked this pull request as ready for review July 13, 2026 18:23
Hide the scheduled tasks UI unless the schedules API is enabled so users do not navigate to endpoints that are absent at runtime.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread agentex-ui/components/task-sidebar/task-sidebar-header.tsx Outdated
Comment thread agentex-ui/hooks/use-agent-run-schedules.ts
Comment thread agentex-ui/lib/schedule-utils.ts
Comment thread agentex-ui/components/scheduled-tasks/scheduled-tasks-page.tsx Outdated
Comment thread agentex-ui/components/scheduled-tasks/scheduled-tasks-page.tsx Outdated
Comment thread agentex-ui/components/scheduled-tasks/scheduled-tasks-page.tsx

@declan-scale declan-scale left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran locally, looks good, just some organization / small ui improvements I'd suggest

jromualdez-scale and others added 2 commits July 14, 2026 21:59
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@jromualdez-scale

Copy link
Copy Markdown
Contributor Author

Ran locally, looks good, just some organization / small ui improvements I'd suggest

Thanks for the great comments @declan-scale I addressed them all

Comment thread agentex-ui/app/page.tsx
@jromualdez-scale
jromualdez-scale merged commit 0d859c2 into main Jul 20, 2026
32 checks passed
@jromualdez-scale
jromualdez-scale deleted the jerome/scheduled-tasks-ui branch July 20, 2026 10:00
Y4NN777 pushed a commit to Y4NN777/scale-agentex that referenced this pull request Aug 26, 2026
…+ two-factor) (scaleapi#252)

## Summary

Wires Spark AuthZ into all six `agent_api_keys` routes. Mirrors Asher's
task-route pattern from
[scaleapi#249](scaleapi#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
[scaleapi#248](scaleapi#248
`register_resource(parent=agent)`.

## Stack

| Repo | PR | State |
|---|---|---|
| scaleapi/scaleapi |
[#144657](scaleapi/scaleapi#144657) — sgp-authz
0.7.0 | ✅ Merged |
| scaleapi/agentex |
[scaleapi#354](scaleapi/agentex#354) — `api_key` mapping
| ✅ Merged |
| scaleapi/scale-agentex |
[scaleapi#248](scaleapi#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 scaleapi#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..."](scaleapi@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>
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.

2 participants