From 74d338676e770c2713a0816466dc5aef0fc60fe9 Mon Sep 17 00:00:00 2001 From: ogt Date: Fri, 17 Jul 2026 10:49:11 +0800 Subject: [PATCH] fix(sre): remove Host188 callback sudo dependency --- .../services/awooop_ansible_audit_service.py | 2 +- .../services/awooop_ansible_post_verifier.py | 40 ++- ...claw_callback_forwarder_ansible_catalog.py | 75 ++++-- ...callback_overlay_post_verifier_contract.py | 14 +- .../manifest.json | 4 +- .../188-openclaw-callback-forwarder.yml | 238 ++++++++---------- 6 files changed, 181 insertions(+), 192 deletions(-) diff --git a/apps/api/src/services/awooop_ansible_audit_service.py b/apps/api/src/services/awooop_ansible_audit_service.py index 95942a7ec..4a32a73cb 100644 --- a/apps/api/src/services/awooop_ansible_audit_service.py +++ b/apps/api/src/services/awooop_ansible_audit_service.py @@ -692,7 +692,7 @@ _CATALOG: tuple[dict[str, Any], ...] = ( "approval_required": False, "risk_level": "medium", "canonical_asset_id": "service:openclaw:host188", - "catalog_revision": "2026-07-17-openclaw-callback-overlay-v2", + "catalog_revision": "2026-07-17-openclaw-callback-user-overlay-v3", "cross_domain_fallback_allowed": False, }, { diff --git a/apps/api/src/services/awooop_ansible_post_verifier.py b/apps/api/src/services/awooop_ansible_post_verifier.py index 578d94e3b..cb50e3297 100644 --- a/apps/api/src/services/awooop_ansible_post_verifier.py +++ b/apps/api/src/services/awooop_ansible_post_verifier.py @@ -249,63 +249,53 @@ _POSTCONDITION_REGISTRY: dict[str, tuple[AssetPostcondition, ...]] = { "configuration", "host_188", "set -euo pipefail; " - "root=/var/lib/awoooi-controlled-apply/openclaw-callback-forwarder/" + "root=/home/ollama/.local/share/awoooi-controlled-apply/" + "openclaw-callback-forwarder/" "payloads/ffc45311b6403b30cb6ae6e1aa83dae1a047e9c9 && " "manifest=\"$root/manifest.json\" && " "test -d \"$root\" && test ! -L \"$root\" && " - "test \"$(stat -c '%u:%g:%a' \"$root\")\" = '0:0:755' && " + "test \"$(stat -c '%U:%G:%a' \"$root\")\" = 'ollama:ollama:755' && " "test -f \"$manifest\" && test ! -L \"$manifest\" && " - "test \"$(stat -c '%u:%g:%a' \"$manifest\")\" = '0:0:444' && " + "test \"$(stat -c '%U:%G:%a' \"$manifest\")\" = 'ollama:ollama:444' && " "test \"$(sha256sum \"$manifest\" | cut -d' ' -f1)\" = " - "f06df6f3163e5ae7bf999624b011a3ae0bff096c9d0c8cb4c43f8b40be43ae2b", + "dafeb467b744ee7e80dc043d51f9e5d7e6905a58103f1710d519b553b56c6e48", ), AssetPostcondition( "host_188_openclaw_callback_overlay_enabled", "configuration", "host_188", "set -euo pipefail; " - "drop=/etc/systemd/system/clawbot.service.d/" - "20-awoooi-callback-forwarder.conf && " - "override=/etc/awoooi/openclaw-callback-forwarder/" - "docker-compose.callback.yml && " - "COMPOSE_FILE_key=COMPOSE_FILE && " - "test \"$COMPOSE_FILE_key\" = 'COMPOSE_FILE' && " "wd=$(systemctl show clawbot.service --property=WorkingDirectory " "--value) && " - "test -f \"$drop\" && test ! -L \"$drop\" && " - "test \"$(stat -c '%u:%g:%a' \"$drop\")\" = '0:0:644' && " - "drop_sha=$(sha256sum \"$drop\" | cut -d' ' -f1) && " - "case \"$wd:$drop_sha\" in " - "'/home/ollama/clawbot-v5:" - "419fb4f249f7e1370fbca87086fd725e57b1f5cf54da2f84caf9a21fea7111e3'" - "|'/opt/openclaw:" - "4a78d414e4b591f54232fab04529861a6042718a7627c4e17ccd5cc0cbd485f9') " - "true ;; *) false ;; esac && " + "test \"$wd\" = '/home/ollama/clawbot-v5' && " + "override=\"$wd/docker-compose.override.yml\" && " "test -f \"$override\" && test ! -L \"$override\" && " - "test \"$(stat -c '%u:%g:%a' \"$override\")\" = '0:0:444' && " + "test \"$(stat -c '%U:%G:%a' \"$override\")\" = " + "'ollama:ollama:444' && " "test \"$(sha256sum \"$override\" | cut -d' ' -f1)\" = " - "f6fa7f6724c35f86f1c8dafac07e2680231dfa29a5d45534ed0515c58ac2de77", + "8f6b7e8a0d6746c1331e99910656e64deebf67c9582bab3e1d36f71bb7a253c0", ), AssetPostcondition( "host_188_openclaw_callback_payload_runtime", "configuration", "host_188", "set -euo pipefail; " - "root=/var/lib/awoooi-controlled-apply/openclaw-callback-forwarder/" + "root=/home/ollama/.local/share/awoooi-controlled-apply/" + "openclaw-callback-forwarder/" "payloads/ffc45311b6403b30cb6ae6e1aa83dae1a047e9c9 && " "telegram=\"$root/app/bot/telegram.py\" && " "config=\"$root/app/core/config.py\" && " "forwarder=\"$root/app/services/telegram_callback_forwarder.py\" && " "test -f \"$telegram\" && test ! -L \"$telegram\" && " - "test \"$(stat -c '%u:%g:%a' \"$telegram\")\" = '0:0:444' && " + "test \"$(stat -c '%U:%G:%a' \"$telegram\")\" = 'ollama:ollama:444' && " "test \"$(sha256sum \"$telegram\" | cut -d' ' -f1)\" = " "af9600f32305ae73184651a8d3437063d7c8d35e3c3eb0910a2e27ac874c9302 && " "test -f \"$config\" && test ! -L \"$config\" && " - "test \"$(stat -c '%u:%g:%a' \"$config\")\" = '0:0:444' && " + "test \"$(stat -c '%U:%G:%a' \"$config\")\" = 'ollama:ollama:444' && " "test \"$(sha256sum \"$config\" | cut -d' ' -f1)\" = " "e1b8b633f7b4780d3078d6c42a55e49d200255d3597a34e0cd2fd649b84eaea1 && " "test -f \"$forwarder\" && test ! -L \"$forwarder\" && " - "test \"$(stat -c '%u:%g:%a' \"$forwarder\")\" = '0:0:444' && " + "test \"$(stat -c '%U:%G:%a' \"$forwarder\")\" = 'ollama:ollama:444' && " "test \"$(sha256sum \"$forwarder\" | cut -d' ' -f1)\" = " "0d7693e34f63db4491d974f9f76a3a1c793d32cda193cb55ed3d8c86297b4fe3 && " "ids=$(docker ps -q " 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 c36150730..7aefd9c6b 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 @@ -1,5 +1,6 @@ from __future__ import annotations +import hashlib import json from pathlib import Path @@ -31,6 +32,12 @@ 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_MANIFEST_SHA256 = ( + "dafeb467b744ee7e80dc043d51f9e5d7e6905a58103f1710d519b553b56c6e48" +) +EXPECTED_OVERRIDE_SHA256 = ( + "8f6b7e8a0d6746c1331e99910656e64deebf67c9582bab3e1d36f71bb7a253c0" +) PAYLOAD_ROOT = ( ROOT / "infra" @@ -201,7 +208,7 @@ def test_openclaw_catalog_and_independent_verifier_are_exact() -> None: "host_188", ] assert catalog["catalog_revision"] == ( - "2026-07-17-openclaw-callback-overlay-v2" + "2026-07-17-openclaw-callback-user-overlay-v3" ) assert catalog["cross_domain_fallback_allowed"] is False @@ -217,7 +224,8 @@ def test_openclaw_catalog_and_independent_verifier_are_exact() -> None: 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 "COMPOSE_FILE" in probes + assert "docker-compose.override.yml" in probes + assert "COMPOSE_FILE" not in probes assert "TELEGRAM_CALLBACK_FORWARD_ENABLED is True" in probes assert all(digest in probes for digest in EXPECTED_PAYLOAD_SHA256.values()) assert "http://127.0.0.1:8088/health" in probes @@ -234,15 +242,15 @@ def test_openclaw_playbook_is_check_first_bounded_and_rollback_capable() -> None assert play["hosts"] == "host_188" assert play["gather_facts"] is False - assert play["become"] is True + assert play["become"] is False 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"]["allowed_working_directories"] == [ - "/opt/openclaw", "/home/ollama/clawbot-v5", ] assert len(play["vars"]["payloads"]) == 3 + assert play["vars"]["expected_manifest_sha256"] == EXPECTED_MANIFEST_SHA256 assert { item["relative_path"]: item["target_sha256"] for item in play["vars"]["payloads"] @@ -298,23 +306,44 @@ def test_openclaw_playbook_is_check_first_bounded_and_rollback_capable() -> None assert check_receipt["ansible.builtin.debug"]["msg"][ "runtime_source_mutation_allowed" ] is False + assert check_receipt["ansible.builtin.debug"]["msg"][ + "runtime_compose_override_write_allowed" + ] is True + assert check_receipt["ansible.builtin.debug"]["msg"][ + "host_privilege_escalation_required" + ] is False assert check_receipt["ansible.builtin.debug"]["msg"][ "image_build_allowed" ] is False for name in ( - "Reload systemd and restart the existing owned service once", - "Reload systemd and restore the previous container definition", + "Activate the existing Compose project without build or pull", + "Restore the previous Compose definition without build or pull", ): - assert tasks[name]["ansible.builtin.systemd"] == { - "name": "clawbot.service", - "state": "restarted", - "daemon_reload": True, - } + assert tasks[name]["ansible.builtin.command"]["argv"] == [ + "/usr/bin/docker", + "compose", + "up", + "-d", + "--no-build", + "--pull", + "never", + ] + assert tasks[name]["args"]["chdir"] == "{{ openclaw_workdir }}" assert tasks[name]["no_log"] is True - assert tasks["Install COMPOSE_FILE-only systemd drop-in"][ - "ansible.builtin.copy" - ]["content"] == "{{ callback_dropin_content }}" + assert "Install COMPOSE_FILE-only systemd drop-in" not in tasks + rendered_override = tasks["Define exact non-secret managed overlay documents"][ + "ansible.builtin.set_fact" + ]["compose_override_content"].replace( + "{{ payload_root }}", + ( + "/home/ollama/.local/share/awoooi-controlled-apply/" + f"openclaw-callback-forwarder/payloads/{EXPECTED_SHA}" + ), + ) + assert hashlib.sha256(rendered_override.encode()).hexdigest() == ( + EXPECTED_OVERRIDE_SHA256 + ) correlation = tasks["Require same-run controlled execution correlation"][ "ansible.builtin.assert" ]["that"] @@ -333,7 +362,7 @@ def test_openclaw_playbook_is_check_first_bounded_and_rollback_capable() -> None assert "target_mounts_rollback == target_mounts_before" in tasks[ "Require bounded rollback verification" ]["ansible.builtin.assert"]["that"] - assert tasks["Reload systemd and restore the previous container definition"][ + assert tasks["Restore the previous Compose definition without build or pull"][ "when" ] == "activation_attempted | default(false) | bool" runtime_setting = tasks[ @@ -346,7 +375,7 @@ def test_openclaw_playbook_is_check_first_bounded_and_rollback_capable() -> None ]["that"] == [ "clawbot_image_after.stdout | trim == clawbot_image_before" ] - assert "openclaw_callback_overlay_rollback_v2" in source + assert "openclaw_callback_overlay_rollback_v3" in source assert "runtime_checkout_preserved" in source for forbidden in ( "git remote", @@ -364,12 +393,18 @@ def test_openclaw_playbook_is_check_first_bounded_and_rollback_capable() -> None def test_openclaw_payload_manifest_is_hash_pinned_and_overlay_only() -> None: - import hashlib - - manifest = json.loads((PAYLOAD_ROOT / "manifest.json").read_text(encoding="utf-8")) + manifest_path = PAYLOAD_ROOT / "manifest.json" + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + assert hashlib.sha256(manifest_path.read_bytes()).hexdigest() == ( + EXPECTED_MANIFEST_SHA256 + ) assert manifest["source_commit"] == EXPECTED_SHA - assert manifest["deployment_mode"] == "root_owned_read_only_overlay_no_build" + assert manifest["deployment_mode"] == ( + "host_user_owned_read_only_compose_override_no_build_no_pull" + ) assert manifest["runtime_source_mutation_allowed"] is False + assert manifest["runtime_compose_override_write_allowed"] is True + assert manifest["host_privilege_escalation_required"] is False assert manifest["runtime_secret_read_allowed"] is False assert {row["path"]: row["sha256"] for row in manifest["payloads"]} == ( EXPECTED_PAYLOAD_SHA256 diff --git a/apps/api/tests/test_host188_openclaw_callback_overlay_post_verifier_contract.py b/apps/api/tests/test_host188_openclaw_callback_overlay_post_verifier_contract.py index 275d8a9a7..00b01532a 100644 --- a/apps/api/tests/test_host188_openclaw_callback_overlay_post_verifier_contract.py +++ b/apps/api/tests/test_host188_openclaw_callback_overlay_post_verifier_contract.py @@ -69,13 +69,13 @@ def test_host188_overlay_probes_are_exact_and_secret_blind() -> None: assert "docker inspect --format='{{.Config.WorkingDir}}'" in joined assert "{{.Source}}|{{.Destination}}|{{.Type}}|{{.RW}}" in joined assert "|bind|false" in joined - assert "stat -c '%u:%g:%a'" in joined - assert "0:0:444" in joined - assert "0:0:644" in joined - assert "f06df6f3163e5ae7bf999624b011a3ae0bff096c9d0c8cb4c43f8b40be43ae2b" in joined - assert "f6fa7f6724c35f86f1c8dafac07e2680231dfa29a5d45534ed0515c58ac2de77" in joined - assert "419fb4f249f7e1370fbca87086fd725e57b1f5cf54da2f84caf9a21fea7111e3" in joined - assert "4a78d414e4b591f54232fab04529861a6042718a7627c4e17ccd5cc0cbd485f9" in joined + assert "stat -c '%U:%G:%a'" in joined + assert "ollama:ollama:444" in joined + assert "ollama:ollama:755" in joined + assert "dafeb467b744ee7e80dc043d51f9e5d7e6905a58103f1710d519b553b56c6e48" in joined + assert "8f6b7e8a0d6746c1331e99910656e64deebf67c9582bab3e1d36f71bb7a253c0" in joined + assert "docker-compose.override.yml" in joined + assert "COMPOSE_FILE" not in joined assert all(digest in joined for digest in EXPECTED_PAYLOAD_SHA256) diff --git a/infra/ansible/files/openclaw-callback-forwarder/ffc45311b6403b30cb6ae6e1aa83dae1a047e9c9/manifest.json b/infra/ansible/files/openclaw-callback-forwarder/ffc45311b6403b30cb6ae6e1aa83dae1a047e9c9/manifest.json index a344e366e..555c0419f 100644 --- a/infra/ansible/files/openclaw-callback-forwarder/ffc45311b6403b30cb6ae6e1aa83dae1a047e9c9/manifest.json +++ b/infra/ansible/files/openclaw-callback-forwarder/ffc45311b6403b30cb6ae6e1aa83dae1a047e9c9/manifest.json @@ -2,8 +2,10 @@ "schema_version": "openclaw_callback_overlay_manifest_v1", "source_repository": "gitea:wooo/clawbot-v5", "source_commit": "ffc45311b6403b30cb6ae6e1aa83dae1a047e9c9", - "deployment_mode": "root_owned_read_only_overlay_no_build", + "deployment_mode": "host_user_owned_read_only_compose_override_no_build_no_pull", "runtime_source_mutation_allowed": false, + "runtime_compose_override_write_allowed": true, + "host_privilege_escalation_required": false, "runtime_secret_read_allowed": false, "payloads": [ { diff --git a/infra/ansible/playbooks/188-openclaw-callback-forwarder.yml b/infra/ansible/playbooks/188-openclaw-callback-forwarder.yml index 31ffb24e0..60d49175c 100644 --- a/infra/ansible/playbooks/188-openclaw-callback-forwarder.yml +++ b/infra/ansible/playbooks/188-openclaw-callback-forwarder.yml @@ -1,41 +1,35 @@ --- # Host188 OpenClaw callback forwarder controlled convergence. # -# This lane never mutates the runtime checkout. It installs three immutable, -# repo-owned Python payloads into a root-owned directory and exposes them to the -# existing clawbot container as read-only file mounts. The existing image, -# service type, command, base Compose file, and runtime secrets remain untouched. +# This lane never mutates the three runtime source payloads. It installs three +# immutable, repo-owned Python payloads into an ollama-owned data directory and +# exposes them to the existing clawbot container through Compose's conventional +# docker-compose.override.yml discovery. The existing image, systemd unit, +# base Compose file, and runtime secrets remain untouched; no sudo is required. - name: Host188 OpenClaw callback forwarder controlled overlay hosts: host_188 gather_facts: false - become: true + become: false serial: 1 any_errors_fatal: true vars: catalog_id: ansible:188-openclaw-callback-forwarder canonical_asset_id: service:openclaw:host188 expected_source_sha: ffc45311b6403b30cb6ae6e1aa83dae1a047e9c9 - expected_manifest_sha256: f06df6f3163e5ae7bf999624b011a3ae0bff096c9d0c8cb4c43f8b40be43ae2b + expected_manifest_sha256: dafeb467b744ee7e80dc043d51f9e5d7e6905a58103f1710d519b553b56c6e48 allowed_working_directories: - - /opt/openclaw - /home/ollama/clawbot-v5 compose_project: clawbot compose_service: clawbot payload_source_root: >- {{ playbook_dir }}/../files/openclaw-callback-forwarder/{{ expected_source_sha }} - payload_root: >- - /var/lib/awoooi-controlled-apply/openclaw-callback-forwarder/payloads/{{ expected_source_sha }} - managed_root: /etc/awoooi/openclaw-callback-forwarder - compose_override_path: >- - /etc/awoooi/openclaw-callback-forwarder/docker-compose.callback.yml - callback_dropin_dir: /etc/systemd/system/clawbot.service.d - callback_dropin_path: >- - /etc/systemd/system/clawbot.service.d/20-awoooi-callback-forwarder.conf - receipt_root: >- - /var/lib/awoooi-controlled-apply/openclaw-callback-forwarder/receipts - rollback_root: >- - /var/lib/awoooi-controlled-apply/openclaw-callback-forwarder/rollback + managed_base: >- + /home/ollama/.local/share/awoooi-controlled-apply/openclaw-callback-forwarder + payload_root: "{{ managed_base }}/payloads/{{ expected_source_sha }}" + compose_override_path: "{{ openclaw_workdir }}/docker-compose.override.yml" + receipt_root: "{{ managed_base }}/receipts" + rollback_root: "{{ managed_base }}/rollback" trace_id: "{{ controlled_trace_id | default('') }}" run_id: "{{ controlled_run_id | default('') }}" work_item_id: "{{ controlled_work_item_id | default('') }}" @@ -68,6 +62,8 @@ baseline_sha256: "" target_sha256: 0d7693e34f63db4491d974f9f76a3a1c793d32cda193cb55ed3d8c86297b4fe3 managed_directories: + - path: "{{ managed_base }}" + mode: "0755" - path: "{{ payload_root }}" mode: "0755" - path: "{{ payload_root }}/app" @@ -78,10 +74,6 @@ mode: "0755" - path: "{{ payload_root }}/app/services" mode: "0755" - - path: "{{ managed_root }}" - mode: "0755" - - path: "{{ callback_dropin_dir }}" - mode: "0755" - path: "{{ receipt_root }}" mode: "0755" - path: "{{ rollback_root }}" @@ -410,9 +402,9 @@ and not (managed_directories_before.results[directory_index].stat.islnk | default(false)) and (managed_directories_before.results[directory_index].stat.pw_name - | default('')) == 'root' + | default('')) == 'ollama' and (managed_directories_before.results[directory_index].stat.gr_name - | default('')) == 'root' + | default('')) == 'ollama' and (managed_directories_before.results[directory_index].stat.mode | default('')) == item.mode ) @@ -431,13 +423,12 @@ get_checksum: true loop: - "{{ compose_override_path }}" - - "{{ callback_dropin_path }}" - "{{ payload_root }}/manifest.json" register: managed_artifacts_before check_mode: false no_log: true - - name: Inspect existing root-owned overlay payloads + - name: Inspect existing user-owned overlay payloads ansible.builtin.stat: path: "{{ item.overlay_path }}" checksum_algorithm: sha256 @@ -449,7 +440,7 @@ check_mode: false no_log: true - - name: Fail closed on foreign root-owned overlay payloads + - name: Fail closed on foreign user-owned overlay payloads ansible.builtin.assert: that: - >- @@ -465,9 +456,9 @@ and (overlay_payload_before.results[payload_index].stat.mode | default('')) == '0444' and (overlay_payload_before.results[payload_index].stat.pw_name - | default('')) == 'root' + | default('')) == 'ollama' and (overlay_payload_before.results[payload_index].stat.gr_name - | default('')) == 'root' + | default('')) == 'ollama' ) fail_msg: openclaw_overlay_payload_foreign_owner loop: "{{ payloads }}" @@ -503,9 +494,6 @@ - "{{ payload_root }}/app/bot/telegram.py:/app/app/bot/telegram.py:ro" - "{{ payload_root }}/app/core/config.py:/app/app/core/config.py:ro" - "{{ payload_root }}/app/services/telegram_callback_forwarder.py:/app/app/services/telegram_callback_forwarder.py:ro" - callback_dropin_content: | - [Service] - Environment="COMPOSE_FILE={{ openclaw_workdir }}/docker-compose.yml:{{ compose_override_path }}" changed_when: false - name: Fail closed on foreign pre-existing managed artifacts @@ -527,38 +515,20 @@ and not (managed_artifacts_before.results[0].stat.islnk | default(false) | bool) and (managed_artifacts_before.results[0].stat.mode | default('')) == '0444' - and (managed_artifacts_before.results[0].stat.pw_name | default('')) == 'root' - and (managed_artifacts_before.results[0].stat.gr_name | default('')) == 'root' + and (managed_artifacts_before.results[0].stat.pw_name | default('')) == 'ollama' + and (managed_artifacts_before.results[0].stat.gr_name | default('')) == 'ollama' - >- not (managed_artifacts_before.results[1].stat.exists | default(false)) or ( - managed_artifact_contents_before.results - | selectattr('item.item', 'equalto', callback_dropin_path) - | map(attribute='content') - | map('b64decode') - | list - | first - | default('') - ) == callback_dropin_content - and (managed_artifacts_before.results[1].stat.isreg - | default(false) | bool) - and not (managed_artifacts_before.results[1].stat.islnk - | default(false) | bool) - and (managed_artifacts_before.results[1].stat.mode | default('')) == '0644' - and (managed_artifacts_before.results[1].stat.pw_name | default('')) == 'root' - and (managed_artifacts_before.results[1].stat.gr_name | default('')) == 'root' - - >- - not (managed_artifacts_before.results[2].stat.exists | default(false)) - or ( - (managed_artifacts_before.results[2].stat.checksum | default('')) + (managed_artifacts_before.results[1].stat.checksum | default('')) == expected_manifest_sha256 - and (managed_artifacts_before.results[2].stat.isreg + and (managed_artifacts_before.results[1].stat.isreg | default(false) | bool) - and not (managed_artifacts_before.results[2].stat.islnk + and not (managed_artifacts_before.results[1].stat.islnk | default(false) | bool) - and (managed_artifacts_before.results[2].stat.mode | default('')) == '0444' - and (managed_artifacts_before.results[2].stat.pw_name | default('')) == 'root' - and (managed_artifacts_before.results[2].stat.gr_name | default('')) == 'root' + and (managed_artifacts_before.results[1].stat.mode | default('')) == '0444' + and (managed_artifacts_before.results[1].stat.pw_name | default('')) == 'ollama' + and (managed_artifacts_before.results[1].stat.gr_name | default('')) == 'ollama' ) fail_msg: openclaw_managed_overlay_foreign_owner changed_when: false @@ -664,8 +634,9 @@ {{ managed_artifacts_before.results[0].stat.exists | default(false) and managed_artifacts_before.results[1].stat.exists | default(false) - and managed_artifacts_before.results[2].stat.exists | default(false) }} + runtime_checkout_delta_expected: >- + {{ 0 if (managed_artifacts_before.results[0].stat.exists | default(false)) else 1 }} changed_when: false - name: Calculate one bounded no-build apply decision @@ -682,7 +653,7 @@ - name: Publish check-mode overlay diff and no-write receipt ansible.builtin.debug: msg: - schema_version: openclaw_callback_overlay_check_v2 + schema_version: openclaw_callback_overlay_check_v3 catalog_id: "{{ catalog_id }}" canonical_asset_id: "{{ canonical_asset_id }}" typed_domain: docker_compose_container @@ -693,6 +664,9 @@ source_artifact_commit: "{{ expected_source_sha }}" runtime_source_dirty_count: "{{ runtime_dirty_count_before.stdout | int }}" runtime_source_mutation_allowed: false + runtime_compose_override_write_allowed: true + host_privilege_escalation_required: false + expected_git_dirty_delta: "{{ runtime_checkout_delta_expected | int }}" image_before: "{{ clawbot_image_before }}" image_build_allowed: false overlay_payload_count: 3 @@ -708,31 +682,28 @@ changed_when: false when: not ansible_check_mode - - name: Apply root-owned callback overlay with bounded rollback + - name: Apply user-owned callback overlay with bounded rollback when: not ansible_check_mode block: - name: Create durable receipt and payload directories ansible.builtin.file: - path: "{{ item }}" + path: "{{ item.path }}" state: directory - owner: root - group: root - mode: "0755" - loop: - - "{{ payload_root }}/app/bot" - - "{{ payload_root }}/app/core" - - "{{ payload_root }}/app/services" - - "{{ managed_root }}" - - "{{ callback_dropin_dir }}" - - "{{ receipt_root }}" - - "{{ rollback_root }}/{{ run_id }}" + owner: ollama + group: ollama + mode: "{{ item.mode }}" + loop: >- + {{ managed_directories + + [{'path': rollback_root ~ '/' ~ run_id, 'mode': '0755'}] }} + loop_control: + label: "{{ item.path }}" - name: Install exact read-only payload files ansible.builtin.copy: src: "{{ item.source_path }}" dest: "{{ item.overlay_path }}" - owner: root - group: root + owner: ollama + group: ollama mode: "0444" loop: "{{ payloads }}" loop_control: @@ -743,8 +714,8 @@ ansible.builtin.copy: src: "{{ payload_source_root }}/manifest.json" dest: "{{ payload_root }}/manifest.json" - owner: root - group: root + owner: ollama + group: ollama mode: "0444" - name: Verify installed overlay manifest checksum @@ -789,33 +760,34 @@ index_var: payload_index changed_when: false - - name: Install root-owned non-secret Compose override + - name: Install user-owned non-secret Compose override ansible.builtin.copy: dest: "{{ compose_override_path }}" - owner: root - group: root + owner: ollama + group: ollama mode: "0444" content: "{{ compose_override_content }}" - - name: Install COMPOSE_FILE-only systemd drop-in - ansible.builtin.copy: - dest: "{{ callback_dropin_path }}" - owner: root - group: root - mode: "0644" - content: "{{ callback_dropin_content }}" - - name: Mark the bounded service activation attempt ansible.builtin.set_fact: activation_attempted: true changed_when: false when: openclaw_apply_required | bool - - name: Reload systemd and restart the existing owned service once - ansible.builtin.systemd: - name: clawbot.service - state: restarted - daemon_reload: true + - name: Activate the existing Compose project without build or pull + ansible.builtin.command: + argv: + - /usr/bin/docker + - compose + - up + - -d + - --no-build + - --pull + - never + args: + chdir: "{{ openclaw_workdir }}" + register: compose_activation + changed_when: true when: openclaw_apply_required | bool no_log: true @@ -1041,11 +1013,14 @@ changed_when: false no_log: true - - name: Require runtime checkout preservation + - name: Require only the bounded managed override checkout delta ansible.builtin.assert: that: - - runtime_dirty_count_after.stdout | int == runtime_dirty_count_before.stdout | int - fail_msg: openclaw_runtime_checkout_drift_changed + - >- + runtime_dirty_count_after.stdout | int + == (runtime_dirty_count_before.stdout | int) + + (runtime_checkout_delta_expected | int) + fail_msg: openclaw_runtime_checkout_delta_outside_managed_override changed_when: false - name: Re-read only the three runtime target paths after apply @@ -1085,12 +1060,12 @@ - name: Save durable technical apply receipt ansible.builtin.copy: dest: "{{ receipt_root }}/{{ run_id }}.json" - owner: root - group: root + owner: ollama + group: ollama mode: "0600" content: | { - "schema_version": "openclaw_callback_overlay_apply_v2", + "schema_version": "openclaw_callback_overlay_apply_v3", "catalog_id": "{{ catalog_id }}", "canonical_asset_id": "{{ canonical_asset_id }}", "trace_id": "{{ trace_id }}", @@ -1102,7 +1077,9 @@ "payload_count": 3, "runtime_source_dirty_count_before": {{ runtime_dirty_count_before.stdout | int }}, "runtime_source_dirty_count_after": {{ runtime_dirty_count_after.stdout | int }}, - "runtime_source_mutated": false, + "runtime_payload_source_mutated": false, + "runtime_checkout_mutated_by_managed_override": true, + "runtime_checkout_expected_delta": {{ runtime_checkout_delta_expected | int }}, "image_rebuilt": false, "technical_apply_verified": true, "real_callback_receipt_required_for_incident_closure": true, @@ -1113,7 +1090,7 @@ - name: Publish technical apply and pending real-callback receipt ansible.builtin.debug: msg: - schema_version: openclaw_callback_overlay_apply_v2 + schema_version: openclaw_callback_overlay_apply_v3 catalog_id: "{{ catalog_id }}" canonical_asset_id: "{{ canonical_asset_id }}" typed_domain: docker_compose_container @@ -1124,7 +1101,8 @@ source_artifact_commit: "{{ expected_source_sha }}" apply_performed: "{{ openclaw_apply_required }}" image_identity_unchanged: true - runtime_checkout_preserved: true + runtime_payload_sources_preserved: true + managed_compose_override_persistent: true technical_apply_verified: true runtime_closure_verified: false next_state: partial_waiting_real_callback_receipt @@ -1133,30 +1111,6 @@ changed_when: false rescue: - - name: Remove managed callback systemd drop-in after failed activation - ansible.builtin.file: - path: "{{ callback_dropin_path }}" - state: absent - when: not (managed_artifacts_before.results[1].stat.exists | default(false)) - - - name: Restore exact pre-existing managed systemd drop-in - ansible.builtin.copy: - dest: "{{ callback_dropin_path }}" - owner: root - group: root - mode: "0644" - content: >- - {{ - managed_artifact_contents_before.results - | selectattr('item.item', 'equalto', callback_dropin_path) - | map(attribute='content') - | map('b64decode') - | list - | first - }} - when: managed_artifacts_before.results[1].stat.exists | default(false) - no_log: true - - name: Remove managed Compose override after failed activation ansible.builtin.file: path: "{{ compose_override_path }}" @@ -1166,8 +1120,8 @@ - name: Restore exact pre-existing managed Compose override ansible.builtin.copy: dest: "{{ compose_override_path }}" - owner: root - group: root + owner: ollama + group: ollama mode: "0444" content: >- {{ @@ -1198,13 +1152,21 @@ path: "{{ payload_root }}/manifest.json" state: absent when: >- - not (managed_artifacts_before.results[2].stat.exists | default(false)) + not (managed_artifacts_before.results[1].stat.exists | default(false)) - - name: Reload systemd and restore the previous container definition - ansible.builtin.systemd: - name: clawbot.service - state: restarted - daemon_reload: true + - name: Restore the previous Compose definition without build or pull + ansible.builtin.command: + argv: + - /usr/bin/docker + - compose + - up + - -d + - --no-build + - --pull + - never + args: + chdir: "{{ openclaw_workdir }}" + changed_when: true when: activation_attempted | default(false) | bool no_log: true @@ -1348,12 +1310,12 @@ - name: Save durable verified rollback receipt ansible.builtin.copy: dest: "{{ rollback_root }}/{{ run_id }}/rollback-receipt.json" - owner: root - group: root + owner: ollama + group: ollama mode: "0600" content: | { - "schema_version": "openclaw_callback_overlay_rollback_v2", + "schema_version": "openclaw_callback_overlay_rollback_v3", "catalog_id": "{{ catalog_id }}", "canonical_asset_id": "{{ canonical_asset_id }}", "trace_id": "{{ trace_id }}",