feat(sofiia-console): add safe script executor for allowlisted runbook steps
- adds safe_executor.py: REPO_ROOT confinement, strict script allowlist, env key allowlist (STRICT/SOFIIA_URL/BFF_A/BFF_B/NODE_ID/AGENT_ID), stdin=DEVNULL, 8KB output cap, timeout clamp (max 300s), non-root warn - integrates script action_type into runbook_runner: next_step handles http_check and script branches; running_as_root -> step_status=warn - extends runbook_parser: rehearsal-v1 now includes 3 built-in script steps (preflight, idempotency smoke, generate evidence) after http_checks - adds tests/test_sofiia_safe_executor.py: 12 tests covering path traversal, absolute path, non-allowlist, env drop, timeout, exit_code, mocked subprocess Made-with: Cursor
This commit is contained in:
@@ -86,23 +86,65 @@ def _parse_sections(markdown: str) -> List[tuple]:
|
||||
return sections
|
||||
|
||||
|
||||
def _rehearsal_script_steps(offset: int) -> List[RunbookStep]:
|
||||
"""PR3: 3 allowlisted script steps for rehearsal v1 (after http_checks)."""
|
||||
return [
|
||||
RunbookStep(
|
||||
step_index=offset,
|
||||
title="Preflight check (STRICT=1)",
|
||||
section="0–5 min — Preflight",
|
||||
action_type="script",
|
||||
action_json={
|
||||
"script": "ops/preflight_sofiia_console.sh",
|
||||
"env": {"STRICT": "1"},
|
||||
"timeout_s": 120,
|
||||
},
|
||||
),
|
||||
RunbookStep(
|
||||
step_index=offset + 1,
|
||||
title="Redis idempotency smoke test",
|
||||
section="10–15 min — Smoke",
|
||||
action_type="script",
|
||||
action_json={
|
||||
"script": "ops/redis_idempotency_smoke.sh",
|
||||
"env": {},
|
||||
"timeout_s": 60,
|
||||
},
|
||||
),
|
||||
RunbookStep(
|
||||
step_index=offset + 2,
|
||||
title="Generate release evidence",
|
||||
section="25–30 min — Evidence",
|
||||
action_type="script",
|
||||
action_json={
|
||||
"script": "ops/generate_release_evidence.sh",
|
||||
"env": {},
|
||||
"timeout_s": 60,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def parse_runbook(
|
||||
runbook_path: str,
|
||||
markdown: str,
|
||||
sofiia_url: str = "http://127.0.0.1:8002",
|
||||
) -> List[RunbookStep]:
|
||||
"""
|
||||
Parse markdown into steps. For rehearsal-v1 prepend 3 http_check steps;
|
||||
rest are manual (one per H2 section with instructions_md).
|
||||
Parse markdown into steps.
|
||||
For rehearsal-v1: prepend 3 http_check + 3 script steps; rest are manual.
|
||||
"""
|
||||
path_lower = runbook_path.lower()
|
||||
steps: List[RunbookStep] = []
|
||||
offset = 0
|
||||
|
||||
if "rehearsal" in path_lower and "30min" in path_lower:
|
||||
builtin = _rehearsal_http_check_steps(sofiia_url)
|
||||
steps.extend(builtin)
|
||||
offset = len(builtin)
|
||||
http_steps = _rehearsal_http_check_steps(sofiia_url)
|
||||
steps.extend(http_steps)
|
||||
offset = len(http_steps)
|
||||
script_steps = _rehearsal_script_steps(offset)
|
||||
steps.extend(script_steps)
|
||||
offset += len(script_steps)
|
||||
|
||||
sections = _parse_sections(markdown)
|
||||
for i, (title, content) in enumerate(sections):
|
||||
|
||||
Reference in New Issue
Block a user