Files
onyx/backend/tests/unit/external_apps/matching/test_engine.py
Wenxi e9c36400a7 feat(craft): enforce personal and task action policies (#16179)
## Description

Give each user and scheduled task its own action settings.
Workspace-disabled actions always remain blocked. Admins can require
workspace settings or let users choose their own settings.

Scheduled tasks now show app and MCP cards. Apps start disabled. When
creating a task, enabling an app copies the user's current settings.
Existing tasks keep their saved choices when apps are enabled again;
missing choices require approval. Later requests use the task's saved
settings and current workspace restrictions.

One migration replaces app-wide task pre-approvals with per-action
settings. Existing tasks receive auto-approval only for known actions in
apps they already pre-approved. Apps remain enabled even if they have no
known actions. New actions require approval. The migration remains with
the code that reads the new tables.

Action recognition identifies the request. One policy resolver then
applies workspace restrictions, task membership, personal or task
settings, and session grants. Failed recognition or policy lookup blocks
the request. A completed scheduled session cannot use personal settings
for later requests.

The next PR adds app settings editors. This PR includes the task editor
because its payload changes with the task API.

## How Has This Been Tested?

The complete stack passed 71 local Kind scenario checks, covering
browser editing, persistence, access restrictions, real proxy requests,
and scheduled execution. Checks include a real 180-second approval
timeout, live policy changes, and completed-task request rejection.

This PR’s intermediate tree passed 1,053 backend unit tests, 148
database tests, five route-contract tests, and 39 frontend tests. Python
and TypeScript checks and changed-file pre-commit checks passed.

No migration was applied to the user's database during this
verification.

## Additional Options

- [ ] [Optional] Please cherry-pick this PR to the latest release
version.
- [x] [Optional] Override Linear Check


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added per-action approval settings for connected apps and MCP tools,
with workspace, personal, and scheduled-task controls.
* Scheduled tasks can enable apps and configure individual actions when
creating or editing tasks; settings are also visible on task details.
New tasks start with apps disabled.
* Policy editors support searching, bulk changes, and resetting editable
choices.
* **Security & Permissions**
* Workspace restrictions remain binding, and task settings are checked
during active runs.
* Actions requiring approval still prompt users when app credentials are
unavailable.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-19 18:44:13 +00:00

49 lines
1.9 KiB
Python

"""Recognition uses provider catalogs without loading permission or credential state."""
import pytest
from pydantic import ValidationError
from onyx.db.enums import ActionPolicy, ExternalAppType, GatedAppKind
from onyx.db.models import ExternalApp
from onyx.external_apps.matching.engine import (
WHOLE_DOMAIN_ACTION_TYPE,
AllMatchedActions,
GatedTarget,
recognize_actions,
)
from onyx.external_apps.matching.request import ProxiedRequest
def test_rejects_empty_actions() -> None:
with pytest.raises(ValidationError, match="actions must be non-empty"):
AllMatchedActions(
actions=(),
target=GatedTarget(kind=GatedAppKind.EXTERNAL_APP, id=1, app_name="X"),
)
@pytest.mark.parametrize(
("path", "action_id", "default"),
[
("/api/conversations.list", "slack.channels.read", ActionPolicy.ALWAYS),
("/api/chat.postMessage", "slack.messages.write", ActionPolicy.ASK),
("/api/unknown", WHOLE_DOMAIN_ACTION_TYPE, ActionPolicy.ASK),
],
)
def test_recognition_preserves_action_identity_and_catalog_default(
path: str, action_id: str, default: ActionPolicy
) -> None:
app = ExternalApp(id=1, name="Slack", app_type=ExternalAppType.SLACK)
matched = recognize_actions(app, ProxiedRequest(method="POST", path=path))
assert matched.target.key == (GatedAppKind.EXTERNAL_APP, app.id)
assert matched.governing_action.action_type == action_id
assert matched.governing_action.policy == default
def test_custom_app_unknown_action_preserves_display_name() -> None:
app = ExternalApp(id=2, name="Customer CRM", app_type=ExternalAppType.CUSTOM)
matched = recognize_actions(app, ProxiedRequest(method="POST", path="/anything"))
assert matched.app_name == "Customer CRM"
assert matched.governing_action.action_type == WHOLE_DOMAIN_ACTION_TYPE
assert matched.governing_action.policy == ActionPolicy.ASK