feat(ai-workspace): consume prebuilt console runtime for final deployment
The macOS console API previously ran via `go run .`, which fails under launchd's minimal PATH (no `go`) and recompiles on every launch. Switch to the same prebuilt-runtime consumption model the bridge/qmd/litellm runtimes already use. The ai-workspace role now does final deployment only (never builds): - download xworkspace-console-runtime-<os>-<arch>.tar.gz (incl. darwin-arm64) from the latest-runtime release, or use an offline-staged archive via XWORKSPACE_CONSOLE_RUNTIME_ARCHIVE; - unpack to a per-user system dir (~/.local/share/xworkspace-console), idempotent via a sha256 marker; - read manifest.json to resolve the prebuilt API binary and assert it is a present, executable native binary; - on macOS, deploy a LaunchAgent that sources portal.env and execs the prebuilt binary directly — no go, no Homebrew, no PATH games. The Go API is pure-Go (no cgo), so CI cross-compiles darwin-arm64 cleanly; this role only consumes that artifact. Validated end-to-end on darwin-arm64: packaged binary serves :8788 (200 with token, 401 without) under launchd. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a5850cfcee
commit
01f1499a60
@ -9,3 +9,40 @@ migrate_litellm_db_host: "127.0.0.1"
|
||||
# Migration paths
|
||||
openclaw_data_dir: "~/.openclaw"
|
||||
xworkspace_state_dir: "~/.local/state/xworkspace"
|
||||
|
||||
# =============================================================================
|
||||
# XWorkspace Console runtime — final deployment (consumption only)
|
||||
#
|
||||
# The console runtime binary (Go API) and dashboard dist are cross-compiled and
|
||||
# published by CI:
|
||||
# ai-workspace-lab/xworkspace-console
|
||||
# .github/workflows/offline-package-xworkspace-console-runtime.yaml
|
||||
# as xworkspace-console-runtime-<os>-<arch>.tar.gz (incl. darwin-arm64). This
|
||||
# role NEVER builds from source; it only downloads/stages the prebuilt tarball,
|
||||
# unpacks it to a per-user system dir, and deploys the launchd service that
|
||||
# execs the prebuilt API binary recorded in the package manifest.
|
||||
# =============================================================================
|
||||
ai_workspace_console_deploy_enabled: true
|
||||
ai_workspace_console_runtime_os: "{{ ansible_system | lower }}"
|
||||
ai_workspace_console_runtime_arch: "{{ 'amd64' if ansible_architecture in ['x86_64', 'amd64'] else 'arm64' }}"
|
||||
# Stable moving release maintained by the CI publish job (matches the
|
||||
# latest-runtime convention used by the bridge/qmd/litellm runtimes).
|
||||
ai_workspace_console_runtime_release_tag: "latest-runtime"
|
||||
ai_workspace_console_runtime_release_base: "https://github.com/ai-workspace-lab/xworkspace-console/releases/download/{{ ai_workspace_console_runtime_release_tag }}"
|
||||
ai_workspace_console_runtime_asset: "xworkspace-console-runtime-{{ ai_workspace_console_runtime_os }}-{{ ai_workspace_console_runtime_arch }}.tar.gz"
|
||||
ai_workspace_console_runtime_url: "{{ ai_workspace_console_runtime_release_base }}/{{ ai_workspace_console_runtime_asset }}"
|
||||
# Offline/air-gapped override: a locally-staged runtime tarball. When set it is
|
||||
# used verbatim and no download happens (offline package path).
|
||||
ai_workspace_console_runtime_archive: "{{ lookup('ansible.builtin.env', 'XWORKSPACE_CONSOLE_RUNTIME_ARCHIVE') | default('', true) }}"
|
||||
# The tarball carries a top-level xworkspace-console/ dir, so it is extracted
|
||||
# into the parent and lands at <parent>/xworkspace-console.
|
||||
ai_workspace_console_install_parent: "{{ ansible_env.HOME }}/.local/share"
|
||||
ai_workspace_console_install_dir: "{{ ai_workspace_console_install_parent }}/xworkspace-console"
|
||||
ai_workspace_console_runtime_marker: "{{ ai_workspace_console_install_dir }}/.runtime-archive-sha256"
|
||||
ai_workspace_console_manifest_path: "{{ ai_workspace_console_install_dir }}/manifest.json"
|
||||
# Token env file produced by the console play; the API sources it for auth.
|
||||
ai_workspace_console_config_dir: "{{ ansible_env.HOME }}/.config/ai-workspace"
|
||||
ai_workspace_console_portal_env: "{{ ai_workspace_console_config_dir }}/portal.env"
|
||||
ai_workspace_console_log_dir: "{{ ansible_env.HOME }}/.local/state/xworkspace"
|
||||
ai_workspace_console_api_label: "plus.svc.xworkspace.api"
|
||||
ai_workspace_console_api_port: 8788
|
||||
|
||||
42
roles/vhosts/ai-workspace/tasks/macos.yml
Normal file
42
roles/vhosts/ai-workspace/tasks/macos.yml
Normal file
@ -0,0 +1,42 @@
|
||||
---
|
||||
# macOS final deployment of the console API: run the prebuilt arm64 binary
|
||||
# from the unpacked runtime via a user LaunchAgent. The binary is self-contained
|
||||
# (pure Go, no cgo), so it needs neither `go` nor any Homebrew tooling at runtime
|
||||
# — this avoids the launchd minimal-PATH problem that broke the `go run .` path.
|
||||
|
||||
- name: Ensure XWorkspace Console runtime directories exist (macOS)
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
loop:
|
||||
- "{{ ai_workspace_console_config_dir }}"
|
||||
- "{{ ai_workspace_console_log_dir }}"
|
||||
- "{{ ansible_env.HOME }}/Library/LaunchAgents"
|
||||
|
||||
- name: Deploy XWorkspace Console API LaunchAgent (macOS)
|
||||
ansible.builtin.template:
|
||||
src: xworkspace-api.plist.j2
|
||||
dest: "{{ ansible_env.HOME }}/Library/LaunchAgents/{{ ai_workspace_console_api_label }}.plist"
|
||||
mode: "0644"
|
||||
register: ai_workspace_console_api_plist
|
||||
|
||||
- name: Restart XWorkspace Console API LaunchAgent on change (macOS)
|
||||
ansible.builtin.command: "launchctl stop {{ ai_workspace_console_api_label }}"
|
||||
when: ai_workspace_console_api_plist.changed
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Load XWorkspace Console API LaunchAgent (macOS)
|
||||
ansible.builtin.command: "launchctl load -w {{ ansible_env.HOME }}/Library/LaunchAgents/{{ ai_workspace_console_api_label }}.plist"
|
||||
register: ai_workspace_console_api_load
|
||||
changed_when: false
|
||||
failed_when: >-
|
||||
ai_workspace_console_api_load.rc != 0
|
||||
and 'already loaded' not in ai_workspace_console_api_load.stderr
|
||||
|
||||
- name: Start XWorkspace Console API LaunchAgent on change (macOS)
|
||||
ansible.builtin.command: "launchctl start {{ ai_workspace_console_api_label }}"
|
||||
when: ai_workspace_console_api_plist.changed
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
125
roles/vhosts/ai-workspace/tasks/main.yml
Normal file
125
roles/vhosts/ai-workspace/tasks/main.yml
Normal file
@ -0,0 +1,125 @@
|
||||
---
|
||||
# =============================================================================
|
||||
# Final deployment of the prebuilt XWorkspace Console runtime.
|
||||
#
|
||||
# The runtime binary is built in CI and published as
|
||||
# xworkspace-console-runtime-<os>-<arch>.tar.gz (incl. darwin-arm64). This role
|
||||
# is consumption-only: download/stage -> unpack to a per-user system dir ->
|
||||
# read the package manifest -> exec the prebuilt API binary via launchd. It
|
||||
# never compiles from source and never runs `go`.
|
||||
# =============================================================================
|
||||
|
||||
- name: Resolve XWorkspace Console runtime source
|
||||
ansible.builtin.set_fact:
|
||||
ai_workspace_console_runtime_archive_resolved: >-
|
||||
{{ ai_workspace_console_runtime_archive
|
||||
if (ai_workspace_console_runtime_archive | length > 0)
|
||||
else '/tmp/xworkspace-console-runtime.tar.gz' }}
|
||||
when: ai_workspace_console_deploy_enabled | bool
|
||||
|
||||
- name: Ensure XWorkspace Console install parent exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ ai_workspace_console_install_parent }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
when: ai_workspace_console_deploy_enabled | bool
|
||||
|
||||
- name: Download XWorkspace Console runtime release
|
||||
ansible.builtin.get_url:
|
||||
url: "{{ ai_workspace_console_runtime_url }}"
|
||||
dest: "{{ ai_workspace_console_runtime_archive_resolved }}"
|
||||
mode: "0644"
|
||||
force: true
|
||||
# Only fetch from the network when an offline archive was not supplied.
|
||||
when:
|
||||
- ai_workspace_console_deploy_enabled | bool
|
||||
- ai_workspace_console_runtime_archive | length == 0
|
||||
|
||||
- name: Stat XWorkspace Console runtime archive
|
||||
ansible.builtin.stat:
|
||||
path: "{{ ai_workspace_console_runtime_archive_resolved }}"
|
||||
checksum_algorithm: sha256
|
||||
register: ai_workspace_console_runtime_archive_stat
|
||||
when: ai_workspace_console_deploy_enabled | bool
|
||||
|
||||
- name: Require a valid XWorkspace Console runtime archive
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ai_workspace_console_runtime_archive_stat.stat.exists | default(false)
|
||||
fail_msg: >-
|
||||
No XWorkspace Console runtime archive at
|
||||
{{ ai_workspace_console_runtime_archive_resolved }}.
|
||||
Set XWORKSPACE_CONSOLE_RUNTIME_ARCHIVE (offline) or ensure
|
||||
{{ ai_workspace_console_runtime_url }} is reachable.
|
||||
when: ai_workspace_console_deploy_enabled | bool
|
||||
|
||||
- name: Read installed XWorkspace Console runtime marker
|
||||
ansible.builtin.slurp:
|
||||
path: "{{ ai_workspace_console_runtime_marker }}"
|
||||
register: ai_workspace_console_runtime_marker_content
|
||||
failed_when: false
|
||||
when: ai_workspace_console_deploy_enabled | bool
|
||||
|
||||
- name: Install (unpack) XWorkspace Console runtime
|
||||
ansible.builtin.unarchive:
|
||||
src: "{{ ai_workspace_console_runtime_archive_resolved }}"
|
||||
dest: "{{ ai_workspace_console_install_parent }}"
|
||||
remote_src: true
|
||||
mode: "0755"
|
||||
# Re-extract only when the package checksum changed or the binary is missing,
|
||||
# so repeat runs are idempotent and do not thrash the service.
|
||||
when:
|
||||
- ai_workspace_console_deploy_enabled | bool
|
||||
- >-
|
||||
(ai_workspace_console_runtime_marker_content.content | default('') | b64decode | trim)
|
||||
!= (ai_workspace_console_runtime_archive_stat.stat.checksum | default(''))
|
||||
or not (ai_workspace_console_manifest_path is file)
|
||||
|
||||
- name: Read XWorkspace Console runtime manifest
|
||||
ansible.builtin.slurp:
|
||||
path: "{{ ai_workspace_console_manifest_path }}"
|
||||
register: ai_workspace_console_manifest_raw
|
||||
when: ai_workspace_console_deploy_enabled | bool
|
||||
|
||||
- name: Resolve XWorkspace Console API binary from manifest
|
||||
ansible.builtin.set_fact:
|
||||
ai_workspace_console_manifest: "{{ ai_workspace_console_manifest_raw.content | b64decode | from_json }}"
|
||||
when: ai_workspace_console_deploy_enabled | bool
|
||||
|
||||
- name: Set XWorkspace Console API binary path
|
||||
ansible.builtin.set_fact:
|
||||
ai_workspace_console_api_binary: "{{ ai_workspace_console_install_dir }}/{{ ai_workspace_console_manifest.apiBinary }}"
|
||||
when: ai_workspace_console_deploy_enabled | bool
|
||||
|
||||
- name: Stat XWorkspace Console API binary
|
||||
ansible.builtin.stat:
|
||||
path: "{{ ai_workspace_console_api_binary }}"
|
||||
register: ai_workspace_console_api_binary_stat
|
||||
when: ai_workspace_console_deploy_enabled | bool
|
||||
|
||||
- name: Require an executable XWorkspace Console API binary
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ai_workspace_console_api_binary_stat.stat.exists | default(false)
|
||||
- ai_workspace_console_api_binary_stat.stat.executable | default(false)
|
||||
fail_msg: >-
|
||||
Prebuilt API binary missing or not executable:
|
||||
{{ ai_workspace_console_api_binary }} (manifest os/arch:
|
||||
{{ ai_workspace_console_manifest.os | default('?') }}/{{ ai_workspace_console_manifest.arch | default('?') }}).
|
||||
when: ai_workspace_console_deploy_enabled | bool
|
||||
|
||||
- name: Record installed XWorkspace Console runtime marker
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ ai_workspace_console_runtime_marker }}"
|
||||
content: "{{ ai_workspace_console_runtime_archive_stat.stat.checksum }}\n"
|
||||
mode: "0644"
|
||||
when:
|
||||
- ai_workspace_console_deploy_enabled | bool
|
||||
- ai_workspace_console_runtime_archive_stat.stat.exists | default(false)
|
||||
|
||||
# --- macOS service: exec the prebuilt binary directly (no go, no PATH games) ---
|
||||
- name: Deploy XWorkspace Console API on macOS
|
||||
ansible.builtin.import_tasks: macos.yml
|
||||
when:
|
||||
- ai_workspace_console_deploy_enabled | bool
|
||||
- ansible_os_family == 'Darwin'
|
||||
29
roles/vhosts/ai-workspace/templates/xworkspace-api.plist.j2
Normal file
29
roles/vhosts/ai-workspace/templates/xworkspace-api.plist.j2
Normal file
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>{{ ai_workspace_console_api_label }}</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/bash</string>
|
||||
<string>-c</string>
|
||||
<string>
|
||||
set -a
|
||||
[ -f "{{ ai_workspace_console_portal_env }}" ] && . "{{ ai_workspace_console_portal_env }}"
|
||||
set +a
|
||||
exec "{{ ai_workspace_console_api_binary }}"
|
||||
</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>{{ ai_workspace_console_install_dir }}</string>
|
||||
<key>StandardOutPath</key>
|
||||
<string>{{ ai_workspace_console_log_dir }}/api.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{{ ai_workspace_console_log_dir }}/api.err.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
Loading…
Reference in New Issue
Block a user