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,12 +1,7 @@
"""Temporal workflow definitions for activity-core.
Two workflows are registered here:
- RunActivityWorkflow → orchestrator-tq
- TaskExecutorWorkflow → task-execution-tq
Workflow IDs follow the conventions in docs/conventions.md:
RunActivityWorkflow: activity-{activity_id}:{trigger_key}
TaskExecutorWorkflow: task-{run_id}:{task_type}:{index}
RunActivityWorkflow is registered on ``orchestrator-tq``. Workflow IDs follow
``activity-{activity_id}:{trigger_key}``; see docs/conventions.md.
"""
from __future__ import annotations
@ -22,15 +17,16 @@ with workflow.unsafe.imports_passed_through():
from activity_core.activities import (
apply_sbom_catchup,
emit_tasks,
execute_bounded_operation,
evaluate_rules,
evaluate_instructions,
load_activity_definition,
log_run,
persist_instruction_reports,
persist_ops_evidence,
persist_task_instance,
resolve_context,
)
from activity_core.bounded_operations import operation_sources
from activity_core.ops_run_queue import emit_triggering_event_id
from activity_core.schedule_manager import SCHEDULED_TRIGGER_KEY
@ -49,7 +45,7 @@ _RETRY_POLICY = RetryPolicy(
_ACTIVITY_TIMEOUT = timedelta(
seconds=int(os.environ.get("ACTIVITY_TIMEOUT_SECONDS", "900"))
)
_TASK_QUEUE = "task-execution-tq"
_NO_RETRY_POLICY = RetryPolicy(maximum_attempts=1)
@workflow.defn
@ -59,9 +55,10 @@ class RunActivityWorkflow:
Sequence:
1. load_activity_definition(activity_id) → defn dict
2. resolve_context(defn.context_sources) → read-only context snapshot
3. apply_sbom_catchup(fixed selection) → bounded outcome patch
4. evaluate rules/instructions → TaskSpec dicts and reports
5. log run, then emit tasks
3. execute admitted bounded operations → bounded outcome patches
4. persist operation evidence
5. evaluate rules/instructions → TaskSpec dicts and reports
6. log run, then emit tasks
"""
@workflow.run
@ -114,7 +111,11 @@ class RunActivityWorkflow:
retry_policy=_RETRY_POLICY,
)
# ── 3. Apply declared bounded side-effects to the fixed selection ────
# ── 3. Execute admitted bounded operations ──────────────────────────
# SBOM retains its dedicated heartbeat/retry activity because it has a
# stable remote idempotency key. Shell-backed operations are one-shot:
# their platform tools do not expose a receipt activity-core can use to
# prove an ambiguous mutation safe to repeat.
if any(
isinstance(source, dict)
and source.get("type") == "sbom-nexus"
@ -138,6 +139,22 @@ class RunActivityWorkflow:
if isinstance(current, dict) and isinstance(patch, dict):
current.update(patch)
for source, operation_spec in operation_sources(
defn.get("context_sources", [])
):
if operation_spec.operation_id == "sbom_nexus_ingest":
continue
operation_patch: dict = await workflow.execute_activity(
execute_bounded_operation,
{"source": source, "run_id": run_id},
start_to_close_timeout=timedelta(
seconds=operation_spec.max_timeout_seconds
),
retry_policy=_NO_RETRY_POLICY,
)
context_snapshot.update(operation_patch)
# ── 4. Persist bounded operation/read evidence ───────────────────────
await workflow.execute_activity(
persist_ops_evidence,
{
@ -152,7 +169,7 @@ class RunActivityWorkflow:
retry_policy=_RETRY_POLICY,
)
# ── 4. Evaluate rules ─────────────────────────────────────────────────
# ── 5. Evaluate rules ─────────────────────────────────────────────────
import json as _json
event_attrs: dict = {}
if event_envelope_json:
@ -187,7 +204,7 @@ class RunActivityWorkflow:
task_spec_dicts.extend(instruction_result.get("task_specs", []))
report_dicts.extend(instruction_result.get("reports", []))
# ── 5. Persist reports ────────────────────────────────────────────────
# ── 6. Persist reports ────────────────────────────────────────────────
if report_dicts:
await workflow.execute_activity(
persist_instruction_reports,
@ -202,7 +219,7 @@ class RunActivityWorkflow:
retry_policy=_RETRY_POLICY,
)
# ── 6. Log the run BEFORE emit ────────────────────────────────────────
# ── 7. Log the run BEFORE emit ────────────────────────────────────────
# ACTIVITY-WP-0021: emit_tasks sink failures used to abort the workflow
# before log_run, so failed Binky/SBOM fires left no activity_runs row
# and automation-status could not observe them. Always record the run;
@ -221,7 +238,7 @@ class RunActivityWorkflow:
retry_policy=_RETRY_POLICY,
)
# ── 7. Emit tasks (may fail independently of run audit) ───────────────
# ── 8. Emit tasks (may fail independently of run audit) ───────────────
if task_spec_dicts:
# Cron schedules pass trigger_key="scheduled" for *every* fire.
# ops_run idempotency is {def}:{source}:{triggering_event_id}, so
@ -239,70 +256,10 @@ class RunActivityWorkflow:
"activity_id": activity_id,
"triggering_event_id": emit_trigger_id,
"run_id": run_id,
"version_used": defn["version"],
},
start_to_close_timeout=_ACTIVITY_TIMEOUT,
retry_policy=_RETRY_POLICY,
)
return {"run_id": run_id, "tasks_spawned": len(task_spec_dicts)}
@workflow.defn
class TaskExecutorWorkflow:
"""LEGACY NO-OP — not a production execution surface (ACTIVITY-WP-0023-T08).
Historical compatibility stub. Real task execution belongs in per-repo
workers / agent-harness, not activity-core.
Behaviour is controlled by ``ACTIVITY_CORE_ENABLE_TASK_EXECUTOR_STUB``:
- unset / false (default): refuse to run (logs error, raises) so the stub
cannot attract production work by accident.
- true: legacy behaviour — persist a done ``task_instances`` row for
idempotent dev/test callers only.
"""
@workflow.run
async def run(self, run_id: str, task_type: str, params: dict) -> dict:
enabled = (
os.environ.get("ACTIVITY_CORE_ENABLE_TASK_EXECUTOR_STUB", "")
.strip()
.lower()
in {"1", "true", "yes", "on"}
)
task_id = str(
uuid.uuid5(uuid.NAMESPACE_URL, workflow.info().workflow_id)
)
if not enabled:
workflow.logger.error(
"TaskExecutorWorkflow refused: stub disabled "
"(set ACTIVITY_CORE_ENABLE_TASK_EXECUTOR_STUB=true only for legacy tests). "
"Real execution belongs in per-repo workers / agent-harness. "
"See docs/task-emission-consumer-contract.md"
)
raise RuntimeError(
"TaskExecutorWorkflow is disabled (ACTIVITY-WP-0023-T08). "
"Use per-repo executors; do not route production work here."
)
workflow.logger.warning(
"TaskExecutorWorkflow stub running (legacy mode)",
extra={"run_id": run_id, "task_type": task_type, "task_id": task_id},
)
await workflow.execute_activity(
persist_task_instance,
{
"id": task_id,
"run_id": run_id,
"type": task_type,
"params": params,
"status": "done",
},
task_queue=_TASK_QUEUE,
start_to_close_timeout=_ACTIVITY_TIMEOUT,
retry_policy=_RETRY_POLICY,
)
return {"task_id": task_id, "status": "done", "legacy_stub": True}