diff --git a/apps/api/tests/test_host188_openclaw_callback_forwarder_ansible_catalog.py b/apps/api/tests/test_host188_openclaw_callback_forwarder_ansible_catalog.py index 1e6df0f04..8aac8605b 100644 --- a/apps/api/tests/test_host188_openclaw_callback_forwarder_ansible_catalog.py +++ b/apps/api/tests/test_host188_openclaw_callback_forwarder_ansible_catalog.py @@ -213,6 +213,29 @@ def test_openclaw_playbook_is_check_first_bounded_and_rollback_capable() -> None assert tasks["Fail closed when systemd source identity is outside host188 scope"][ "ansible.builtin.assert" ]["that"] == ["openclaw_workdir in allowed_working_directories"] + systemd_fields = tasks[ + "Pin bounded systemd start and stop contracts separately" + ]["ansible.builtin.set_fact"] + assert "^ExecStart=" in systemd_fields["clawbot_exec_start_line"] + assert "^ExecStop=" in systemd_fields["clawbot_exec_stop_line"] + systemd_contract = tasks[ + "Require the existing systemd-owned Compose up contract" + ]["ansible.builtin.assert"]["that"] + assert ( + "'argv[]=/usr/bin/docker compose up -d ;' in clawbot_exec_start_line" + in systemd_contract + ) + assert ( + "'argv[]=/usr/bin/docker compose down ;' in clawbot_exec_stop_line" + in systemd_contract + ) + assert ( + "' down ' not in (' ' ~ clawbot_exec_start_line ~ ' ')" + in systemd_contract + ) + assert "' down ' not in (' ' ~ clawbot_systemd_shape.stdout ~ ' ')" not in ( + systemd_contract + ) check_receipt = tasks["Publish check-mode overlay diff and no-write receipt"] assert check_receipt["when"] == "ansible_check_mode" diff --git a/infra/ansible/playbooks/188-openclaw-callback-forwarder.yml b/infra/ansible/playbooks/188-openclaw-callback-forwarder.yml index 7dc02bb42..31ffb24e0 100644 --- a/infra/ansible/playbooks/188-openclaw-callback-forwarder.yml +++ b/infra/ansible/playbooks/188-openclaw-callback-forwarder.yml @@ -143,16 +143,31 @@ changed_when: false no_log: true + - name: Pin bounded systemd start and stop contracts separately + ansible.builtin.set_fact: + clawbot_exec_start_line: >- + {{ (clawbot_systemd_shape.stdout_lines + | select('match', '^ExecStart=') | list | first) | default('') }} + clawbot_exec_stop_line: >- + {{ (clawbot_systemd_shape.stdout_lines + | select('match', '^ExecStop=') | list | first) | default('') }} + changed_when: false + - name: Require the existing systemd-owned Compose up contract ansible.builtin.assert: that: - "'Type=oneshot' in clawbot_systemd_shape.stdout" - - "'docker compose up -d' in clawbot_systemd_shape.stdout" - - "'--file' not in clawbot_systemd_shape.stdout" - - "' -f ' not in (' ' ~ clawbot_systemd_shape.stdout ~ ' ')" - - "'--build' not in clawbot_systemd_shape.stdout" - - "' pull ' not in (' ' ~ clawbot_systemd_shape.stdout ~ ' ')" - - "' down ' not in (' ' ~ clawbot_systemd_shape.stdout ~ ' ')" + - "'argv[]=/usr/bin/docker compose up -d ;' in clawbot_exec_start_line" + - "'argv[]=/usr/bin/docker compose down ;' in clawbot_exec_stop_line" + - "'--file' not in clawbot_exec_start_line" + - "'--file' not in clawbot_exec_stop_line" + - "' -f ' not in (' ' ~ clawbot_exec_start_line ~ ' ')" + - "' -f ' not in (' ' ~ clawbot_exec_stop_line ~ ' ')" + - "'--build' not in clawbot_exec_start_line" + - "'--build' not in clawbot_exec_stop_line" + - "' pull ' not in (' ' ~ clawbot_exec_start_line ~ ' ')" + - "' pull ' not in (' ' ~ clawbot_exec_stop_line ~ ' ')" + - "' down ' not in (' ' ~ clawbot_exec_start_line ~ ' ')" - "' prune ' not in (' ' ~ clawbot_systemd_shape.stdout ~ ' ')" - "' rm ' not in (' ' ~ clawbot_systemd_shape.stdout ~ ' ')" fail_msg: clawbot_systemd_command_contract_mismatch