feat: add host188 OpenClaw callback playbook
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from src.services.awooop_ansible_audit_service import (
|
||||
_catalog_hints,
|
||||
get_ansible_catalog_item,
|
||||
)
|
||||
from src.services.awooop_ansible_check_mode_service import (
|
||||
build_ansible_apply_command,
|
||||
build_ansible_check_mode_command,
|
||||
)
|
||||
from src.services.awooop_ansible_post_verifier import postconditions_for_catalog
|
||||
from src.services.controlled_alert_target_router import resolve_typed_alert_target
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
PLAYBOOK = (
|
||||
ROOT
|
||||
/ "infra"
|
||||
/ "ansible"
|
||||
/ "playbooks"
|
||||
/ "188-openclaw-callback-forwarder.yml"
|
||||
)
|
||||
SERVICE_REGISTRY = ROOT / "ops" / "config" / "service-registry.yaml"
|
||||
MONITORING_REGISTRY = ROOT / "ops" / "monitoring" / "service-registry.yaml"
|
||||
CATALOG_ID = "ansible:188-openclaw-callback-forwarder"
|
||||
CANONICAL_ASSET_ID = "service:openclaw:host188"
|
||||
EXPECTED_SHA = "ffc45311b6403b30cb6ae6e1aa83dae1a047e9c9"
|
||||
EXPECTED_REMOTE = "ssh://git@192.168.0.110:2222/wooo/clawbot-v5.git"
|
||||
|
||||
|
||||
def _flatten_tasks(tasks: list[dict]) -> list[dict]:
|
||||
flattened: list[dict] = []
|
||||
for task in tasks:
|
||||
flattened.append(task)
|
||||
for section in ("block", "rescue", "always"):
|
||||
nested = task.get(section)
|
||||
if isinstance(nested, list):
|
||||
flattened.extend(_flatten_tasks(nested))
|
||||
return flattened
|
||||
|
||||
|
||||
def _incident() -> dict:
|
||||
return {
|
||||
"incident_id": "INC-OPENCLAW-CALLBACK-FORWARDER",
|
||||
"project_id": "awoooi",
|
||||
"alertname": "OpenClawDown",
|
||||
"alert_category": "ai_agent",
|
||||
"affected_services": ["openclaw"],
|
||||
"signals": [
|
||||
{
|
||||
"alert_name": "OpenClawDown",
|
||||
"labels": {"component": "openclaw", "host": "188"},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_openclaw_routes_only_to_exact_host188_compose_lane() -> None:
|
||||
for target in ("openclaw", "clawbot", "clawbot.service"):
|
||||
route = resolve_typed_alert_target(
|
||||
alertname="OpenClawDown",
|
||||
target_resource=target,
|
||||
namespace="",
|
||||
labels={"host": "188"},
|
||||
alert_category="ai_agent",
|
||||
)
|
||||
|
||||
assert route["resolution_status"] == "resolved"
|
||||
assert route["target_kind"] == "docker_compose_container"
|
||||
assert route["canonical_asset_id"] == CANONICAL_ASSET_ID
|
||||
assert route["host"] == "192.168.0.188"
|
||||
assert route["executor"] == "host_ansible_executor"
|
||||
assert route["verifier"] == (
|
||||
"openclaw_callback_forwarder_independent_verifier"
|
||||
)
|
||||
assert route["risk_class"] == "medium"
|
||||
assert route["allowed_catalog_ids"] == [CATALOG_ID]
|
||||
assert route["allowed_inventory_hosts"] == ["host_188"]
|
||||
assert route["cross_domain_fallback_allowed"] is False
|
||||
|
||||
|
||||
def test_openclaw_incident_selects_one_exact_bounded_catalog() -> None:
|
||||
hints = _catalog_hints(_incident(), None)
|
||||
|
||||
assert hints["match_mode"] == "typed_domain_router_v2"
|
||||
assert hints["decision_effect"] == "bounded_domain_candidate"
|
||||
assert [row["catalog_id"] for row in hints["candidates"]] == [CATALOG_ID]
|
||||
candidate = hints["candidates"][0]
|
||||
assert candidate["inventory_hosts"] == ["host_188"]
|
||||
assert candidate["canonical_asset_id"] == CANONICAL_ASSET_ID
|
||||
assert candidate["typed_domain"] == "docker_compose_container"
|
||||
assert candidate["supports_check_mode"] is True
|
||||
assert candidate["auto_apply_enabled"] is True
|
||||
assert candidate["approval_required"] is False
|
||||
assert candidate["risk_level"] == "medium"
|
||||
assert hints["cross_domain_fallback_allowed"] is False
|
||||
|
||||
|
||||
def test_openclaw_catalog_and_independent_verifier_are_exact() -> None:
|
||||
catalog = get_ansible_catalog_item(CATALOG_ID)
|
||||
conditions = postconditions_for_catalog(CATALOG_ID)
|
||||
|
||||
assert catalog is not None
|
||||
assert catalog["playbook_path"].endswith(
|
||||
"188-openclaw-callback-forwarder.yml"
|
||||
)
|
||||
assert catalog["check_mode_playbook_path"] == catalog["playbook_path"]
|
||||
assert catalog["canonical_asset_id"] == CANONICAL_ASSET_ID
|
||||
assert catalog["inventory_hosts"] == ["host_188"]
|
||||
assert catalog["domains"] == [
|
||||
"docker_compose_container",
|
||||
"openclaw",
|
||||
"telegram_callback_forwarder",
|
||||
"host_188",
|
||||
]
|
||||
assert catalog["catalog_revision"] == (
|
||||
"2026-07-17-openclaw-callback-forwarder-v1"
|
||||
)
|
||||
assert catalog["cross_domain_fallback_allowed"] is False
|
||||
|
||||
assert {condition.condition_id for condition in conditions} == {
|
||||
"host_188_openclaw_immutable_source_identity",
|
||||
"host_188_openclaw_callback_dropin_enabled",
|
||||
"host_188_clawbot_systemd_runtime_active",
|
||||
"host_188_openclaw_local_health",
|
||||
"host_188_awoooi_callback_receiver_public_health",
|
||||
}
|
||||
assert {condition.inventory_host for condition in conditions} == {"host_188"}
|
||||
probes = "\n".join(condition.probe for condition in conditions)
|
||||
assert EXPECTED_SHA in probes
|
||||
assert EXPECTED_REMOTE in probes
|
||||
assert "TELEGRAM_CALLBACK_FORWARD_ENABLED=true" in probes
|
||||
assert "http://127.0.0.1:8088/health" in probes
|
||||
assert "https://awoooi.wooo.work/api/v1/telegram/health" in probes
|
||||
assert "--env-file" not in probes
|
||||
|
||||
|
||||
def test_openclaw_playbook_is_check_first_bounded_and_rollback_capable() -> None:
|
||||
payload = yaml.safe_load(PLAYBOOK.read_text(encoding="utf-8"))
|
||||
source = PLAYBOOK.read_text(encoding="utf-8")
|
||||
play = payload[0]
|
||||
tasks = {task["name"]: task for task in _flatten_tasks(play["tasks"])}
|
||||
|
||||
assert play["hosts"] == "host_188"
|
||||
assert play["gather_facts"] is False
|
||||
assert play["become"] is True
|
||||
assert play["vars"]["catalog_id"] == CATALOG_ID
|
||||
assert play["vars"]["canonical_asset_id"] == CANONICAL_ASSET_ID
|
||||
assert play["vars"]["expected_source_sha"] == EXPECTED_SHA
|
||||
assert play["vars"]["expected_remote_url"] == EXPECTED_REMOTE
|
||||
assert play["vars"]["allowed_working_directories"] == [
|
||||
"/opt/openclaw",
|
||||
"/home/ollama/clawbot-v5",
|
||||
]
|
||||
assert play["vars"]["callback_dropin_content"].splitlines() == [
|
||||
"[Service]",
|
||||
"Environment=TELEGRAM_CALLBACK_FORWARD_ENABLED=true",
|
||||
]
|
||||
|
||||
working_directory = tasks[
|
||||
"Resolve OpenClaw working directory from systemd truth"
|
||||
]
|
||||
assert working_directory["check_mode"] is False
|
||||
assert working_directory["ansible.builtin.command"]["argv"] == [
|
||||
"systemctl",
|
||||
"show",
|
||||
"clawbot.service",
|
||||
"--property=WorkingDirectory",
|
||||
"--value",
|
||||
]
|
||||
assert tasks["Fail closed when systemd source identity is outside host188 scope"][
|
||||
"ansible.builtin.assert"
|
||||
]["that"] == ["openclaw_workdir in allowed_working_directories"]
|
||||
|
||||
check_receipt = tasks["Publish check-mode source diff and no-write receipt"]
|
||||
assert check_receipt["when"] == "ansible_check_mode"
|
||||
assert check_receipt["ansible.builtin.debug"]["msg"][
|
||||
"persistent_writes_performed"
|
||||
] == 0
|
||||
for key in ("trace_id", "run_id", "work_item_id"):
|
||||
assert check_receipt["ansible.builtin.debug"]["msg"][key] == (
|
||||
"{{ " + key + " }}"
|
||||
)
|
||||
|
||||
for name in (
|
||||
"Build only the immutable OpenClaw compose service with timeout",
|
||||
"Rebuild only the restored OpenClaw compose service",
|
||||
):
|
||||
task = tasks[name]
|
||||
argv = task["ansible.builtin.command"]["argv"]
|
||||
assert argv[:4] == [
|
||||
"/usr/bin/timeout",
|
||||
"--signal=TERM",
|
||||
"--kill-after=15",
|
||||
"300",
|
||||
]
|
||||
assert "build" in argv
|
||||
assert "up" not in argv
|
||||
assert argv[-1] == "{{ compose_service }}"
|
||||
assert task["args"]["chdir"] == "{{ openclaw_workdir }}"
|
||||
assert task["no_log"] is True
|
||||
|
||||
for name in (
|
||||
"Reload systemd and restart the owned service with callback flag",
|
||||
"Reload systemd and restart restored clawbot.service contract",
|
||||
):
|
||||
assert tasks[name]["ansible.builtin.systemd"] == {
|
||||
"name": "clawbot.service",
|
||||
"state": "restarted",
|
||||
"daemon_reload": True,
|
||||
}
|
||||
assert tasks[name]["no_log"] is True
|
||||
|
||||
rollback = tasks["Restore exact old source SHA after failed apply or verifier"]
|
||||
assert rollback["ansible.builtin.command"]["argv"][-2:] == [
|
||||
"--hard",
|
||||
"{{ source_head_before.stdout | trim }}",
|
||||
]
|
||||
assert "old-source-sha" in source
|
||||
assert "old-callback-dropin.conf" in source
|
||||
assert "openclaw_callback_forwarder_rollback_v1" in source
|
||||
assert "git merge --ff-only" not in source
|
||||
assert "--ff-only" in source
|
||||
assert "docker compose config" not in source
|
||||
assert "--env-file" not in source
|
||||
assert "- up" not in source
|
||||
assert "docker compose down" not in source
|
||||
assert "docker system prune" not in source
|
||||
assert "force push" not in source
|
||||
|
||||
|
||||
def test_openclaw_commands_carry_same_safe_controlled_correlation(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
key = tmp_path / "ssh_mcp_key"
|
||||
known_hosts = tmp_path / "known_hosts"
|
||||
expected = {
|
||||
"ansible_ssh_private_key_file": str(key),
|
||||
"controlled_trace_id": "trace:openclaw:001",
|
||||
"controlled_run_id": "run:openclaw:001",
|
||||
"controlled_work_item_id": "P0-OPENCLAW-CALLBACK-001",
|
||||
}
|
||||
|
||||
for builder in (build_ansible_check_mode_command, build_ansible_apply_command):
|
||||
spec = builder(
|
||||
playbook_path=(
|
||||
"infra/ansible/playbooks/188-openclaw-callback-forwarder.yml"
|
||||
),
|
||||
inventory_hosts=("host_188",),
|
||||
playbook_root=ROOT / "infra" / "ansible",
|
||||
check_mode_ssh_key_path=key,
|
||||
check_mode_known_hosts_path=known_hosts,
|
||||
trace_id="trace:openclaw:001",
|
||||
run_id="run:openclaw:001",
|
||||
work_item_id="P0-OPENCLAW-CALLBACK-001",
|
||||
)
|
||||
assert json.loads(spec.command[-1]) == expected
|
||||
|
||||
with pytest.raises(ValueError, match="unsafe_controlled_execution_correlation"):
|
||||
build_ansible_check_mode_command(
|
||||
playbook_path=(
|
||||
"infra/ansible/playbooks/188-openclaw-callback-forwarder.yml"
|
||||
),
|
||||
inventory_hosts=("host_188",),
|
||||
playbook_root=ROOT / "infra" / "ansible",
|
||||
check_mode_ssh_key_path=key,
|
||||
check_mode_known_hosts_path=known_hosts,
|
||||
trace_id="unsafe correlation",
|
||||
)
|
||||
|
||||
|
||||
def test_openclaw_source_and_monitoring_registries_use_exact_catalog() -> None:
|
||||
registry = yaml.safe_load(SERVICE_REGISTRY.read_text(encoding="utf-8"))
|
||||
service = next(row for row in registry["services"] if row["name"] == "openclaw")
|
||||
assert service["canonical_id"] == CANONICAL_ASSET_ID
|
||||
assert service["host"] == "192.168.0.188"
|
||||
assert service["asset_domain"] == "docker_compose_container"
|
||||
assert service["executor"] == "host_ansible_executor"
|
||||
assert service["verifier"] == (
|
||||
"openclaw_callback_forwarder_independent_verifier"
|
||||
)
|
||||
assert service["allowed_catalog_ids"] == [CATALOG_ID]
|
||||
|
||||
monitoring = yaml.safe_load(
|
||||
MONITORING_REGISTRY.read_text(encoding="utf-8")
|
||||
)
|
||||
monitored = next(
|
||||
row for row in monitoring["services"] if row["name"] == "openclaw"
|
||||
)
|
||||
assert monitored["host"] == "192.168.0.188"
|
||||
assert monitored["auto_repair"] == {
|
||||
"enabled": True,
|
||||
"actions": [CATALOG_ID],
|
||||
}
|
||||
Reference in New Issue
Block a user