opencode.plist.j2 exports PATH from acp_opencode_path, but the var was never
defined (codex/gemini define their *_path), causing AnsibleUndefinedVariable on
the 'Create launchd plist template for OpenCode ACP' task. Add
acp_opencode_npm_global_bin + acp_opencode_path mirroring gemini (macOS uses
~/.local/bin + Homebrew + system paths).
The OpenCode 'Ensure Caddy conf directory exists' task ran unconditionally
(owner root), unlike codex which gates on acp_*_manage_caddy. On macOS (caddy
off by default, manage_caddy=false) it templated the caddy path and tried to
create/chown the dir as root. Gate it on acp_opencode_manage_caddy like codex,
so macOS single-host skips Caddy entirely.
caddy_base_dir was {{ /opt/homebrew/etc/caddy if ansible_os_family == Darwin
else /etc/caddy }} with unquoted paths/value, so Jinja parsed '/' as division
-> 'unexpected /' templating error (hit on acp_opencode). Quote the literals:
'{{ "/opt/homebrew/etc/caddy" if ansible_os_family == "Darwin" else "/etc/caddy" }}'
across the 9 affected roles.
The opencode/gemini/hermes install tasks ran ansible.builtin.apt unconditionally,
so apt-get update fired even on macOS (no apt) and with empty package lists.
Gate them on 'packages | length > 0' and ansible_os_family == 'Debian' (apt is
Debian-only), matching codex. macOS skips apt entirely.
ss is Linux-only; on macOS it is absent and the validate tasks failed with
[Errno 2] No such file or directory: ss. Use ss when present, else lsof
(-nP -iTCP -sTCP:LISTEN). The acp diagnostic checks are also made non-fatal;
the bridge capture keeps host:port output so its port assertion still matches.
Add caddy_config_dir = /etc/caddy on Linux, /opt/homebrew/etc/caddy on macOS.
Derive the Caddyfile / conf.d / fragment paths in the caddy role and the
gateway_openclaw/litellm/xworkmate_bridge roles from it, so a force-enabled
Caddy (caddy_enabled=true) on macOS writes to the Homebrew location instead of
the unwritable /etc/caddy. Default (caddy_enabled=false on macOS) still skips
Caddy entirely.
litellm: the Caddy fragment-dir task missed the gate its siblings had; gate it
on caddy_enabled + litellm_caddy_config_enabled. xworkmate_bridge: wrap the
whole Caddy ingress block in caddy_enabled so macOS single-host never touches
/etc/caddy (the bridge service task stays outside the block).
Add caddy_enabled (group_vars/all) defaulting to ansible_os_family != 'Darwin',
overridable via -e caddy_enabled=true/false. Wrap the dedicated caddy role and
the gateway_openclaw Caddy ingress block in 'when: caddy_enabled | bool' so
macOS single-host deploys never touch /etc/caddy or start caddy, while Linux
VPS deploys keep Caddy + HTTP/TLS by default. Notifies only fire from gated
tasks, so the Reload caddy handlers stay inert when disabled.
On macOS the compile cache is still being written by OpenClaw while ansible
removes it, so shutil.rmtree fails with [Errno 66] Directory not empty. Retry
the deletion (5x, 3s) until the directory is gone.