Enforce bounded operation guardrails
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a028de-e2c8-7732-8521-46a7fc5db82f
This commit is contained in:
parent
c384f60530
commit
26934e25b9
51 changed files with 1843 additions and 472 deletions
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue