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