--- - name: Setup AI Agentic Workspace runtime hosts: "{{ xworkspace_console_hosts | default('all') }}" become: true gather_facts: true vars: xworkspace_console_user: ubuntu xworkspace_console_home: /home/ubuntu xworkspace_console_root: /home/ubuntu/xworkspace xworkspace_console_portal_dir: /home/ubuntu/xworkspace/portal xworkspace_console_scripts_dir: /home/ubuntu/xworkspace/scripts xworkspace_console_portal_url: http://localhost:17000 xworkspace_console_portal_port: 17000 xworkspace_console_ttyd_port: 7681 xworkspace_console_enable_ttyd: true xworkspace_console_install_chrome: true xworkspace_console_autostart_enabled: true xworkspace_console_ttyd_binary_path: /usr/local/bin/ttyd xworkspace_console_dashboard_local_src: /Users/shenlan/workspaces/ai-workspace-lab/xworkspace-console/dashboard tasks: - name: Install Google Chrome apt repository prerequisites ansible.builtin.apt: name: - ca-certificates - curl - gnupg - xdg-utils state: present install_recommends: false update_cache: true - name: Ensure Google Chrome apt keyring directory exists ansible.builtin.file: path: /etc/apt/keyrings state: directory owner: root group: root mode: "0755" - name: Install Google Linux signing key ansible.builtin.shell: | set -euo pipefail tmp="$(mktemp)" curl -fsSL "https://dl.google.com/linux/linux_signing_key.pub" -o "$tmp" gpg --dearmor -o /etc/apt/keyrings/google-linux-signing-key.gpg "$tmp" rm -f "$tmp" chmod 0644 /etc/apt/keyrings/google-linux-signing-key.gpg args: executable: /bin/bash creates: /etc/apt/keyrings/google-linux-signing-key.gpg - name: Configure Google Chrome apt repository ansible.builtin.copy: dest: /etc/apt/sources.list.d/google-chrome.list owner: root group: root mode: "0644" content: "deb [arch=amd64 signed-by=/etc/apt/keyrings/google-linux-signing-key.gpg] https://dl.google.com/linux/chrome/deb/ stable main\n" - name: Refresh apt cache after Google Chrome repository changes ansible.builtin.apt: update_cache: true - name: Install AI Agentic Workspace runtime packages ansible.builtin.apt: update_cache: true name: - xfce4 - python3 - google-chrome-stable state: present - name: Ensure ttyd binary target directory exists ansible.builtin.file: path: "{{ xworkspace_console_ttyd_binary_path | dirname }}" state: directory owner: root group: root mode: "0755" - name: Set ttyd binary download metadata ansible.builtin.set_fact: xworkspace_console_ttyd_arch: >- {{ 'x86_64' if ansible_architecture in ['x86_64', 'amd64'] else 'aarch64' if ansible_architecture in ['aarch64', 'arm64'] else '' }} - name: Fail when ttyd binary architecture is unsupported ansible.builtin.fail: msg: "Unsupported architecture for ttyd binary: {{ ansible_architecture }}" when: xworkspace_console_ttyd_arch == '' - name: Download ttyd release binary ansible.builtin.get_url: url: "https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.{{ xworkspace_console_ttyd_arch }}" dest: "{{ xworkspace_console_ttyd_binary_path }}" mode: "0755" owner: root group: root force: true - name: Verify ttyd binary ansible.builtin.command: "{{ xworkspace_console_ttyd_binary_path }} --version" register: xworkspace_console_ttyd_version changed_when: false - name: Show ttyd binary version ansible.builtin.debug: var: xworkspace_console_ttyd_version.stdout - name: Ensure AI Agentic Workspace user exists ansible.builtin.user: name: "{{ xworkspace_console_user }}" state: present create_home: true shell: /bin/bash when: xworkspace_console_user != 'root' - name: Ensure AI Agentic Workspace directories exist ansible.builtin.file: path: "{{ item }}" state: directory owner: "{{ xworkspace_console_user }}" group: "{{ xworkspace_console_user }}" mode: "0755" loop: - "{{ xworkspace_console_root }}" - "{{ xworkspace_console_portal_dir }}" - "{{ xworkspace_console_scripts_dir }}" - "{{ xworkspace_console_home }}/.config" - "{{ xworkspace_console_home }}/.config/autostart" - "{{ xworkspace_console_home }}/.config/systemd" - "{{ xworkspace_console_home }}/.config/systemd/user" - "{{ xworkspace_console_home }}/.config/systemd/user/default.target.wants" - "{{ xworkspace_console_home }}/.config/systemd/user/timers.target.wants" - name: Deploy AI Agentic Workspace status generator ansible.builtin.copy: dest: "{{ xworkspace_console_scripts_dir }}/generate-status.py" owner: "{{ xworkspace_console_user }}" group: "{{ xworkspace_console_user }}" mode: "0755" content: | #!/usr/bin/env python3 from __future__ import annotations import datetime as dt import json import os import re import subprocess import sys from pathlib import Path from urllib import request, error HOME = Path("{{ xworkspace_console_home }}") ROOT = Path("{{ xworkspace_console_root }}") PORTAL_DIR = Path("{{ xworkspace_console_portal_dir }}") LITELLM_CONFIG = HOME / ".local/share/xworkspace/litellm-config.yaml" OUTPUT = PORTAL_DIR / "status.json" def run(*cmd: str) -> str: try: return subprocess.check_output(cmd, text=True, stderr=subprocess.DEVNULL).strip() except Exception: return "" def systemd_state(unit: str, scope: str = "--user") -> str: cmd = ["systemctl"] if scope == "--user": cmd.append("--user") cmd += ["is-active", unit] return run(*cmd) or "unknown" def health(url: str) -> tuple[str, str]: try: with request.urlopen(url, timeout=2) as response: payload = response.read().decode("utf-8", "replace").strip() return ("active" if response.status < 400 else "degraded", payload[:160] or "ok") except error.HTTPError as exc: return ("degraded", f"HTTP {exc.code}") except Exception as exc: return ("inactive", str(exc).splitlines()[0][:160]) def probe_unit(name: str, label: str, group: str, endpoint: str | None = None, candidates: list[str] | None = None, scope: str = "--user") -> dict: candidates = candidates or [name] state = "unknown" found = None for candidate in candidates: state = systemd_state(candidate, scope=scope) if state != "unknown": found = candidate break details = f"{label} {state}" if endpoint: endpoint_state, endpoint_detail = health(endpoint) if endpoint_state == "active" and state in {"active", "unknown"}: state = "active" elif endpoint_state == "degraded" and state == "active": state = "degraded" details = endpoint_detail return { "id": found or name, "name": found or name, "label": label, "state": state, "details": details, "endpoint": endpoint, "group": group, } def parse_models(path: Path) -> list[dict]: if not path.exists(): return [] text = path.read_text(encoding="utf-8", errors="replace") models = [] current = None for line in text.splitlines(): match = re.search(r"^\s*model_name:\s*(.+?)\s*$", line) if match: current = match.group(1).strip().strip('"\'') models.append({ "name": current, "state": "active", "details": "Detected from litellm-config.yaml", }) return models services = [ probe_unit("xworkspace-portal.service", "portal", "core", "{{ xworkspace_console_portal_url }}", ["xworkspace-portal.service"], "--user"), probe_unit("xworkspace-chrome.service", "chrome", "workspace", None, ["xworkspace-chrome.service"], "--user"), probe_unit("xworkspace-litellm.service", "litellm", "core", "http://127.0.0.1:4000/health/liveliness", ["xworkspace-litellm.service", "litellm.service"], "--user"), probe_unit("xworkmate-bridge.service", "xworkmate-bridge", "workspace", None, ["xworkmate-bridge.service"], "--user"), probe_unit("openclaw-gateway.service", "openclaw", "workspace", None, ["openclaw-gateway.service", "openclaw.service"], "--user"), probe_unit("hermes-gateway.service", "hermes", "workspace", None, ["hermes-gateway.service", "hermes.service"], "--user"), probe_unit("qmd.service", "QMD", "workspace", None, ["qmd.service", "QMD.service", "xworkspace-qmd.service"], "--user"), probe_unit("xworkspace-ttyd.service", "ttyd", "workspace", "http://127.0.0.1:7681", ["xworkspace-ttyd.service"], "--user"), probe_unit("ttyd.service", "ttyd(system)", "workspace", "http://127.0.0.1:7681", ["ttyd.service"], "system"), ] litellm = next((item for item in services if item["label"] == "litellm"), None) terminal = next((item for item in services if item["label"].startswith("ttyd")), None) chrome = next((item for item in services if item["label"] == "chrome"), None) output = { "generatedAt": dt.datetime.now(dt.timezone.utc).isoformat(), "summary": "Workspace healthy" if all(item["state"] == "active" for item in services if item["label"] != "QMD") else "Workspace degraded", "services": services, "models": parse_models(LITELLM_CONFIG), "terminalTranscript": "ubuntu@workspace:~$ openclaw status\nstatus snapshot generated from live probes", "latencySummary": "live probe", "latencySource": litellm["details"] if litellm else "no litellm probe", "rpmSummary": "live probe", "rpmSource": "xworkspace-litellm.service", "costSummary": "live probe", "costSource": "litellm-config.yaml", "chromeState": chrome["state"] if chrome else "unknown", "chromeDetails": chrome["details"] if chrome else "No chrome probe", "terminalState": terminal["state"] if terminal else "unknown", "terminalDetails": terminal["details"] if terminal else "No ttyd probe", } OUTPUT.parent.mkdir(parents=True, exist_ok=True) tmp = OUTPUT.with_suffix(".json.tmp") tmp.write_text(json.dumps(output, indent=2, sort_keys=True), encoding="utf-8") os.replace(tmp, OUTPUT) print(f"Wrote {OUTPUT}") - name: Deploy AI Agentic Workspace status service ansible.builtin.copy: dest: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-status.service" owner: "{{ xworkspace_console_user }}" group: "{{ xworkspace_console_user }}" mode: "0644" content: | [Unit] Description=AI Agentic Workspace status snapshot generator After=xworkspace-portal.service xworkspace-litellm.service Wants=xworkspace-portal.service xworkspace-litellm.service [Service] Type=oneshot ExecStart={{ xworkspace_console_scripts_dir }}/generate-status.py - name: Deploy AI Agentic Workspace status timer ansible.builtin.copy: dest: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-status.timer" owner: "{{ xworkspace_console_user }}" group: "{{ xworkspace_console_user }}" mode: "0644" content: | [Unit] Description=Refresh AI Agentic Workspace status snapshot periodically [Timer] OnBootSec=5 OnUnitActiveSec=8 Unit=xworkspace-status.service Persistent=true [Install] WantedBy=timers.target - name: Deploy Xinit entrypoint for AI Agentic Workspace ansible.builtin.copy: dest: "{{ xworkspace_console_home }}/.xinitrc" owner: "{{ xworkspace_console_user }}" group: "{{ xworkspace_console_user }}" mode: "0755" content: | #!/usr/bin/env bash set -euo pipefail export XDG_CURRENT_DESKTOP=XFCE export DESKTOP_SESSION=xfce export BROWSER={{ xworkspace_console_scripts_dir }}/chrome-app.sh mkdir -p "$HOME/.config/xworkspace" if [ -z "${XAUTHORITY:-}" ]; then export XAUTHORITY="$HOME/.Xauthority" fi cat > "$HOME/.config/xworkspace/session.env" </dev/null 2>&1 || true dbus-update-activation-environment --systemd DISPLAY XAUTHORITY XDG_RUNTIME_DIR DBUS_SESSION_BUS_ADDRESS >/dev/null 2>&1 || true systemctl --user daemon-reload >/dev/null 2>&1 || true systemctl --user start xworkspace-console.target >/dev/null 2>&1 || true exec dbus-launch --exit-with-session startxfce4 - name: Point Xsession to Xinit entrypoint ansible.builtin.copy: dest: "{{ xworkspace_console_home }}/.xsession" owner: "{{ xworkspace_console_user }}" group: "{{ xworkspace_console_user }}" mode: "0755" content: | #!/usr/bin/env bash exec "$HOME/.xinitrc" - name: Deploy AI Agentic Workspace console launcher script ansible.builtin.copy: dest: "{{ xworkspace_console_scripts_dir }}/start.sh" owner: "{{ xworkspace_console_user }}" group: "{{ xworkspace_console_user }}" mode: "0755" content: | #!/usr/bin/env bash set -euo pipefail exit 0 - name: Deploy AI Agentic Workspace Chrome launcher wrapper ansible.builtin.copy: dest: "{{ xworkspace_console_scripts_dir }}/chrome-app.sh" owner: "{{ xworkspace_console_user }}" group: "{{ xworkspace_console_user }}" mode: "0755" content: | #!/usr/bin/env bash set -euo pipefail mkdir -p "$HOME/.config/xworkspace-chrome" exec /usr/bin/google-chrome \ --app=http://localhost:17000 \ --user-data-dir="$HOME/.config/xworkspace-chrome" \ --profile-directory=Default \ --no-first-run \ --disable-session-crashed-bubble \ --disable-sync \ --new-window - name: Sync dashboard source from controller to target ansible.posix.synchronize: src: "{{ xworkspace_console_dashboard_local_src }}/" dest: "{{ xworkspace_console_local_dashboard_dir }}/" delete: true recursive: true rsync_opts: --exclude=node_modules --exclude=.git --exclude=dist become: false delegate_to: localhost - name: Build dashboard assets on target ansible.builtin.shell: | cd "{{ xworkspace_console_local_dashboard_dir }}" && npm install && npm run build become: false - name: Sync dashboard dist to portal directory ansible.builtin.shell: | mkdir -p "{{ xworkspace_console_portal_dir }}/dist" cp -r "{{ xworkspace_console_local_dashboard_dir }}/dist/"* "{{ xworkspace_console_portal_dir }}/dist/" cp "{{ xworkspace_console_local_dashboard_dir }}/package.json" "{{ xworkspace_console_portal_dir }}/" cp "{{ xworkspace_console_local_dashboard_dir }}/vite.config.ts" "{{ xworkspace_console_portal_dir }}/" 2>/dev/null || true become: false - name: Deploy AI Agentic Workspace portal service ansible.builtin.copy: dest: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-portal.service" owner: "{{ xworkspace_console_user }}" group: "{{ xworkspace_console_user }}" mode: "0644" content: | [Unit] Description=AI Agentic Workspace Portal After=graphical-session.target Wants=graphical-session.target [Service] Type=simple WorkingDirectory={{ xworkspace_console_portal_dir }} ExecStart=/usr/bin/npm run preview -- --host 127.0.0.1 --port {{ xworkspace_console_portal_port }} Restart=always RestartSec=2 [Install] WantedBy=default.target - name: Deploy AI Agentic Workspace ttyd service ansible.builtin.copy: dest: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-ttyd.service" owner: "{{ xworkspace_console_user }}" group: "{{ xworkspace_console_user }}" mode: "0644" content: | [Unit] Description=AI Agentic Workspace ttyd After=graphical-session.target Wants=graphical-session.target [Service] Type=simple ExecStart=/usr/bin/ttyd -i lo -p {{ xworkspace_console_ttyd_port }} -O login bash Restart=always RestartSec=2 [Install] WantedBy=default.target - name: Deploy AI Agentic Workspace Chrome service ansible.builtin.copy: dest: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-chrome.service" owner: "{{ xworkspace_console_user }}" group: "{{ xworkspace_console_user }}" mode: "0644" content: | [Unit] Description=AI Agentic Workspace Chrome App After=graphical-session.target xworkspace-portal.service Requires=xworkspace-portal.service Wants=graphical-session.target [Service] Type=simple Environment=HOME={{ xworkspace_console_home }} EnvironmentFile=%h/.config/xworkspace/session.env ExecStart=/bin/bash -lc 'mkdir -p "$HOME/.config/xworkspace-chrome" && exec /usr/bin/google-chrome --app={{ xworkspace_console_portal_url }} --user-data-dir="$HOME/.config/xworkspace-chrome" --profile-directory=Default --no-first-run --disable-session-crashed-bubble --disable-sync --new-window' Restart=always RestartSec=2 [Install] WantedBy=default.target - name: Deploy AI Agentic Workspace target ansible.builtin.copy: dest: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-console.target" owner: "{{ xworkspace_console_user }}" group: "{{ xworkspace_console_user }}" mode: "0644" content: | [Unit] Description=AI Agentic Workspace Console Wants=xworkspace-portal.service xworkspace-ttyd.service xworkspace-chrome.service xworkspace-litellm.service xworkspace-status.service xworkmate-bridge.service openclaw-gateway.service hermes-gateway.service After=graphical-session.target [Install] WantedBy=default.target - name: Enable AI Agentic Workspace target ansible.builtin.file: src: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-console.target" dest: "{{ xworkspace_console_home }}/.config/systemd/user/default.target.wants/xworkspace-console.target" state: link become_user: "{{ xworkspace_console_user }}" - name: Enable AI Agentic Workspace portal service ansible.builtin.file: src: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-portal.service" dest: "{{ xworkspace_console_home }}/.config/systemd/user/default.target.wants/xworkspace-portal.service" state: link become_user: "{{ xworkspace_console_user }}" - name: Enable AI Agentic Workspace ttyd service ansible.builtin.file: src: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-ttyd.service" dest: "{{ xworkspace_console_home }}/.config/systemd/user/default.target.wants/xworkspace-ttyd.service" state: link become_user: "{{ xworkspace_console_user }}" - name: Enable AI Agentic Workspace Chrome service ansible.builtin.file: src: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-chrome.service" dest: "{{ xworkspace_console_home }}/.config/systemd/user/default.target.wants/xworkspace-chrome.service" state: link become_user: "{{ xworkspace_console_user }}" - name: Enable AI Agentic Workspace LiteLLM service ansible.builtin.file: src: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-litellm.service" dest: "{{ xworkspace_console_home }}/.config/systemd/user/default.target.wants/xworkspace-litellm.service" state: link become_user: "{{ xworkspace_console_user }}" - name: Enable AI Agentic Workspace status timer ansible.builtin.file: src: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-status.timer" dest: "{{ xworkspace_console_home }}/.config/systemd/user/timers.target.wants/xworkspace-status.timer" state: link become_user: "{{ xworkspace_console_user }}" - name: Kill legacy python http.server on port 7000 ansible.builtin.shell: | pid=$(lsof -ti:7000 2>/dev/null || true) if [ -n "$pid" ]; then kill "$pid" 2>/dev/null || true sleep 1 fi become_user: "{{ xworkspace_console_user }}" - name: Reload systemd user daemon ansible.builtin.shell: | su - {{ xworkspace_console_user }} -c "systemctl --user daemon-reload" become: true - name: Restart xworkspace-portal service ansible.builtin.shell: | su - {{ xworkspace_console_user }} -c "systemctl --user restart xworkspace-portal.service" become: true - name: Restart xworkspace-chrome service ansible.builtin.shell: | su - {{ xworkspace_console_user }} -c "systemctl --user restart xworkspace-chrome.service" become: true - name: Restart xworkspace-ttyd service ansible.builtin.shell: | su - {{ xworkspace_console_user }} -c "systemctl --user restart xworkspace-ttyd.service" become: true - name: Hide XFCE desktop icons ansible.builtin.command: xfconf-query -c xfce4-desktop -p /desktop-icons/style -t int -s 0 --create changed_when: true failed_when: false - name: Ensure portal index exists ansible.builtin.copy: dest: "{{ xworkspace_console_portal_dir }}/index.html" owner: "{{ xworkspace_console_user }}" group: "{{ xworkspace_console_user }}" mode: "0644" content: | AI Agentic Workspace Console

Workspace

Live control plane status from /status.json.

Loading live status...

Services

OpenClaw, Bridge, LiteLLM, Vault, ttyd, Chrome, and related workspace units.

Models

Detected directly from the LiteLLM config snapshot.

Embedded Terminal

Waiting for terminal snapshot...
Loading live terminal snapshot...
- name: Ensure portal tabs config exists ansible.builtin.copy: dest: "{{ xworkspace_console_portal_dir }}/portal-tabs.json" owner: "{{ xworkspace_console_user }}" group: "{{ xworkspace_console_user }}" mode: "0644" content: | [ { "label": "OpenClaw", "href": "http://127.0.0.1:18789/channels", "external": true }, { "label": "Vault", "href": "http://127.0.0.1:8200", "external": true }, { "label": "LiteLLM", "href": "http://127.0.0.1:4000", "external": true } ]