Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ web/packages/agenta-api-client/dist/
web/tsconfig.tsbuildinfo
# Agent Pi extension bundle, built by `pnpm run build:extension` and in the Docker image.
services/agent/dist/
# Agent runner test/coverage artifacts (vitest writes these on `pnpm test` / coverage runs).
services/agent/test-results/
services/agent/coverage/

__pycache__/
**/__pycache__/
Expand Down
2 changes: 2 additions & 0 deletions api/entrypoints/routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
from oss.src.core.folders.service import FoldersService
from oss.src.core.workflows.service import WorkflowsService
from oss.src.core.workflows.service import SimpleWorkflowsService
from oss.src.core.workflows.platform_catalog import PlatformWorkflowCatalog
from oss.src.core.evaluators.service import EvaluatorsService
from oss.src.core.evaluators.service import SimpleEvaluatorsService
from oss.src.core.environments.service import EnvironmentsService
Expand Down Expand Up @@ -495,6 +496,7 @@ async def lifespan(*args, **kwargs):

workflows_service = WorkflowsService(
workflows_dao=workflows_dao,
platform_catalog=PlatformWorkflowCatalog(),
)

environments_service = EnvironmentsService(
Expand Down
34 changes: 34 additions & 0 deletions api/oss/src/apis/fastapi/workflows/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Typed HTTP exceptions and a translation decorator for workflow-domain errors.

Mirrors ``api/oss/src/apis/fastapi/git/exceptions.py`` but for workflow-specific domain errors
(``oss.src.core.workflows.types``) that the shared git pattern does not cover. Place the decorator
inside ``@intercept_exceptions()`` so the typed HTTP exception is the one re-raised.
"""

from functools import wraps

from fastapi import HTTPException

from oss.src.core.workflows.types import ReservedWorkflowSlug


class ReservedWorkflowSlugException(HTTPException):
def __init__(
self,
message: str = "The slug prefix '_agenta.' is reserved for platform workflows.",
):
super().__init__(status_code=400, detail=message)


def handle_workflow_exceptions():
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
try:
return await func(*args, **kwargs)
except ReservedWorkflowSlug as e:
raise ReservedWorkflowSlugException(message=e.message) from e

return wrapper

return decorator
7 changes: 7 additions & 0 deletions api/oss/src/apis/fastapi/workflows/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
)
from oss.src.core.git.utils import build_retrieval_info
from oss.src.apis.fastapi.git.exceptions import handle_git_exceptions
from oss.src.apis.fastapi.workflows.exceptions import handle_workflow_exceptions
from oss.src.core.workflows.service import (
WorkflowsService,
SimpleWorkflowsService,
Expand Down Expand Up @@ -598,6 +599,7 @@ async def fetch_workflow_catalog_preset(
# WORKFLOWS ----------------------------------------------------------------

@intercept_exceptions()
@handle_workflow_exceptions()
async def create_workflow(
self,
request: Request,
Expand Down Expand Up @@ -877,6 +879,7 @@ async def query_workflows(
# WORKFLOW VARIANTS --------------------------------------------------------

@intercept_exceptions()
@handle_workflow_exceptions()
async def create_workflow_variant(
self,
request: Request,
Expand Down Expand Up @@ -1151,6 +1154,7 @@ async def query_workflow_variants(
return workflow_variants_response

@intercept_exceptions()
@handle_workflow_exceptions()
@handle_git_exceptions()
async def fork_workflow_variant(
self,
Expand Down Expand Up @@ -1188,6 +1192,7 @@ async def fork_workflow_variant(
# WORKFLOW REVISIONS -------------------------------------------------------

@intercept_exceptions()
@handle_workflow_exceptions()
@handle_git_exceptions()
async def create_workflow_revision(
self,
Expand Down Expand Up @@ -1438,6 +1443,7 @@ async def query_workflow_revisions(
return workflow_revisions_response

@intercept_exceptions()
@handle_workflow_exceptions()
async def commit_workflow_revision(
self,
request: Request,
Expand Down Expand Up @@ -1929,6 +1935,7 @@ def __init__(
# SIMPLE WORKFLOWS ---------------------------------------------------------

@intercept_exceptions()
@handle_workflow_exceptions()
async def create_simple_workflow(
self,
request: Request,
Expand Down
8 changes: 8 additions & 0 deletions api/oss/src/core/workflows/dtos.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ class WorkflowArtifactFlags(BaseModel):
is_application: bool = False
is_evaluator: bool = False
is_snippet: bool = False
is_skill: bool = False
# platform-owned (read-only): served from the PlatformWorkflowCatalog under the reserved
# `_agenta.*` slug namespace, never the database. Lives in JSONB flags, no migration.
is_platform: bool = False


class WorkflowVariantFlags(WorkflowArtifactFlags):
Expand Down Expand Up @@ -153,6 +157,8 @@ class WorkflowArtifactQueryFlags(BaseModel):
is_application: Optional[bool] = None
is_evaluator: Optional[bool] = None
is_snippet: Optional[bool] = None
is_skill: Optional[bool] = None
is_platform: Optional[bool] = None


class WorkflowVariantQueryFlags(WorkflowArtifactQueryFlags):
Expand Down Expand Up @@ -197,6 +203,8 @@ class WorkflowCatalogFlags(BaseModel):
is_application: bool = False
is_evaluator: bool = False
is_snippet: bool = False
is_skill: bool = False
is_platform: bool = False


# workflows --------------------------------------------------------------------
Expand Down
64 changes: 64 additions & 0 deletions api/oss/src/core/workflows/interfaces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Core contracts the workflows service depends on (not concrete DB/DAO).

The :class:`PlatformWorkflowProvider` is the read-only seam for platform-owned workflows served
from code under a reserved slug namespace. ``WorkflowsService`` depends on this interface, never on
a concrete catalogue, so the layering rule (core depends on interfaces, the composition root wires
the implementation) holds.
"""

from abc import ABC, abstractmethod
from typing import Optional
from uuid import UUID

from oss.src.core.workflows.dtos import WorkflowRevision


class PlatformWorkflowProvider(ABC):
"""A read-only provider of synthetic, code-defined workflow revisions.

Platform workflows live under a reserved slug namespace and are served from code, never the
database. The provider answers two questions for ``WorkflowsService``: whether a slug belongs
to the reserved namespace (so the service short-circuits before any DB lookup and so user
create/edit/commit can be rejected), and what synthetic revision a reserved slug resolves to.
"""

@abstractmethod
def is_reserved_slug(self, slug: Optional[str]) -> bool:
"""Whether ``slug`` is in the reserved platform namespace.

A slug in this namespace is never read from or written to the database.
"""

@abstractmethod
def is_reserved_id(self, entity_id: Optional[UUID]) -> bool:
"""Whether ``entity_id`` is a synthetic platform artifact / variant / revision id.

Lets an id-only reference short-circuit to the catalogue (deploy emits synthetic ids), so
a platform id never DB-queries.
"""

@abstractmethod
def get_revision(
self,
*,
slug: str,
version: Optional[str] = None,
) -> Optional[WorkflowRevision]:
"""Resolve a reserved slug to a synthetic :class:`WorkflowRevision`.

With no ``version`` (an artifact-level lookup) returns the catalogue entry's ``current``
version. With a ``version`` (a revision-level lookup) returns that immutable version, or
``None`` if the slug is unknown or the version does not exist.
"""

@abstractmethod
def get_revision_by_id(
self,
*,
entity_id: UUID,
) -> Optional[WorkflowRevision]:
"""Resolve a synthetic platform id (artifact / variant / revision) to its revision.

An artifact or variant id resolves to the ``current`` revision; a revision id pins its
version. Returns ``None`` if ``entity_id`` is not a known platform id.
"""
Loading
Loading