Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions api/ee/src/core/access/permissions/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,11 @@ class Permission(str, Enum):
EDIT_TOOLS = "edit_tools"
RUN_TOOLS = "run_tools"

# Triggers
VIEW_TRIGGERS = "view_triggers"
EDIT_TRIGGERS = "edit_triggers"
RUN_TRIGGERS = "run_triggers"

@classmethod
def default_permissions(cls, role):
VIEWER_PERMISSIONS = [
Expand Down Expand Up @@ -217,6 +222,7 @@ def default_permissions(cls, role):
cls.VIEW_EVALUATION_METRICS,
cls.VIEW_EVALUATION_QUEUES,
cls.VIEW_TOOLS,
cls.VIEW_TRIGGERS,
]
ANNOTATOR_PERMISSIONS = VIEWER_PERMISSIONS + [
cls.CREATE_EVALUATION,
Expand All @@ -230,6 +236,7 @@ def default_permissions(cls, role):
cls.EDIT_EVALUATION_QUEUES,
cls.EDIT_SPANS,
cls.RUN_TOOLS,
cls.RUN_TRIGGERS,
]
EDITOR_PERMISSIONS = ANNOTATOR_PERMISSIONS + [
cls.EDIT_APPLICATIONS,
Expand All @@ -251,6 +258,7 @@ def default_permissions(cls, role):
cls.EDIT_TESTSETS,
cls.EDIT_INVOCATIONS,
cls.EDIT_TOOLS,
cls.EDIT_TRIGGERS,
]
DEVELOPER_PERMISSIONS = EDITOR_PERMISSIONS + [
cls.VIEW_API_KEYS,
Expand Down
Empty file.
160 changes: 160 additions & 0 deletions api/ee/tests/pytest/acceptance/triggers/test_triggers_catalog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
"""EE acceptance tests for the triggers events catalog.

Mirrors the OSS suite (oss/tests/pytest/acceptance/triggers/test_triggers_catalog.py)
but exercises /triggers/catalog/* as a business-plan, developer-role account.
Under EE the catalog is gated on the VIEW_TOOLS permission (the triggers domain
shares the gateway permission surface with tools); a developer role carries
VIEW_TOOLS, so this verifies the endpoint behaves once the gate is satisfied.
Comment on lines +5 to +7

Provider-catalog reads need no Composio credentials (empty catalog is valid).
Event browse / config-schema fetch make real Composio calls and are gated on
COMPOSIO_API_KEY being present in the runner's environment.

Requires a running API.
"""

import os
from uuid import uuid4

import pytest
import requests

from utils.constants import BASE_TIMEOUT


_COMPOSIO_ENABLED = bool(os.getenv("COMPOSIO_API_KEY"))
_requires_composio = pytest.mark.skipif(
not _COMPOSIO_ENABLED,
reason="needs live Composio credentials (COMPOSIO_API_KEY)",
)


def _create_developer_business_account(admin_api):
uid = uuid4().hex[:12]
email = f"triggers-dev-{uid}@test.agenta.ai"
resp = admin_api(
"POST",
"/admin/simple/accounts/",
json={
"accounts": {
"u": {
"user": {"email": email},
"options": {
"create_api_keys": True,
"return_api_keys": True,
"seed_defaults": False,
},
"subscription": {"plan": "cloud_v0_business"},
"organization_memberships": [
{
"organization_ref": {"ref": "org"},
"user_ref": {"ref": "user"},
"role": "developer",
}
],
"workspace_memberships": [
{
"workspace_ref": {"ref": "wrk"},
"user_ref": {"ref": "user"},
"role": "developer",
}
],
"project_memberships": [
{
"project_ref": {"ref": "prj"},
"user_ref": {"ref": "user"},
"role": "developer",
}
],
}
}
},
)
assert resp.status_code == 200, resp.text
account = resp.json()["accounts"]["u"]
return {
"email": email,
"credentials": f"ApiKey {account['api_keys']['key']}",
}


def _delete_account_by_email(admin_api, *, email):
resp = admin_api(
"DELETE",
"/admin/simple/accounts/",
json={"accounts": {"u": {"user": {"email": email}}}, "confirm": "delete"},
)
assert resp.status_code == 204, resp.text


@pytest.fixture(scope="class")
def triggers_api(admin_api, ag_env):
account = _create_developer_business_account(admin_api)

def _request(method: str, endpoint: str, **kwargs):
headers = kwargs.pop("headers", {})
headers.setdefault("Authorization", account["credentials"])
return requests.request(
method=method,
url=f"{ag_env['api_url']}{endpoint}",
headers=headers,
timeout=BASE_TIMEOUT,
**kwargs,
)

yield _request

_delete_account_by_email(admin_api, email=account["email"])


class TestTriggersCatalogProviders:
def test_list_providers_returns_200(self, triggers_api):
response = triggers_api("GET", "/triggers/catalog/providers/")
assert response.status_code == 200

def test_list_providers_response_shape(self, triggers_api):
body = triggers_api("GET", "/triggers/catalog/providers/").json()
assert "count" in body
assert "providers" in body
assert isinstance(body["providers"], list)
assert body["count"] == len(body["providers"])

@pytest.mark.skipif(
_COMPOSIO_ENABLED,
reason="catalog is non-empty when Composio is enabled",
)
def test_list_providers_empty_when_composio_disabled(self, triggers_api):
body = triggers_api("GET", "/triggers/catalog/providers/").json()
assert body["count"] == 0
assert body["providers"] == []


@_requires_composio
class TestTriggersCatalogEvents:
def test_browse_events_returns_200(self, triggers_api):
response = triggers_api(
"GET",
"/triggers/catalog/providers/composio/integrations/github/events/",
)
assert response.status_code == 200
body = response.json()
assert "events" in body
assert isinstance(body["events"], list)

def test_fetch_event_config_schema(self, triggers_api):
listing = triggers_api(
"GET",
"/triggers/catalog/providers/composio/integrations/github/events/",
).json()
if not listing["events"]:
pytest.skip("no github events available from Composio")

event_key = listing["events"][0]["key"]
response = triggers_api(
"GET",
f"/triggers/catalog/providers/composio/integrations/github/events/{event_key}",
)
assert response.status_code == 200
event = response.json()["event"]
assert event["key"] == event_key
assert "trigger_config" in event
45 changes: 45 additions & 0 deletions api/entrypoints/routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@
from oss.src.core.tools.registry import ToolsGatewayRegistry
from oss.src.core.tools.service import ToolsService
from oss.src.apis.fastapi.tools.router import ToolsRouter
from oss.src.core.triggers.providers.composio import ComposioTriggersAdapter
from oss.src.core.triggers.registry import TriggersGatewayRegistry
from oss.src.core.triggers.service import TriggersService
from oss.src.apis.fastapi.triggers.router import TriggersRouter
from oss.src.apis.fastapi.shared.utils import SupportHeadersMiddleware


Expand Down Expand Up @@ -215,6 +219,9 @@ async def lifespan(*args, **kwargs):
for adapter in _composio_connections_adapters.values():
await adapter.close()

for adapter in _composio_triggers_adapters.values():
await adapter.close()

await _transactions_engine.close()
await _analytics_engine.close()
await _streams_engine.close()
Expand Down Expand Up @@ -308,6 +315,11 @@ async def lifespan(*args, **kwargs):
"description": "External tool connections and OAuth integrations available to applications.",
},
# --
{
"name": "Triggers",
"description": "Inbound provider event triggers and their watchable event catalog.",
},
# --
{
"name": "Folders",
"description": "Organize applications and other resources into folder hierarchies.",
Expand Down Expand Up @@ -616,6 +628,22 @@ async def lifespan(*args, **kwargs):
adapter_registry=tools_adapter_registry,
)

# Triggers adapter + service
_composio_triggers_adapters = {}
if env.composio.enabled:
_composio_triggers_adapters["composio"] = ComposioTriggersAdapter(
api_key=env.composio.api_key, # type: ignore[arg-type] # guarded by .enabled
api_url=env.composio.api_url,
)

triggers_adapter_registry = TriggersGatewayRegistry(
adapters=_composio_triggers_adapters,
)

triggers_service = TriggersService(
adapter_registry=triggers_adapter_registry,
)

_t_services_done = time.perf_counter() - _t_services
print(f"[STARTUP] Service initialization completed (+{_t_services_done:.3f}s)")
_t_routers = time.perf_counter()
Expand Down Expand Up @@ -730,6 +758,10 @@ async def lifespan(*args, **kwargs):
tools_service=tools_service,
)

triggers = TriggersRouter(
triggers_service=triggers_service,
)

simple_traces = SimpleTracesRouter(
simple_traces_service=simple_traces_service,
)
Expand Down Expand Up @@ -1097,6 +1129,19 @@ async def lifespan(*args, **kwargs):
include_in_schema=False,
)

app.include_router(
router=triggers.router,
prefix="/triggers",
tags=["Triggers"],
)

app.include_router(
router=triggers.router,
prefix="/preview/triggers",
tags=["Triggers"],
include_in_schema=False,
)

app.include_router(
router=evaluations.admin_router,
prefix="/admin/evaluations",
Expand Down
Empty file.
36 changes: 36 additions & 0 deletions api/oss/src/apis/fastapi/triggers/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing import List, Optional

from pydantic import BaseModel

from oss.src.core.triggers.dtos import (
Comment on lines +1 to +5
TriggerCatalogEvent,
TriggerCatalogEventDetails,
TriggerCatalogProvider,
)


# ---------------------------------------------------------------------------
# Trigger Catalog
# ---------------------------------------------------------------------------


class TriggerCatalogProviderResponse(BaseModel):
count: int = 0
provider: Optional[TriggerCatalogProvider] = None


class TriggerCatalogProvidersResponse(BaseModel):
count: int = 0
providers: List[TriggerCatalogProvider] = []

Comment on lines +22 to +25

class TriggerCatalogEventResponse(BaseModel):
count: int = 0
event: Optional[TriggerCatalogEventDetails] = None


class TriggerCatalogEventsResponse(BaseModel):
count: int = 0
total: int = 0
cursor: Optional[str] = None
events: List[TriggerCatalogEvent] = []
Loading
Loading