Spaces:
Sleeping
Sleeping
r1cksync
feat(phase8-10): saboteur+slack+replay, 381 scenarios, Colab Unsloth+Qwen72B critic, push helpers
94db88e | """Simulator dispatch engine. | |
| `dispatch(action, state)` looks up the action id in the catalog, applies | |
| any scenario-scheduled failures, then routes to the right handler. If no | |
| bespoke handler exists for the service, it falls back to the generic | |
| handler which produces a deterministic, schema-shaped response. | |
| """ | |
| from __future__ import annotations | |
| import importlib | |
| import json | |
| from functools import lru_cache | |
| from pathlib import Path | |
| from typing import Any | |
| from .state import ActionResult, SimState | |
| CAT_DIR = Path(__file__).resolve().parent / "catalogs" | |
| def load_catalogs() -> dict[str, Any]: | |
| """Read services.json + actions.json + errors.json once and cache.""" | |
| services = json.loads((CAT_DIR / "services.json").read_text(encoding="utf-8")) | |
| actions = json.loads((CAT_DIR / "actions.json").read_text(encoding="utf-8")) | |
| errors = json.loads((CAT_DIR / "errors.json").read_text(encoding="utf-8")) | |
| by_id = {a["id"]: a for a in actions} | |
| return {"services": services, "actions": actions, | |
| "errors": errors, "by_id": by_id} | |
| # Map service-slug -> handler module name. Anything not listed routes to | |
| # `generic`. Each handler exposes `handle(action, params, state)`. | |
| _HANDLERS = { | |
| "sqs": "sqs", | |
| "lambda": "lambda_", | |
| "ec2": "ec2", | |
| "eks": "eks", | |
| "s3": "s3", | |
| "dynamodb": "dynamodb", | |
| "secretsmanager": "secrets", | |
| "kms": "kms", | |
| "ssm": "ssm", | |
| "iam": "iam", | |
| "events": "eventbridge", | |
| "stepfunctions": "stepfunctions", | |
| "cloudwatch": "cloudwatch", | |
| "cloudtrail": "cloudtrail", | |
| } | |
| def _load_handler(service: str): | |
| name = _HANDLERS.get(service, "generic") | |
| return importlib.import_module(f".handlers.{name}", package=__package__) | |
| def dispatch(action: dict, state: SimState) -> ActionResult: | |
| """Dispatch one action. | |
| `action` is `{"id": "lambda.invoke", "params": {...}}`. Returns a | |
| fully-populated `ActionResult` and mutates `state` accordingly. | |
| """ | |
| cat = load_catalogs() | |
| aid = action.get("id") or f"{action.get('service','?')}.{action.get('verb','?')}" | |
| params = action.get("params", {}) or {} | |
| # ---------------- Runbook trap arm-check --------------------------- | |
| # We inspect each armed trap *before* dispatch to record that the | |
| # forbidden action was attempted. Corruption is applied AFTER the | |
| # action runs so that the action's own status mutations don't override | |
| # the trap's CORRUPTED state. | |
| triggered_traps = [] | |
| for trap in state.runbook_traps: | |
| if trap.maybe_trigger(aid): | |
| triggered_traps.append(trap) | |
| # ---------------- platform.* pseudo-service ------------------------ | |
| if aid.startswith("platform."): | |
| verb = aid.split(".", 1)[1] | |
| spec = {"id": aid, "service": "platform", "verb": verb} | |
| handler = importlib.import_module(".handlers.platform", | |
| package=__package__) | |
| try: | |
| result = handler.handle(spec, params, state) | |
| except Exception as e: # pragma: no cover | |
| result = ActionResult(ok=False, error="ServiceException", | |
| error_message=str(e)) | |
| if triggered_traps: | |
| from .topology import CORRUPTED | |
| for trap in triggered_traps: | |
| state.topology.set_status(trap.target, CORRUPTED) | |
| state.action_log.append({ | |
| "ts": state.clock_s, "action": "_runbook.trap_triggered", | |
| "params": {"runbook": trap.runbook_id, "target": trap.target, | |
| "trigger": aid}, | |
| "ok": False, "error": "RunbookTrapTriggered", | |
| }) | |
| result.tags = list(result.tags) + ["runbook_trap"] | |
| state.log(aid, params, result) | |
| state.tick() | |
| return result | |
| spec = cat["by_id"].get(aid) | |
| if spec is None: | |
| result = ActionResult( | |
| ok=False, error="InvalidAction", | |
| error_message=f"unknown action id: {aid}", | |
| ) | |
| state.log(aid, params, result) | |
| return result | |
| # Scheduled failures (set by scenario.load_scenario) take priority. | |
| sched = state.consume_scheduled_failure(aid) | |
| if sched is not None: | |
| msg = sched.get("message") or f"scheduled failure for {aid}" | |
| result = ActionResult( | |
| ok=False, error=sched["error"], error_message=msg, | |
| tags=["scheduled_failure"], | |
| ) | |
| state.log(aid, params, result) | |
| state.tick() | |
| return result | |
| handler = _load_handler(spec["service"]) | |
| try: | |
| result = handler.handle(spec, params, state) | |
| except Exception as e: # pragma: no cover | |
| result = ActionResult(ok=False, error="ServiceException", | |
| error_message=str(e)) | |
| # Apply trap corruption AFTER the handler so the action's own status | |
| # writes don't override CORRUPTED. | |
| if triggered_traps: | |
| from .topology import CORRUPTED | |
| for trap in triggered_traps: | |
| state.topology.set_status(trap.target, CORRUPTED) | |
| state.action_log.append({ | |
| "ts": state.clock_s, "action": "_runbook.trap_triggered", | |
| "params": {"runbook": trap.runbook_id, "target": trap.target, | |
| "trigger": aid}, | |
| "ok": False, "error": "RunbookTrapTriggered", | |
| }) | |
| # Surface the catastrophe in the action result tags. | |
| result.tags = list(result.tags) + ["runbook_trap"] | |
| state.log(aid, params, result) | |
| state.tick() | |
| return result | |