feat(scheduled-tasks): support MCP pre-approvals (#13884)

This commit is contained in:
Wenxi authored and GitHub committed 2026-08-12 19:08:05 +00:00
1 parent 8a5902a6a4
commit d98a5d855d
8 files changed
+365 -146

No files matched your search

@@ -42,7 +42,7 @@ Proxy runtime:
Approval persistence and API:
- `backend/onyx/db/models.py` defines `ActionApproval`,
`ExternalAppPolicy`, and `ScheduledTaskPreApprovedApp`.
`ExternalAppPolicy`, and `ScheduledTaskPreApprovedTarget`.
- `backend/onyx/db/enums.py` defines `EndpointPolicy`,
`ApprovalDecision`, and `ApprovalDecidedVia`.
- `backend/onyx/server/features/build/db/action_approval.py` owns approval DB
@@ -451,8 +451,8 @@ Partial coverage still parks.
### Scheduled Task Pre-Approvals
Scheduled task pre-approvals are another grant source inside the same proxy
approval path. A task can store a set of pre-approved external app ids in
`scheduled_task_pre_approved_app`.
approval path. A task can store external-app and MCP-server grants in the
legacy-named `scheduled_task_pre_approved_app` table.
For an `ASK` request, the gate checks grant sources in this order:
@@ -463,7 +463,7 @@ A scheduled-task grant applies only when:
- The `BuildSession` has a `ScheduledTaskRun` row.
- That run is currently `RUNNING`.
- The task has a grant for the matched `external_app_id`.
- The task has a grant for the matched `(target kind, target id)`.
When it applies, the proxy inserts an `action_approval` row already
`APPROVED` with `decided_via=PRE_APPROVAL`, emits a deduped scheduled-task
@@ -125,20 +125,22 @@ unguarded after an `APPROVED` row is already committed.
## Data Model
New table `scheduled_task_pre_approved_app` — one row per `(task, app)`
grant:
The legacy-named `scheduled_task_pre_approved_app` table stores one row per
`(task, target)` grant:
- `scheduled_task_id` → `scheduled_task.id` (`ON DELETE CASCADE`) and
`external_app_id` → `external_app.id` (`ON DELETE CASCADE`), with a
`UNIQUE(scheduled_task_id, external_app_id)` constraint that keeps
grants idempotent and serves the per-task lookup. The FKs give real
referential integrity — a grant can't point at a removed app, and
removing either side drops the grant. `ScheduledTask.pre_approved_apps`
is the ORM collection; `pre_approved_app_ids` is a read-only accessor
over it, so the API contract (`list[int]`) is unchanged. The write
path replaces the whole set (`set_pre_approved_apps`), validated
against the configured apps (via the tenant-scoped session) and
deduped order-preserving.
- `scheduled_task_id` references `scheduled_task.id` with `ON DELETE CASCADE`.
- `gated_app_id` references the polymorphic `gated_app.id` with
`ON DELETE CASCADE`. Each `gated_app` row identifies exactly one external app
or MCP server.
- `UNIQUE(scheduled_task_id, gated_app_id)` keeps grants idempotent and serves
the per-task lookup.
- `ScheduledTask.pre_approved_targets` is the ORM collection of
`ScheduledTaskPreApprovedTarget` rows. The
`pre_approved_external_app_ids` and `pre_approved_mcp_server_ids` properties
project each target kind into its API field.
- `_replace_pre_approved_targets` replaces each supplied target kind and
preserves omitted kinds. It deduplicates submitted IDs and reuses unchanged
rows.
- `action_approval.decided_via` — nullable (`user | pre_approval`,
NULL for legacy/expired rows): the audit marker distinguishing a
human click from a pre-approval. Kept separate from `decision` so
@@ -160,10 +162,10 @@ pre-decided inserts go through `insert_action_approval` in
- `ScheduledTaskCreate` / `ScheduledTaskPatch` gain
`pre_approved_app_ids: list[int]`; `ScheduledTaskDetail` returns it.
The write path validates ids via `_validated_app_ids` and dedupes
(order-preserving) — existence only; a credential / ≥1-`ASK` filter is
editor-side advisory, since a grant on a no-`ASK` app is inert and
never consulted.
The write path validates ids via `_validate_app_ids` and stores each grant
once. Grant order has no meaning. Validation checks existence only; a
credential / ≥1-`ASK` filter is editor-side advisory because a grant on a
no-`ASK` app is inert and never consulted.
- New `NotificationType.SCHEDULED_TASK_PRE_APPROVED_ACTION`, emitted
per `(run, app)` on the first unattended forward so chatty tasks
don't flood the bell. Dedup rides `create_notification`'s existing
@@ -254,7 +256,7 @@ The grant-source seam means future modes drop in as new
- Grant patch semantics: a prompt edit preserves grants, supplied
`pre_approved_app_ids` replaces the set, and re-submitting an existing
grant is idempotent (no unique-key collision).
- Create persistence + `_validated_app_ids` dedupe and unknown-id
- Create persistence, duplicate grant normalization, and unknown-id
rejection.
- **Unit** (gate, stubbed DB):
`backend/tests/unit/sandbox_proxy/test_gate.py`