Enforce bounded operation guardrails
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 21s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a028de-e2c8-7732-8521-46a7fc5db82f
This commit is contained in:
tegwick 2026-08-23 12:31:13 +02:00
parent c384f60530
commit 26934e25b9
51 changed files with 1843 additions and 472 deletions

View file

@ -1,6 +1,8 @@
"""FastAPI REST API for activity-core.
T30: CRUD for ActivityDefinition + manual one-shot trigger.
T30: basic row administration for ActivityDefinition + manual one-shot trigger.
The REST schema does not round-trip markdown-authored rules or instructions;
use source sync for the complete definition contract.
ACTIVITY-WP-0024: operator automation console under /ops.
Endpoints:
@ -40,6 +42,7 @@ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_asyn
from temporalio.api.workflowservice.v1 import GetSystemInfoRequest
from temporalio.client import Client
from activity_core.bounded_operations import validate_bounded_operations
from activity_core.models import ActivityDefinition, CronTriggerConfig
from activity_core.execution_api import router as execution_router
from activity_core.ops_api import bind_ops_deps, router as ops_router
@ -83,7 +86,14 @@ async def lifespan(app: FastAPI): # type: ignore[type-arg]
await engine.dispose()
app = FastAPI(title="activity-core API", lifespan=lifespan)
app = FastAPI(
title="activity-core API",
description=(
"Automation operations plus basic ActivityDefinition row administration. "
"Markdown source sync owns the full rules/instructions contract."
),
lifespan=lifespan,
)
app.include_router(webhook_router)
app.include_router(ops_router)
app.include_router(ops_runs_router)
@ -152,6 +162,16 @@ def _row_to_response(row: ActivityDefinitionRow) -> ActivityDefinitionResponse:
)
def _validate_context_admission(
context_sources: list[dict[str, Any]],
instructions: list[dict[str, Any]] | None = None,
) -> None:
try:
validate_bounded_operations(context_sources, instructions or [])
except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
async def _upsert_schedule_if_cron(row: ActivityDefinitionRow) -> None:
"""Upsert a Temporal Schedule for the row if it uses a cron trigger."""
try:
@ -197,7 +217,8 @@ async def get_definition(definition_id: uuid.UUID) -> ActivityDefinitionResponse
@app.post("/activity-definitions/", response_model=ActivityDefinitionResponse, status_code=201)
async def create_definition(body: ActivityDefinitionCreate) -> ActivityDefinitionResponse:
"""Create a new ActivityDefinition. Upserts a Temporal Schedule if trigger_type='cron'."""
"""Create a basic definition row; source sync owns rules and instructions."""
_validate_context_admission(body.context_sources)
trigger_type = body.trigger_config.get("trigger_type", "")
row = ActivityDefinitionRow(
id=uuid.uuid4(),
@ -222,13 +243,20 @@ async def create_definition(body: ActivityDefinitionCreate) -> ActivityDefinitio
async def update_definition(
definition_id: uuid.UUID, body: ActivityDefinitionUpdate
) -> ActivityDefinitionResponse:
"""Update an ActivityDefinition. Re-upserts the Temporal Schedule if trigger_type='cron'."""
"""Update basic row fields; source sync owns rules and instructions."""
Session = _get_db()
async with Session() as session:
row = await session.get(ActivityDefinitionRow, definition_id)
if row is None:
raise HTTPException(status_code=404, detail="ActivityDefinition not found")
_validate_context_admission(
body.context_sources
if body.context_sources is not None
else list(row.context_sources or []),
list(row.instructions_json or []),
)
if body.name is not None:
row.name = body.name
if body.enabled is not None: