diff --git a/api.plist.j2 b/api.plist.j2 new file mode 100644 index 0000000..0f405e7 --- /dev/null +++ b/api.plist.j2 @@ -0,0 +1,27 @@ + + + + + Label + plus.svc.xworkspace.api + ProgramArguments + + /bin/bash + -c + + source "{{ xworkspace_console_config_dir }}/portal.env" + exec {{ xworkspace_console_api_exec }} + + + RunAtLoad + + KeepAlive + + WorkingDirectory + {{ xworkspace_console_api_working_dir }} + StandardOutPath + {{ ansible_env.HOME }}/.local/state/xworkspace/api.log + StandardErrorPath + {{ ansible_env.HOME }}/.local/state/xworkspace/api.err.log + + diff --git a/console.plist.j2 b/console.plist.j2 new file mode 100644 index 0000000..0c04ed9 --- /dev/null +++ b/console.plist.j2 @@ -0,0 +1,27 @@ + + + + + Label + plus.svc.xworkspace.console + ProgramArguments + + /bin/bash + -c + + export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH" + exec /usr/bin/env npm run preview -- --host 127.0.0.1 --port {{ xworkspace_console_port }} + + + RunAtLoad + + KeepAlive + + WorkingDirectory + {{ xworkspace_console_dashboard_dir }} + StandardOutPath + {{ ansible_env.HOME }}/.local/state/xworkspace/console.log + StandardErrorPath + {{ ansible_env.HOME }}/.local/state/xworkspace/console.err.log + + diff --git a/roles/ai_agent_runtime/tasks/docs.yml b/roles/ai_agent_runtime/tasks/docs.yml index a25cb8c..d53bfff 100644 --- a/roles/ai_agent_runtime/tasks/docs.yml +++ b/roles/ai_agent_runtime/tasks/docs.yml @@ -9,3 +9,4 @@ DEBIAN_FRONTEND: noninteractive APT_LISTCHANGES_FRONTEND: none become: true + when: ansible_os_family != 'Darwin' diff --git a/roles/ai_agent_runtime/tasks/fonts.yml b/roles/ai_agent_runtime/tasks/fonts.yml index f0cee80..ebd389c 100644 --- a/roles/ai_agent_runtime/tasks/fonts.yml +++ b/roles/ai_agent_runtime/tasks/fonts.yml @@ -9,3 +9,4 @@ DEBIAN_FRONTEND: noninteractive APT_LISTCHANGES_FRONTEND: none become: true + when: ansible_os_family != 'Darwin' diff --git a/roles/ai_agent_runtime/tasks/main.yml b/roles/ai_agent_runtime/tasks/main.yml index 1a3a576..8e53ec1 100644 --- a/roles/ai_agent_runtime/tasks/main.yml +++ b/roles/ai_agent_runtime/tasks/main.yml @@ -1,9 +1,9 @@ --- -- name: Assert AI agent runtime is only supported on Debian family +- name: Assert AI agent runtime is supported on Debian or Darwin family ansible.builtin.assert: that: - - ansible_facts.os_family == "Debian" - fail_msg: "roles/ai_agent_runtime currently supports Debian-based hosts only." + - ansible_facts.os_family in ["Debian", "Darwin"] + fail_msg: "roles/ai_agent_runtime currently supports Debian-based and Darwin hosts only." - name: Install AI agent runtime base packages ansible.builtin.apt: @@ -15,6 +15,7 @@ DEBIAN_FRONTEND: noninteractive APT_LISTCHANGES_FRONTEND: none become: true + when: ansible_os_family != 'Darwin' - name: Configure Node.js runtime ansible.builtin.include_tasks: nodejs.yml diff --git a/roles/ai_agent_runtime/tasks/nodejs.yml b/roles/ai_agent_runtime/tasks/nodejs.yml index d2c9cdb..cf772e5 100644 --- a/roles/ai_agent_runtime/tasks/nodejs.yml +++ b/roles/ai_agent_runtime/tasks/nodejs.yml @@ -7,18 +7,25 @@ install_yarn: "{{ ai_agent_runtime_install_yarn }}" yarn_version: "{{ ai_agent_runtime_yarn_version }}" +- name: Ensure user local bin directory exists on macOS + ansible.builtin.file: + path: "{{ ansible_env.HOME }}/.local/bin" + state: directory + mode: "0755" + when: ansible_os_family == 'Darwin' + - name: Install npm global package manager helper ansible.builtin.copy: src: manage_npm_global_package.sh - dest: /usr/local/sbin/ai-workspace-manage-npm-global-package - owner: root - group: root + dest: "{{ '/usr/local/sbin' if ansible_os_family != 'Darwin' else ansible_env.HOME + '/.local/bin' }}/ai-workspace-manage-npm-global-package" + owner: "{{ 'root' if ansible_os_family != 'Darwin' else xworkspace_console_user }}" + group: "{{ 'root' if ansible_os_family != 'Darwin' else ('staff' if ansible_os_family == 'Darwin' else xworkspace_console_user) }}" mode: "0755" - become: true + become: "{{ ansible_os_family != 'Darwin' }}" - name: Install global npm packages for AI runtime ansible.builtin.command: - cmd: "/usr/local/sbin/ai-workspace-manage-npm-global-package {{ ai_agent_runtime_npm_global_package_action }} {{ item }}" + cmd: "{{ '/usr/local/sbin' if ansible_os_family != 'Darwin' else ansible_env.HOME + '/.local/bin' }}/ai-workspace-manage-npm-global-package {{ ai_agent_runtime_npm_global_package_action }} {{ item }}" loop: "{{ ai_agent_runtime_npm_global_packages }}" register: ai_agent_runtime_npm_global_install changed_when: "'changed=1' in ai_agent_runtime_npm_global_install.stdout" @@ -26,7 +33,7 @@ - name: Install pinned Playwright package for AI runtime ansible.builtin.command: - cmd: "/usr/local/sbin/ai-workspace-manage-npm-global-package {{ ai_agent_runtime_npm_global_package_action }} playwright@{{ ai_agent_runtime_playwright_version }}" + cmd: "{{ '/usr/local/sbin' if ansible_os_family != 'Darwin' else ansible_env.HOME + '/.local/bin' }}/ai-workspace-manage-npm-global-package {{ ai_agent_runtime_npm_global_package_action }} playwright@{{ ai_agent_runtime_playwright_version }}" register: ai_agent_runtime_playwright_install changed_when: "'changed=1' in ai_agent_runtime_playwright_install.stdout" when: diff --git a/roles/vhosts/acp_server_gemini/defaults/main.yml b/roles/vhosts/acp_server_gemini/defaults/main.yml index 28bb0e3..bedc33e 100644 --- a/roles/vhosts/acp_server_gemini/defaults/main.yml +++ b/roles/vhosts/acp_server_gemini/defaults/main.yml @@ -12,7 +12,7 @@ acp_gemini_args: --experimental-acp acp_gemini_bridge_local_source_dir: "{{ playbook_dir }}/../xworkmate-bridge" acp_gemini_bridge_local_build_dir: "{{ playbook_dir }}/.artifacts/acp_gemini" acp_gemini_bridge_local_binary_path: "{{ acp_gemini_bridge_local_build_dir }}/xworkmate-go-core" -acp_gemini_bridge_build_goos: linux +acp_gemini_bridge_build_goos: "{{ 'darwin' if ansible_os_family == 'Darwin' else 'linux' }}" acp_gemini_bridge_build_goarch: "{{ 'arm64' if ansible_architecture in ['aarch64', 'arm64'] else 'amd64' }}" acp_gemini_bridge_binary_path: /usr/local/bin/xworkmate-go-core acp_gemini_bridge_use_prebuilt: "{{ lookup('ansible.builtin.env', 'AI_WORKSPACE_USE_PREBUILT_BRIDGE') | default('false', true) | bool }}" diff --git a/roles/vhosts/acp_server_gemini/handlers/main.yml b/roles/vhosts/acp_server_gemini/handlers/main.yml index 30dc71f..fafac1b 100644 --- a/roles/vhosts/acp_server_gemini/handlers/main.yml +++ b/roles/vhosts/acp_server_gemini/handlers/main.yml @@ -1,8 +1,23 @@ --- +- name: Reload caddy + ansible.builtin.service: + name: caddy + state: reloaded + when: ansible_os_family != 'Darwin' + - name: Restart acp gemini - ansible.builtin.systemd: + ansible.builtin.service: name: "{{ acp_gemini_service_name }}" state: restarted - daemon_reload: true - when: - - not ansible_check_mode + when: ansible_os_family != 'Darwin' + +- name: Restart acp gemini on macOS + ansible.builtin.command: "launchctl stop plus.svc.xworkspace.acp.gemini" + register: launchctl_stop + failed_when: false + changed_when: false + notify: Start acp gemini on macOS + +- name: Start acp gemini on macOS + ansible.builtin.command: "launchctl start plus.svc.xworkspace.acp.gemini" + changed_when: false diff --git a/roles/vhosts/acp_server_gemini/tasks/config.yml b/roles/vhosts/acp_server_gemini/tasks/config.yml index b877bb1..cb850d7 100644 --- a/roles/vhosts/acp_server_gemini/tasks/config.yml +++ b/roles/vhosts/acp_server_gemini/tasks/config.yml @@ -33,6 +33,7 @@ ansible.builtin.command: cmd: chattr -i "{{ acp_gemini_bridge_binary_path }}" when: + - ansible_os_family != 'Darwin' - "'i' in (acp_gemini_bridge_binary_attrs.stdout | default(''))" changed_when: true become: true @@ -51,50 +52,70 @@ ansible.builtin.command: cmd: chattr +i "{{ acp_gemini_bridge_binary_path }}" when: + - ansible_os_family != 'Darwin' - "'i' in (acp_gemini_bridge_binary_attrs.stdout | default(''))" changed_when: true become: true -- name: Deploy Gemini ACP adapter service +- name: Deploy Gemini ACP systemd service ansible.builtin.command: cmd: lsattr "/etc/systemd/system/{{ acp_gemini_service_name }}.service" register: acp_gemini_service_attrs changed_when: false failed_when: false + when: ansible_os_family != 'Darwin' - name: Remove immutable flag from Gemini ACP systemd service when present ansible.builtin.command: cmd: chattr -i "/etc/systemd/system/{{ acp_gemini_service_name }}.service" when: + - ansible_os_family != 'Darwin' - "'i' in (acp_gemini_service_attrs.stdout | default(''))" changed_when: true become: true -- name: Deploy Gemini ACP adapter service +- name: Deploy Gemini ACP systemd service ansible.builtin.template: - src: gemini-acp-adapter.service.j2 + src: gemini-acp.service.j2 dest: "/etc/systemd/system/{{ acp_gemini_service_name }}.service" owner: root group: root mode: "0644" notify: Restart acp gemini + when: ansible_os_family != 'Darwin' - name: Restore immutable flag on Gemini ACP systemd service ansible.builtin.command: cmd: chattr +i "/etc/systemd/system/{{ acp_gemini_service_name }}.service" when: + - ansible_os_family != 'Darwin' - "'i' in (acp_gemini_service_attrs.stdout | default(''))" changed_when: true become: true -- name: Reload systemd manager configuration for Gemini ACP +- name: Reload systemd manager configuration ansible.builtin.systemd: daemon_reload: true + when: ansible_os_family != 'Darwin' -- name: Ensure Gemini ACP adapter service is enabled and running +- name: Ensure Caddy is enabled and running + ansible.builtin.systemd: + name: caddy + enabled: true + state: started + when: + - ansible_os_family != 'Darwin' + - acp_gemini_manage_caddy | bool + +- name: Ensure Gemini ACP service is enabled and running ansible.builtin.systemd: name: "{{ acp_gemini_service_name }}" enabled: true state: started when: - not ansible_check_mode + - ansible_os_family != 'Darwin' + +- name: Import macOS specific Gemini ACP tasks + ansible.builtin.import_tasks: macos.yml + when: ansible_os_family == 'Darwin' diff --git a/roles/vhosts/acp_server_gemini/tasks/macos.yml b/roles/vhosts/acp_server_gemini/tasks/macos.yml new file mode 100644 index 0000000..22e13f7 --- /dev/null +++ b/roles/vhosts/acp_server_gemini/tasks/macos.yml @@ -0,0 +1,13 @@ +--- +- name: Create launchd plist template for Gemini ACP + ansible.builtin.template: + src: gemini.plist.j2 + dest: "{{ ansible_env.HOME }}/Library/LaunchAgents/plus.svc.xworkspace.acp.gemini.plist" + mode: "0644" + notify: Restart acp gemini on macOS + +- name: Reload launchd agent for Gemini ACP + ansible.builtin.command: "launchctl load -w {{ ansible_env.HOME }}/Library/LaunchAgents/plus.svc.xworkspace.acp.gemini.plist" + register: launchctl_result + changed_when: false + failed_when: launchctl_result.rc != 0 and 'already loaded' not in launchctl_result.stderr diff --git a/roles/vhosts/acp_server_gemini/templates/gemini.plist.j2 b/roles/vhosts/acp_server_gemini/templates/gemini.plist.j2 new file mode 100644 index 0000000..9eee67e --- /dev/null +++ b/roles/vhosts/acp_server_gemini/templates/gemini.plist.j2 @@ -0,0 +1,41 @@ + + + + + Label + plus.svc.xworkspace.acp.gemini + ProgramArguments + + /bin/bash + -c + + export PATH="{{ acp_gemini_path }}" + export LITELLM_MASTER_KEY="{{ acp_gemini_auth_token }}" + {% for key, value in acp_gemini_environment.items() %} + export {{ key }}="{{ value }}" + {% endfor %} + + exec "{{ acp_gemini_bridge_binary_path }}" acp-server \ + --port {{ acp_gemini_listen_port }} \ + --host {{ acp_gemini_listen_host }} \ + --sub-command gemini \ + --sub-command mcp-app-server + + + RunAtLoad + + KeepAlive + + WorkingDirectory + {{ acp_gemini_workdir }} + StandardOutPath + {{ ansible_env.HOME }}/.local/state/xworkspace/acp.gemini.log + StandardErrorPath + {{ ansible_env.HOME }}/.local/state/xworkspace/acp.gemini.err.log + EnvironmentVariables + + HOME + {{ acp_gemini_workdir }} + + + diff --git a/roles/vhosts/acp_server_hermes/defaults/main.yml b/roles/vhosts/acp_server_hermes/defaults/main.yml index 9d6d8ef..c5e6cd4 100644 --- a/roles/vhosts/acp_server_hermes/defaults/main.yml +++ b/roles/vhosts/acp_server_hermes/defaults/main.yml @@ -4,7 +4,7 @@ acp_hermes_version: "0.15" acp_hermes_bridge_local_source_dir: "{{ playbook_dir }}/../xworkmate-bridge" acp_hermes_bridge_local_build_dir: "{{ playbook_dir }}/.artifacts/acp_hermes" acp_hermes_bridge_local_binary_path: "{{ acp_hermes_bridge_local_build_dir }}/xworkmate-go-core" -acp_hermes_bridge_build_goos: linux +acp_hermes_bridge_build_goos: "{{ 'darwin' if ansible_os_family == 'Darwin' else 'linux' }}" acp_hermes_bridge_build_goarch: "{{ 'arm64' if ansible_architecture in ['aarch64', 'arm64'] else 'amd64' }}" acp_hermes_bridge_use_prebuilt: "{{ lookup('ansible.builtin.env', 'AI_WORKSPACE_USE_PREBUILT_BRIDGE') | default('false', true) | bool }}" acp_hermes_listen_host: 127.0.0.1 diff --git a/roles/vhosts/acp_server_hermes/handlers/main.yml b/roles/vhosts/acp_server_hermes/handlers/main.yml index 5cc9604..1ec6cc0 100644 --- a/roles/vhosts/acp_server_hermes/handlers/main.yml +++ b/roles/vhosts/acp_server_hermes/handlers/main.yml @@ -1,8 +1,23 @@ --- +- name: Reload caddy + ansible.builtin.service: + name: caddy + state: reloaded + when: ansible_os_family != 'Darwin' + - name: Restart acp hermes - ansible.builtin.systemd: + ansible.builtin.service: name: "{{ acp_hermes_service_name }}" state: restarted - daemon_reload: true - when: - - not ansible_check_mode + when: ansible_os_family != 'Darwin' + +- name: Restart acp hermes on macOS + ansible.builtin.command: "launchctl stop plus.svc.xworkspace.acp.hermes" + register: launchctl_stop + failed_when: false + changed_when: false + notify: Start acp hermes on macOS + +- name: Start acp hermes on macOS + ansible.builtin.command: "launchctl start plus.svc.xworkspace.acp.hermes" + changed_when: false diff --git a/roles/vhosts/acp_server_hermes/tasks/config.yml b/roles/vhosts/acp_server_hermes/tasks/config.yml index 6eeb1f7..554dcc3 100644 --- a/roles/vhosts/acp_server_hermes/tasks/config.yml +++ b/roles/vhosts/acp_server_hermes/tasks/config.yml @@ -34,6 +34,7 @@ ansible.builtin.command: cmd: chattr -i "{{ acp_hermes_bridge_binary_path }}" when: + - ansible_os_family != 'Darwin' - "'i' in (acp_hermes_bridge_binary_attrs.stdout | default(''))" changed_when: true become: true @@ -52,6 +53,7 @@ ansible.builtin.command: cmd: chattr +i "{{ acp_hermes_bridge_binary_path }}" when: + - ansible_os_family != 'Darwin' - "'i' in (acp_hermes_bridge_binary_attrs.stdout | default(''))" changed_when: true become: true @@ -124,11 +126,13 @@ changed_when: false failed_when: false check_mode: false + when: ansible_os_family != 'Darwin' - name: Remove immutable flag from Hermes ACP systemd service when present ansible.builtin.command: cmd: chattr -i "/etc/systemd/system/{{ acp_hermes_service_name }}.service" when: + - ansible_os_family != 'Darwin' - "'i' in (acp_hermes_service_attrs.stdout | default(''))" changed_when: true become: true @@ -147,6 +151,7 @@ failed_when: false no_log: true check_mode: false + when: ansible_os_family != 'Darwin' - name: Resolve Hermes ACP auth token ansible.builtin.set_fact: @@ -166,11 +171,13 @@ group: root mode: "0644" notify: Restart acp hermes + when: ansible_os_family != 'Darwin' - name: Restore immutable flag on Hermes ACP systemd service ansible.builtin.command: cmd: chattr +i "/etc/systemd/system/{{ acp_hermes_service_name }}.service" when: + - ansible_os_family != 'Darwin' - "'i' in (acp_hermes_service_attrs.stdout | default(''))" changed_when: true become: true @@ -178,6 +185,7 @@ - name: Reload systemd manager configuration for Hermes ACP ansible.builtin.systemd: daemon_reload: true + when: ansible_os_family != 'Darwin' - name: Ensure Hermes ACP adapter service is enabled and running ansible.builtin.systemd: @@ -186,3 +194,8 @@ state: started when: - not ansible_check_mode + - ansible_os_family != 'Darwin' + +- name: Import macOS specific Hermes ACP tasks + ansible.builtin.import_tasks: macos.yml + when: ansible_os_family == 'Darwin' diff --git a/roles/vhosts/acp_server_hermes/tasks/macos.yml b/roles/vhosts/acp_server_hermes/tasks/macos.yml new file mode 100644 index 0000000..7cb5b25 --- /dev/null +++ b/roles/vhosts/acp_server_hermes/tasks/macos.yml @@ -0,0 +1,13 @@ +--- +- name: Create launchd plist template for Hermes ACP + ansible.builtin.template: + src: hermes.plist.j2 + dest: "{{ ansible_env.HOME }}/Library/LaunchAgents/plus.svc.xworkspace.acp.hermes.plist" + mode: "0644" + notify: Restart acp hermes on macOS + +- name: Reload launchd agent for Hermes ACP + ansible.builtin.command: "launchctl load -w {{ ansible_env.HOME }}/Library/LaunchAgents/plus.svc.xworkspace.acp.hermes.plist" + register: launchctl_result + changed_when: false + failed_when: launchctl_result.rc != 0 and 'already loaded' not in launchctl_result.stderr diff --git a/roles/vhosts/acp_server_hermes/templates/hermes.plist.j2 b/roles/vhosts/acp_server_hermes/templates/hermes.plist.j2 new file mode 100644 index 0000000..68d0827 --- /dev/null +++ b/roles/vhosts/acp_server_hermes/templates/hermes.plist.j2 @@ -0,0 +1,42 @@ + + + + + Label + plus.svc.xworkspace.acp.hermes + ProgramArguments + + /bin/bash + -c + + export PATH="{{ acp_hermes_path }}" + export LITELLM_MASTER_KEY="{{ acp_hermes_auth_token }}" + export HERMES_ADAPTER_AUTH_TOKEN="{{ acp_hermes_effective_auth_token }}" + {% for key, value in acp_hermes_environment.items() %} + export {{ key }}="{{ value }}" + {% endfor %} + + exec "{{ acp_hermes_bridge_binary_path }}" acp-server \ + --port {{ acp_hermes_listen_port }} \ + --host {{ acp_hermes_listen_host }} \ + --sub-command hermes \ + --sub-command mcp-app-server + + + RunAtLoad + + KeepAlive + + WorkingDirectory + {{ acp_hermes_workdir }} + StandardOutPath + {{ ansible_env.HOME }}/.local/state/xworkspace/acp.hermes.log + StandardErrorPath + {{ ansible_env.HOME }}/.local/state/xworkspace/acp.hermes.err.log + EnvironmentVariables + + HOME + {{ acp_hermes_workdir }} + + + diff --git a/roles/vhosts/nodejs/tasks/main.yml b/roles/vhosts/nodejs/tasks/main.yml index 3a67cf6..c982a55 100644 --- a/roles/vhosts/nodejs/tasks/main.yml +++ b/roles/vhosts/nodejs/tasks/main.yml @@ -37,12 +37,14 @@ - gnupg - ca-certificates state: present + when: ansible_os_family != 'Darwin' - name: Ensure apt keyrings directory exists file: path: /etc/apt/keyrings state: directory mode: '0755' + when: ansible_os_family != 'Darwin' - name: Remove old NodeSource repository list files file: @@ -55,6 +57,7 @@ when: - nodejs_needs_install | default(true) - not nodejs_offline_active + - ansible_os_family != 'Darwin' - name: Add NodeSource GPG key get_url: @@ -65,6 +68,7 @@ when: - nodejs_needs_install | default(true) - not nodejs_offline_active + - ansible_os_family != 'Darwin' - name: Add NodeSource repository apt_repository: @@ -75,6 +79,7 @@ when: - nodejs_needs_install | default(true) - not nodejs_offline_active + - ansible_os_family != 'Darwin' - name: Update apt cache for NodeSource repository apt: @@ -84,6 +89,7 @@ - nodejs_needs_install | default(true) - not nodejs_offline_active - nodesource_key.changed or nodesource_repo.changed + - ansible_os_family != 'Darwin' - name: Install Node.js (exact version) apt: @@ -92,7 +98,9 @@ state: present allow_downgrade: true update_cache: yes - when: nodejs_needs_install | default(true) + when: + - nodejs_needs_install | default(true) + - ansible_os_family != 'Darwin' - name: Verify npm is available command: npm --version @@ -123,6 +131,7 @@ - install_yarn | default(true) - yarn_desired_version | length == 0 - not nodejs_offline_active + - ansible_os_family != 'Darwin' - name: Add Yarn repository apt_repository: @@ -133,6 +142,7 @@ - install_yarn | default(true) - yarn_desired_version | length == 0 - not nodejs_offline_active + - ansible_os_family != 'Darwin' - name: Install Yarn apt: @@ -142,6 +152,7 @@ when: - install_yarn | default(true) - yarn_desired_version | length == 0 + - ansible_os_family != 'Darwin' - name: Enable Corepack command: corepack enable @@ -160,7 +171,7 @@ - name: Set npm to use version tags by default shell: npm config set save-exact true args: - creates: /root/.npmrc + creates: "{{ ansible_env.HOME }}/.npmrc" - name: Create npm global directory file: @@ -173,7 +184,9 @@ src: npm_global.sh.j2 dest: /etc/profile.d/npm_global.sh mode: '0644' - when: add_npm_to_path | default(true) + when: + - add_npm_to_path | default(true) + - ansible_os_family != 'Darwin' - name: Verify installations debug: diff --git a/roles/vhosts/qmd/handlers/main.yml b/roles/vhosts/qmd/handlers/main.yml new file mode 100644 index 0000000..ac3a97c --- /dev/null +++ b/roles/vhosts/qmd/handlers/main.yml @@ -0,0 +1,11 @@ +--- +- name: Restart QMD on macOS + ansible.builtin.command: "launchctl stop plus.svc.xworkspace.qmd" + register: launchctl_stop + failed_when: false + changed_when: false + notify: Start QMD on macOS + +- name: Start QMD on macOS + ansible.builtin.command: "launchctl start plus.svc.xworkspace.qmd" + changed_when: false diff --git a/roles/vhosts/qmd/tasks/macos.yml b/roles/vhosts/qmd/tasks/macos.yml new file mode 100644 index 0000000..310a1c6 --- /dev/null +++ b/roles/vhosts/qmd/tasks/macos.yml @@ -0,0 +1,13 @@ +--- +- name: Create launchd plist template for QMD + ansible.builtin.template: + src: qmd.plist.j2 + dest: "{{ ansible_env.HOME }}/Library/LaunchAgents/plus.svc.xworkspace.qmd.plist" + mode: "0644" + notify: Restart QMD on macOS + +- name: Reload launchd agent for QMD + ansible.builtin.command: "launchctl load -w {{ ansible_env.HOME }}/Library/LaunchAgents/plus.svc.xworkspace.qmd.plist" + register: launchctl_result + changed_when: false + failed_when: launchctl_result.rc != 0 and 'already loaded' not in launchctl_result.stderr diff --git a/roles/vhosts/qmd/tasks/main.yml b/roles/vhosts/qmd/tasks/main.yml index b1bfcd4..59e95d1 100644 --- a/roles/vhosts/qmd/tasks/main.yml +++ b/roles/vhosts/qmd/tasks/main.yml @@ -1,9 +1,9 @@ --- -- name: Assert QMD is only supported on Debian family +- name: Assert QMD is supported on Debian or Darwin family ansible.builtin.assert: that: - - ansible_facts.os_family == "Debian" - fail_msg: "roles/vhosts/qmd currently supports Debian-based hosts only." + - ansible_facts.os_family in ["Debian", "Darwin"] + fail_msg: "roles/vhosts/qmd currently supports Debian-based and Darwin hosts only." - name: Ensure QMD config and cache directories exist ansible.builtin.file: @@ -27,10 +27,12 @@ cmd: "id -u {{ qmd_user }}" register: qmd_service_uid_result changed_when: false + when: ansible_os_family != 'Darwin' - name: Set QMD service user uid ansible.builtin.set_fact: qmd_service_uid: "{{ qmd_service_uid_result.stdout | trim }}" + when: ansible_os_family != 'Darwin' - name: Deploy QMD collection index config ansible.builtin.template: @@ -221,6 +223,7 @@ group: "{{ qmd_group }}" mode: "0644" register: qmd_mcp_user_service_unit + when: ansible_os_family != 'Darwin' - name: Enable QMD service user linger ansible.builtin.command: @@ -228,6 +231,7 @@ creates: "/var/lib/systemd/linger/{{ qmd_user }}" when: - not ansible_check_mode + - ansible_os_family != 'Darwin' - name: Ensure QMD service user manager is running ansible.builtin.systemd: @@ -235,6 +239,7 @@ state: started when: - not ansible_check_mode + - ansible_os_family != 'Darwin' - name: Reload QMD user systemd manager ansible.builtin.command: @@ -248,6 +253,7 @@ changed_when: false when: - not ansible_check_mode + - ansible_os_family != 'Darwin' - name: Ensure QMD MCP daemon is enabled and running ansible.builtin.command: @@ -267,6 +273,7 @@ 'Created symlink' in (qmd_mcp_service_enable.stderr | default('')) when: - not ansible_check_mode + - ansible_os_family != 'Darwin' - name: Restart QMD MCP daemon after unit changes ansible.builtin.command: @@ -280,6 +287,11 @@ when: - qmd_mcp_user_service_unit.changed | default(false) - not ansible_check_mode + - ansible_os_family != 'Darwin' + +- name: Import macOS specific QMD tasks + ansible.builtin.import_tasks: macos.yml + when: ansible_os_family == 'Darwin' - name: Validate QMD status ansible.builtin.command: diff --git a/roles/vhosts/qmd/templates/qmd.plist.j2 b/roles/vhosts/qmd/templates/qmd.plist.j2 new file mode 100644 index 0000000..45e4ff8 --- /dev/null +++ b/roles/vhosts/qmd/templates/qmd.plist.j2 @@ -0,0 +1,34 @@ + + + + + Label + plus.svc.xworkspace.qmd + ProgramArguments + + /bin/bash + -c + + source "{{ qmd_env_path }}" + exec "{{ qmd_binary_path }}" mcp --http --port {{ qmd_mcp_port }} + + + RunAtLoad + + KeepAlive + + WorkingDirectory + {{ qmd_home }} + StandardOutPath + {{ ansible_env.HOME }}/.local/state/xworkspace/qmd.log + StandardErrorPath + {{ ansible_env.HOME }}/.local/state/xworkspace/qmd.err.log + EnvironmentVariables + + HOME + {{ qmd_home }} + PATH + /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:{{ ansible_env.HOME }}/.nvm/versions/node/{{ nodejs_version }}/bin + + + diff --git a/roles/vhosts/vault/tasks/macos.yml b/roles/vhosts/vault/tasks/macos.yml new file mode 100644 index 0000000..2c339b9 --- /dev/null +++ b/roles/vhosts/vault/tasks/macos.yml @@ -0,0 +1,30 @@ +--- +- name: Install HashiCorp Tap + ansible.builtin.command: brew tap hashicorp/tap + changed_when: false + +- name: Install Vault via Homebrew + ansible.builtin.command: brew install hashicorp/tap/vault + args: + creates: /opt/homebrew/bin/vault + changed_when: true + +- name: Create symlink for Vault binary to match Linux path + ansible.builtin.file: + src: /opt/homebrew/bin/vault + dest: /usr/local/bin/vault + state: link + become: true + ignore_errors: true + +- name: Create launchd plist template for Vault + ansible.builtin.template: + src: vault.plist.j2 + dest: "{{ ansible_env.HOME }}/Library/LaunchAgents/plus.svc.xworkspace.vault.plist" + mode: "0644" + +- name: Start Vault on macOS + ansible.builtin.command: "launchctl load -w {{ ansible_env.HOME }}/Library/LaunchAgents/plus.svc.xworkspace.vault.plist" + register: launchctl_result + changed_when: false + failed_when: launchctl_result.rc != 0 and 'already loaded' not in launchctl_result.stderr diff --git a/roles/vhosts/vault/tasks/main.yml b/roles/vhosts/vault/tasks/main.yml index bff8b50..083b7f6 100755 --- a/roles/vhosts/vault/tasks/main.yml +++ b/roles/vhosts/vault/tasks/main.yml @@ -23,6 +23,7 @@ update_cache: true when: - vault_deploy_mode == "standalone" + - ansible_os_family != 'Darwin' - name: Check standalone Vault binary ansible.builtin.command: "{{ vault_binary_path }} version" @@ -31,6 +32,7 @@ failed_when: false when: - vault_deploy_mode == "standalone" + - ansible_os_family != 'Darwin' - name: Download standalone Vault release ansible.builtin.unarchive: @@ -40,6 +42,7 @@ mode: "0755" when: - vault_deploy_mode == "standalone" + - ansible_os_family != 'Darwin' - vault_binary_check.rc != 0 or (vault_binary_check.stdout | default('')) is not search(vault_version) - name: Ensure standalone Vault directories exist @@ -81,6 +84,7 @@ no_log: true when: - vault_deploy_mode == "standalone" + - ansible_os_family != 'Darwin' - name: Start standalone Vault service ansible.builtin.systemd: @@ -90,6 +94,11 @@ daemon_reload: true when: - vault_deploy_mode == "standalone" + - ansible_os_family != 'Darwin' + +- name: Import macOS specific Vault tasks + ansible.builtin.import_tasks: macos.yml + when: ansible_os_family == 'Darwin' - name: Wait for standalone Vault API ansible.builtin.uri: diff --git a/roles/vhosts/vault/templates/vault.plist.j2 b/roles/vhosts/vault/templates/vault.plist.j2 new file mode 100644 index 0000000..17ee110 --- /dev/null +++ b/roles/vhosts/vault/templates/vault.plist.j2 @@ -0,0 +1,27 @@ + + + + + Label + plus.svc.xworkspace.vault + ProgramArguments + + /bin/bash + -c + + export VAULT_DEV_ROOT_TOKEN_ID="{{ vault_server_root_access_token }}" + exec "{{ vault_binary_path }}" server -dev -dev-listen-address={{ vault_listen_addr }} -dev-root-token-id={{ vault_server_root_access_token }} + + + RunAtLoad + + KeepAlive + + WorkingDirectory + {{ vault_data_dir }} + StandardOutPath + {{ ansible_env.HOME }}/.local/state/xworkspace/vault.log + StandardErrorPath + {{ ansible_env.HOME }}/.local/state/xworkspace/vault.err.log + + diff --git a/roles/vhosts/xworkmate_bridge/handlers/main.yml b/roles/vhosts/xworkmate_bridge/handlers/main.yml index 04ebcbe..099d20a 100644 --- a/roles/vhosts/xworkmate_bridge/handlers/main.yml +++ b/roles/vhosts/xworkmate_bridge/handlers/main.yml @@ -6,6 +6,18 @@ daemon_reload: true when: - not ansible_check_mode + - ansible_os_family != 'Darwin' + +- name: Restart bridge on macOS + ansible.builtin.command: "launchctl stop plus.svc.xworkspace.bridge" + register: launchctl_stop + failed_when: false + changed_when: false + notify: Start bridge on macOS + +- name: Start bridge on macOS + ansible.builtin.command: "launchctl start plus.svc.xworkspace.bridge" + changed_when: false - name: Reload caddy ansible.builtin.systemd: @@ -13,3 +25,4 @@ state: reloaded when: - not ansible_check_mode + - ansible_os_family != 'Darwin' diff --git a/roles/vhosts/xworkmate_bridge/tasks/macos.yml b/roles/vhosts/xworkmate_bridge/tasks/macos.yml new file mode 100644 index 0000000..b4930dc --- /dev/null +++ b/roles/vhosts/xworkmate_bridge/tasks/macos.yml @@ -0,0 +1,13 @@ +--- +- name: Create launchd plist template for XWorkMate Bridge + ansible.builtin.template: + src: bridge.plist.j2 + dest: "{{ ansible_env.HOME }}/Library/LaunchAgents/plus.svc.xworkspace.bridge.plist" + mode: "0644" + notify: Restart bridge on macOS + +- name: Reload launchd agent for XWorkMate Bridge + ansible.builtin.command: "launchctl load -w {{ ansible_env.HOME }}/Library/LaunchAgents/plus.svc.xworkspace.bridge.plist" + register: launchctl_result + changed_when: false + failed_when: launchctl_result.rc != 0 and 'already loaded' not in launchctl_result.stderr diff --git a/roles/vhosts/xworkmate_bridge/tasks/main.yml b/roles/vhosts/xworkmate_bridge/tasks/main.yml index 95a13fb..755629a 100644 --- a/roles/vhosts/xworkmate_bridge/tasks/main.yml +++ b/roles/vhosts/xworkmate_bridge/tasks/main.yml @@ -3,6 +3,7 @@ ansible.builtin.package: name: "{{ xworkmate_bridge_packages }}" state: present + when: ansible_os_family != 'Darwin' - name: Ensure xworkmate-bridge service group exists ansible.builtin.group: @@ -43,6 +44,7 @@ changed_when: false failed_when: false no_log: true + when: ansible_os_family != 'Darwin' - name: Read existing xworkmate-bridge review auth token from systemd units ansible.builtin.shell: | @@ -62,6 +64,7 @@ changed_when: false failed_when: false no_log: true + when: ansible_os_family != 'Darwin' - name: Resolve xworkmate-bridge auth token ansible.builtin.set_fact: @@ -112,6 +115,7 @@ failed_when: false when: - not ansible_check_mode + - ansible_os_family != 'Darwin' - name: Remove deprecated Docker bridge compose file ansible.builtin.file: @@ -134,6 +138,7 @@ failed_when: false when: - not ansible_check_mode + - ansible_os_family != 'Darwin' - name: Remove obsolete user-level xworkmate-serve service file ansible.builtin.file: @@ -146,11 +151,13 @@ register: xworkmate_bridge_config_attrs changed_when: false failed_when: false + when: ansible_os_family != 'Darwin' - name: Remove immutable flag from xworkmate-bridge config file when present ansible.builtin.command: cmd: chattr -i "{{ xworkmate_bridge_config_file }}" when: + - ansible_os_family != 'Darwin' - "'i' in (xworkmate_bridge_config_attrs.stdout | default(''))" changed_when: true @@ -169,6 +176,7 @@ changed_when: true when: - not ansible_check_mode + - ansible_os_family != 'Darwin' - name: Inspect xworkmate-bridge systemd unit attributes ansible.builtin.command: @@ -176,11 +184,13 @@ register: xworkmate_bridge_unit_attrs changed_when: false failed_when: false + when: ansible_os_family != 'Darwin' - name: Remove immutable flag from xworkmate-bridge systemd unit when present ansible.builtin.command: cmd: chattr -i "{{ xworkmate_bridge_systemd_unit_path }}" when: + - ansible_os_family != 'Darwin' - "'i' in (xworkmate_bridge_unit_attrs.stdout | default(''))" changed_when: true @@ -195,6 +205,7 @@ no_log: true register: xworkmate_bridge_systemd_unit notify: Reload bridge + when: ansible_os_family != 'Darwin' - name: Restore immutable flag on xworkmate-bridge systemd unit ansible.builtin.command: @@ -202,6 +213,7 @@ changed_when: true when: - not ansible_check_mode + - ansible_os_family != 'Darwin' - name: Reload systemd after xworkmate-bridge unit changes ansible.builtin.systemd: @@ -209,6 +221,7 @@ when: - xworkmate_bridge_systemd_unit.changed | default(false) - not ansible_check_mode + - ansible_os_family != 'Darwin' - name: Ensure Caddy fragment directory exists ansible.builtin.file: @@ -224,11 +237,13 @@ register: xworkmate_bridge_caddyfile_attrs changed_when: false failed_when: false + when: ansible_os_family != 'Darwin' - name: Remove immutable flag from Caddy main file when present ansible.builtin.command: cmd: chattr -i "{{ xworkmate_bridge_caddyfile_path }}" when: + - ansible_os_family != 'Darwin' - "'i' in (xworkmate_bridge_caddyfile_attrs.stdout | default(''))" changed_when: true @@ -248,6 +263,7 @@ ansible.builtin.command: cmd: chattr +i "{{ xworkmate_bridge_caddyfile_path }}" when: + - ansible_os_family != 'Darwin' - "'i' in (xworkmate_bridge_caddyfile_attrs.stdout | default(''))" changed_when: true @@ -257,11 +273,13 @@ register: xworkmate_bridge_site_fragment_attrs changed_when: false failed_when: false + when: ansible_os_family != 'Darwin' - name: Remove immutable flag from xworkmate-bridge Caddy fragment when present ansible.builtin.command: cmd: chattr -i "{{ xworkmate_bridge_service_caddy_fragment_path }}" when: + - ansible_os_family != 'Darwin' - "'i' in (xworkmate_bridge_site_fragment_attrs.stdout | default(''))" changed_when: true @@ -288,6 +306,7 @@ changed_when: true when: - not ansible_check_mode + - ansible_os_family != 'Darwin' - xworkmate_bridge_public_access | bool - name: Inspect deprecated ACP Caddy fragment attributes @@ -297,11 +316,13 @@ changed_when: false failed_when: false loop: "{{ xworkmate_bridge_obsolete_caddy_fragment_paths }}" + when: ansible_os_family != 'Darwin' - name: Remove immutable flag from deprecated ACP Caddy fragments when present ansible.builtin.command: cmd: chattr -i "{{ item.item }}" when: + - ansible_os_family != 'Darwin' - "'i' in (item.stdout | default(''))" changed_when: true loop: "{{ xworkmate_bridge_obsolete_fragment_attrs.results }}" @@ -322,6 +343,7 @@ state: started when: - not ansible_check_mode + - ansible_os_family != 'Darwin' - name: Ensure Caddy is enabled and running ansible.builtin.systemd: @@ -330,6 +352,7 @@ state: started when: - not ansible_check_mode + - ansible_os_family != 'Darwin' - name: Apply xworkmate-bridge service and Caddy changes before validation ansible.builtin.meta: flush_handlers @@ -339,3 +362,7 @@ tags: [xworkmate_bridge, xworkmate_bridge_validate] when: - not ansible_check_mode + +- name: Import macOS specific xworkmate-bridge tasks + ansible.builtin.import_tasks: macos.yml + when: ansible_os_family == 'Darwin' diff --git a/roles/vhosts/xworkmate_bridge/templates/bridge.plist.j2 b/roles/vhosts/xworkmate_bridge/templates/bridge.plist.j2 new file mode 100644 index 0000000..e2e9add --- /dev/null +++ b/roles/vhosts/xworkmate_bridge/templates/bridge.plist.j2 @@ -0,0 +1,37 @@ + + + + + Label + plus.svc.xworkspace.bridge + ProgramArguments + + /bin/bash + -c + + {% for key, value in xworkmate_bridge_service_environment.items() %} + export {{ key }}="{{ value }}" + {% endfor %} + + exec "{{ xworkmate_bridge_binary_path }}" server \ + --port {{ xworkmate_bridge_listen_port }} \ + --host {{ xworkmate_bridge_listen_host }} + + + RunAtLoad + + KeepAlive + + WorkingDirectory + {{ xworkmate_bridge_base_dir }} + StandardOutPath + {{ ansible_env.HOME }}/.local/state/xworkspace/bridge.log + StandardErrorPath + {{ ansible_env.HOME }}/.local/state/xworkspace/bridge.err.log + EnvironmentVariables + + HOME + {{ xworkmate_bridge_base_dir }} + + + diff --git a/setup-xworkspace-console.yaml b/setup-xworkspace-console.yaml index 83a9ad4..92fb53a 100644 --- a/setup-xworkspace-console.yaml +++ b/setup-xworkspace-console.yaml @@ -10,9 +10,9 @@ xworkspace_console_user: ubuntu xworkspace_console_public_access: false xworkspace_console_domain: workspace.svc.plus - xworkspace_console_home: /home/ubuntu - xworkspace_console_root: /home/ubuntu/xworkspace - xworkspace_console_repo_dir: /home/ubuntu/xworkspace-console + xworkspace_console_home: "{{ ansible_env.HOME | default('/home/ubuntu') }}" + xworkspace_console_root: "{{ xworkspace_console_home }}/.local/state/ai-workspace" + xworkspace_console_repo_dir: "{{ xworkspace_console_home }}/xworkspace-console" xworkspace_console_source_repo: "https://github.com/ai-workspace-lab/xworkspace-console.git" xworkspace_console_source_version: "main" xworkspace_console_source_repo_safe_dir: >- @@ -23,16 +23,16 @@ }} xworkspace_console_runtime_archive: "{{ lookup('ansible.builtin.env', 'XWORKSPACE_CONSOLE_RUNTIME_ARCHIVE') | default('', true) }}" ai_workspace_prebuilt_components_required: "{{ lookup('ansible.builtin.env', 'AI_WORKSPACE_PREBUILT_COMPONENTS_REQUIRED') | default('false', true) | bool }}" - xworkspace_console_dashboard_dir: /home/ubuntu/xworkspace-console/dashboard - xworkspace_console_api_dir: /home/ubuntu/xworkspace-console/api - xworkspace_console_api_binary: /home/ubuntu/xworkspace-console/bin/xworkspace-api - xworkspace_console_runtime_marker: /home/ubuntu/xworkspace-console/.runtime-archive-sha256 + xworkspace_console_dashboard_dir: "{{ xworkspace_console_repo_dir }}/dashboard" + xworkspace_console_api_dir: "{{ xworkspace_console_repo_dir }}/api" + xworkspace_console_api_binary: "{{ xworkspace_console_repo_dir }}/bin/xworkspace-api" + xworkspace_console_runtime_marker: "{{ xworkspace_console_repo_dir }}/.runtime-archive-sha256" xworkspace_console_api_working_dir: >- {{ xworkspace_console_repo_dir if xworkspace_console_runtime_archive | length > 0 else xworkspace_console_api_dir }} xworkspace_console_api_exec: >- {{ xworkspace_console_api_binary if xworkspace_console_runtime_archive | length > 0 else '/usr/bin/env go run .' }} - xworkspace_console_scripts_dir: /home/ubuntu/xworkspace/scripts - xworkspace_console_config_dir: /home/ubuntu/.config/xworkspace + xworkspace_console_scripts_dir: "{{ xworkspace_console_root }}/scripts" + xworkspace_console_config_dir: "{{ xworkspace_console_home }}/.config/xworkspace" xworkspace_console_url: http://127.0.0.1:17000 xworkspace_console_port: 17000 xworkspace_console_api_port: 8788 @@ -111,6 +111,7 @@ state: present install_recommends: false update_cache: true + when: ansible_os_family != 'Darwin' - name: Ensure Google Chrome apt keyring directory exists ansible.builtin.file: @@ -119,6 +120,7 @@ owner: root group: root mode: "0755" + when: ansible_os_family != 'Darwin' - name: Install Google Linux signing key ansible.builtin.shell: | @@ -134,6 +136,7 @@ when: - ansible_architecture in ['x86_64', 'amd64'] - not ai_workspace_offline_active + - ansible_os_family != 'Darwin' - name: Configure Google Chrome apt repository ansible.builtin.copy: @@ -145,6 +148,7 @@ when: - ansible_architecture in ['x86_64', 'amd64'] - not ai_workspace_offline_active + - ansible_os_family != 'Darwin' - name: Refresh apt cache after Google Chrome repository changes ansible.builtin.apt: @@ -152,6 +156,7 @@ when: - ansible_architecture in ['x86_64', 'amd64'] - not ai_workspace_offline_active + - ansible_os_family != 'Darwin' - name: Install AI Agentic Workspace runtime packages ansible.builtin.apt: @@ -162,6 +167,7 @@ + ([xworkspace_console_browser_package] if xworkspace_console_browser_package | length > 0 else []) }} state: present + when: ansible_os_family != 'Darwin' - name: Ensure ttyd binary target directory exists ansible.builtin.file: @@ -170,6 +176,7 @@ owner: root group: root mode: "0755" + when: ansible_os_family != 'Darwin' - name: Set ttyd binary download metadata ansible.builtin.set_fact: @@ -191,16 +198,31 @@ owner: root group: root force: false - when: not ai_workspace_offline_active + when: + - not ai_workspace_offline_active + - ansible_os_family != 'Darwin' - name: Verify ttyd binary ansible.builtin.command: "{{ xworkspace_console_ttyd_binary_path }} --version" - register: xworkspace_console_ttyd_version changed_when: false + register: ttyd_version_check + failed_when: ttyd_version_check.rc != 0 + when: ansible_os_family != 'Darwin' + + - name: Find ttyd path on macOS + ansible.builtin.command: which ttyd + register: ttyd_macos_path + changed_when: false + when: ansible_os_family == 'Darwin' + + - name: Set ttyd binary path for macOS + ansible.builtin.set_fact: + xworkspace_console_ttyd_binary_path: "{{ ttyd_macos_path.stdout }}" + when: ansible_os_family == 'Darwin' - name: Show ttyd binary version ansible.builtin.debug: - var: xworkspace_console_ttyd_version.stdout + msg: "{{ ttyd_version_check.stdout | default('ttyd path: ' + ttyd_macos_path.stdout | default('Unknown')) }}" - name: Ensure AI Agentic Workspace user exists ansible.builtin.user: @@ -208,32 +230,36 @@ state: present create_home: true shell: /bin/bash - when: xworkspace_console_user != 'root' + when: + - xworkspace_console_user != 'root' + - ansible_os_family != 'Darwin' - name: Ensure AI Agentic Workspace directories exist ansible.builtin.file: path: "{{ item }}" state: directory owner: "{{ xworkspace_console_user }}" - group: "{{ xworkspace_console_user }}" + group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}" mode: "0755" - loop: - - "{{ xworkspace_console_root }}" - - "{{ xworkspace_console_scripts_dir }}" - - "{{ xworkspace_console_repo_dir }}" - - "{{ xworkspace_console_home }}/.config" - - "{{ xworkspace_console_config_dir }}" - - "{{ 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" + loop: "{{ _directories | reject('search', 'systemd') | list if ansible_os_family == 'Darwin' else _directories }}" + vars: + _directories: + - "{{ xworkspace_console_root }}" + - "{{ xworkspace_console_scripts_dir }}" + - "{{ xworkspace_console_repo_dir }}" + - "{{ xworkspace_console_home }}/.config" + - "{{ xworkspace_console_config_dir }}" + - "{{ 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 }}" + group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}" mode: "0755" content: | #!/usr/bin/env python3 @@ -365,7 +391,7 @@ ansible.builtin.copy: dest: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-status.service" owner: "{{ xworkspace_console_user }}" - group: "{{ xworkspace_console_user }}" + group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}" mode: "0644" content: | [Unit] @@ -376,12 +402,13 @@ [Service] Type=oneshot ExecStart={{ xworkspace_console_scripts_dir }}/generate-status.py + when: ansible_os_family != 'Darwin' - 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 }}" + group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}" mode: "0644" content: | [Unit] @@ -395,12 +422,13 @@ [Install] WantedBy=timers.target + when: ansible_os_family != 'Darwin' - name: Deploy Xinit entrypoint for AI Agentic Workspace ansible.builtin.copy: dest: "{{ xworkspace_console_home }}/.xinitrc" owner: "{{ xworkspace_console_user }}" - group: "{{ xworkspace_console_user }}" + group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}" mode: "0755" content: | #!/usr/bin/env bash @@ -427,22 +455,24 @@ systemctl --user start xworkspace-console.service >/dev/null 2>&1 || true exec dbus-launch --exit-with-session startxfce4 + when: ansible_os_family != 'Darwin' - name: Point Xsession to Xinit entrypoint ansible.builtin.copy: dest: "{{ xworkspace_console_home }}/.xsession" owner: "{{ xworkspace_console_user }}" - group: "{{ xworkspace_console_user }}" + group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}" mode: "0755" content: | #!/usr/bin/env bash exec "$HOME/.xinitrc" + when: ansible_os_family != 'Darwin' - 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 }}" + group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}" mode: "0755" content: | #!/usr/bin/env bash @@ -453,7 +483,7 @@ ansible.builtin.copy: dest: "{{ xworkspace_console_scripts_dir }}/chrome-app.sh" owner: "{{ xworkspace_console_user }}" - group: "{{ xworkspace_console_user }}" + group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}" mode: "0755" content: | #!/usr/bin/env bash @@ -495,7 +525,7 @@ dest: "{{ xworkspace_console_repo_dir | dirname }}" remote_src: true owner: "{{ xworkspace_console_user }}" - group: "{{ xworkspace_console_user }}" + group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}" when: - xworkspace_console_runtime_archive | length > 0 - xworkspace_console_runtime_archive_stat.stat.exists | default(false) @@ -509,7 +539,7 @@ ansible.builtin.copy: dest: "{{ xworkspace_console_runtime_marker }}" owner: "{{ xworkspace_console_user }}" - group: "{{ xworkspace_console_user }}" + group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}" mode: "0644" content: "{{ xworkspace_console_runtime_archive_stat.stat.checksum }}\n" when: @@ -581,7 +611,7 @@ ansible.builtin.copy: dest: "{{ xworkspace_console_config_dir }}/portal-services.json" owner: "{{ xworkspace_console_user }}" - group: "{{ xworkspace_console_user }}" + group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}" mode: "0644" content: "{{ {'services': xworkspace_console_portal_services} | to_nice_json }}\n" @@ -589,7 +619,7 @@ ansible.builtin.copy: dest: "{{ xworkspace_console_config_dir }}/auth-token" owner: "{{ xworkspace_console_user }}" - group: "{{ xworkspace_console_user }}" + group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}" mode: "0600" content: "{{ xworkspace_console_auth_token }}\n" no_log: true @@ -598,7 +628,7 @@ ansible.builtin.copy: dest: "{{ xworkspace_console_home }}/.ai_workspace_auth_token" owner: "{{ xworkspace_console_user }}" - group: "{{ xworkspace_console_user }}" + group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}" mode: "0600" content: "{{ xworkspace_console_auth_token }}\n" no_log: true @@ -607,7 +637,7 @@ ansible.builtin.copy: dest: "{{ xworkspace_console_config_dir }}/portal.env" owner: "{{ xworkspace_console_user }}" - group: "{{ xworkspace_console_user }}" + group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}" mode: "0600" content: | AI_WORKSPACE_AUTH_TOKEN={{ xworkspace_console_auth_token }} @@ -623,7 +653,7 @@ ansible.builtin.copy: dest: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-console.service" owner: "{{ xworkspace_console_user }}" - group: "{{ xworkspace_console_user }}" + group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}" mode: "0644" content: | [Unit] @@ -640,12 +670,13 @@ [Install] WantedBy=default.target + when: ansible_os_family != 'Darwin' - name: Deploy XWorkspace API service ansible.builtin.copy: dest: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-api.service" owner: "{{ xworkspace_console_user }}" - group: "{{ xworkspace_console_user }}" + group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}" mode: "0644" content: | [Unit] @@ -663,12 +694,13 @@ [Install] WantedBy=default.target + when: ansible_os_family != 'Darwin' - 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 }}" + group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}" mode: "0644" content: | [Unit] @@ -684,6 +716,7 @@ [Install] WantedBy=default.target + when: ansible_os_family != 'Darwin' - name: Enable XWorkspace Console service ansible.builtin.file: @@ -691,6 +724,7 @@ dest: "{{ xworkspace_console_home }}/.config/systemd/user/default.target.wants/xworkspace-console.service" state: link become_user: "{{ xworkspace_console_user }}" + when: ansible_os_family != 'Darwin' - name: Enable XWorkspace API service ansible.builtin.file: @@ -698,6 +732,7 @@ dest: "{{ xworkspace_console_home }}/.config/systemd/user/default.target.wants/xworkspace-api.service" state: link become_user: "{{ xworkspace_console_user }}" + when: ansible_os_family != 'Darwin' - name: Enable AI Agentic Workspace ttyd service ansible.builtin.file: @@ -705,6 +740,7 @@ dest: "{{ xworkspace_console_home }}/.config/systemd/user/default.target.wants/xworkspace-ttyd.service" state: link become_user: "{{ xworkspace_console_user }}" + when: ansible_os_family != 'Darwin' - name: Enable AI Agentic Workspace LiteLLM service ansible.builtin.file: @@ -713,6 +749,7 @@ state: link force: true become_user: "{{ xworkspace_console_user }}" + when: ansible_os_family != 'Darwin' - name: Enable AI Agentic Workspace status timer ansible.builtin.file: @@ -720,6 +757,7 @@ dest: "{{ xworkspace_console_home }}/.config/systemd/user/timers.target.wants/xworkspace-status.timer" state: link become_user: "{{ xworkspace_console_user }}" + when: ansible_os_family != 'Darwin' - name: Kill legacy python http.server on port 7000 ansible.builtin.shell: | @@ -741,6 +779,7 @@ - "{{ xworkspace_console_home }}/.config/systemd/user/default.target.wants/xworkspace-portal.service" - "{{ xworkspace_console_home }}/.config/systemd/user/default.target.wants/xworkspace-chrome.service" - "{{ xworkspace_console_home }}/.config/systemd/user/default.target.wants/xworkspace-console.target" + when: ansible_os_family != 'Darwin' - name: Remove legacy portal directory ansible.builtin.file: @@ -754,6 +793,7 @@ systemctl start "user@${uid}.service" || true runuser -u {{ xworkspace_console_user }} -- env XDG_RUNTIME_DIR="/run/user/${uid}" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${uid}/bus" systemctl --user daemon-reload become: true + when: ansible_os_family != 'Darwin' - name: Restart xworkspace-console service ansible.builtin.shell: | @@ -762,6 +802,7 @@ systemctl start "user@${uid}.service" || true runuser -u {{ xworkspace_console_user }} -- env XDG_RUNTIME_DIR="/run/user/${uid}" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${uid}/bus" systemctl --user restart xworkspace-console.service become: true + when: ansible_os_family != 'Darwin' - name: Restart xworkspace-api service ansible.builtin.shell: | @@ -770,6 +811,7 @@ systemctl start "user@${uid}.service" || true runuser -u {{ xworkspace_console_user }} -- env XDG_RUNTIME_DIR="/run/user/${uid}" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${uid}/bus" systemctl --user restart xworkspace-api.service become: true + when: ansible_os_family != 'Darwin' - name: Restart xworkspace-ttyd service ansible.builtin.shell: | @@ -778,6 +820,7 @@ systemctl start "user@${uid}.service" || true runuser -u {{ xworkspace_console_user }} -- env XDG_RUNTIME_DIR="/run/user/${uid}" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${uid}/bus" systemctl --user restart xworkspace-ttyd.service become: true + when: ansible_os_family != 'Darwin' - name: Hide XFCE desktop icons ansible.builtin.command: xfconf-query -c xfce4-desktop -p /desktop-icons/style -t int -s 0 --create @@ -791,6 +834,7 @@ owner: root group: root mode: "0755" + when: ansible_os_family != 'Darwin' - name: Deploy xworkspace-console public Caddy site ansible.builtin.copy: @@ -802,19 +846,29 @@ {{ xworkspace_console_domain }} { reverse_proxy 127.0.0.1:{{ xworkspace_console_port }} } - when: xworkspace_console_public_access | bool + when: + - xworkspace_console_public_access | bool + - ansible_os_family != 'Darwin' register: xworkspace_caddy_deploy - name: Remove xworkspace-console public Caddy site when disabled ansible.builtin.file: path: "/etc/caddy/conf.d/{{ xworkspace_console_domain }}.caddy" state: absent - when: not (xworkspace_console_public_access | bool) + when: + - not (xworkspace_console_public_access | bool) + - ansible_os_family != 'Darwin' register: xworkspace_caddy_remove - name: Reload Caddy if xworkspace-console proxy changed ansible.builtin.service: name: caddy state: reloaded - when: (xworkspace_caddy_deploy.changed or xworkspace_caddy_remove.changed) and not ansible_check_mode + when: + - (xworkspace_caddy_deploy.changed or xworkspace_caddy_remove.changed) and not ansible_check_mode + - ansible_os_family != 'Darwin' failed_when: false + + - name: Import macOS specific XWorkspace console tasks + ansible.builtin.include_tasks: xworkspace_console_macos.yml + when: ansible_os_family == 'Darwin' diff --git a/ttyd.plist.j2 b/ttyd.plist.j2 new file mode 100644 index 0000000..55d8a9b --- /dev/null +++ b/ttyd.plist.j2 @@ -0,0 +1,29 @@ + + + + + Label + plus.svc.xworkspace.ttyd + ProgramArguments + + {{ xworkspace_console_ttyd_binary_path }} + -i + lo0 + -p + {{ xworkspace_console_ttyd_port }} + -O + login + bash + + RunAtLoad + + KeepAlive + + WorkingDirectory + {{ ansible_env.HOME }} + StandardOutPath + {{ ansible_env.HOME }}/.local/state/xworkspace/ttyd.log + StandardErrorPath + {{ ansible_env.HOME }}/.local/state/xworkspace/ttyd.err.log + + diff --git a/xworkspace_console_macos.yml b/xworkspace_console_macos.yml new file mode 100644 index 0000000..939cad0 --- /dev/null +++ b/xworkspace_console_macos.yml @@ -0,0 +1,37 @@ +--- +- name: Create launchd plist templates for XWorkspace + ansible.builtin.template: + src: "{{ item.src }}" + dest: "{{ ansible_env.HOME }}/Library/LaunchAgents/plus.svc.xworkspace.{{ item.name }}.plist" + mode: "0644" + loop: + - src: console.plist.j2 + name: console + - src: api.plist.j2 + name: api + - src: ttyd.plist.j2 + name: ttyd + register: xworkspace_mac_plists + +- name: Restart launchd agents for XWorkspace on template change + ansible.builtin.command: "launchctl stop plus.svc.xworkspace.{{ item.item.name }}" + loop: "{{ xworkspace_mac_plists.results }}" + when: item.changed + failed_when: false + changed_when: false + +- name: Reload launchd agents for XWorkspace + ansible.builtin.command: "launchctl load -w {{ ansible_env.HOME }}/Library/LaunchAgents/plus.svc.xworkspace.{{ item.name }}.plist" + loop: + - { name: console } + - { name: api } + - { name: ttyd } + register: launchctl_result + changed_when: false + failed_when: launchctl_result.rc != 0 and 'already loaded' not in launchctl_result.stderr + +- name: Start launchd agents for XWorkspace + ansible.builtin.command: "launchctl start plus.svc.xworkspace.{{ item.item.name }}" + loop: "{{ xworkspace_mac_plists.results }}" + when: item.changed + changed_when: false