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>
LiteLLM crash-looped on macOS with Prisma `P1013: invalid port number in
database URL`. The shared auth token is generated by `openssl rand -base64`
and can contain '/', '+' or '='; injected raw into the DATABASE_URL
userinfo, a '/' truncates the authority so the port parses as invalid and
proxy startup fails (port 4000 never binds).
Percent-encode the password for the DATABASE_URL only, via an explicit
reserved-set replace chain ('%' first to avoid double-encoding) since
Jinja's urlencode leaves '/' unescaped. The DB user password stays raw in
provision-database and LITELLM_DB_PASSWORD, and the URL form decodes back
to the identical secret (verified round-trip), so authentication is
unchanged. No effect when no DB host is configured.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The all-in-one flow reached "Update apt cache" in the
xfce_desktop_minimal_runtime role on macOS and failed with
`[Errno 2] No such file or directory: b'update'` (no apt on Darwin).
XFCE + XRDP is a Linux remote-desktop stack and is meaningless on macOS,
which already has a native GUI. Guard both role includes in
setup-xfce-xrdp.yaml with `ansible_os_family != 'Darwin'` so the apt/systemd
tasks never run there. Linux behavior is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`qmd status` aborted with ERR_DLOPEN_FAILED — better-sqlite3 was compiled
against NODE_MODULE_VERSION 137 (node@24) but the validate-status task ran
under nvm's Node 20 (NODE_MODULE_VERSION 115), because the user's PATH puts
nvm node ahead of Homebrew and the task pinned no PATH.
Pin `/opt/homebrew/bin` (node@24) ahead of nvm on Darwin for the npm
install, npm build, and validate-status tasks so the native module is
built and loaded against one consistent Node ABI — the same node@24 the
launchd plist already uses. Linux PATH is left unchanged via an
ansible_os_family conditional.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The QMD launchd plist hardcoded an NVM node path
(`~/.nvm/versions/node/{{ nodejs_version }}/bin`), but `nodejs_version` is
never defined in the Homebrew-based macOS deploy, so "Deploy QMD
LaunchAgent" aborted with `AnsibleUndefinedVariable: 'nodejs_version' is
undefined`.
QMD is a bun binary and the Linux user unit already uses
`.bun/bin:.local/bin:...`. Mirror that for the plist PATH and add the
Homebrew prefix (`/opt/homebrew/bin`) for the brew-installed node@24,
removing the nvm/nodejs_version dependency entirely (same remedy as the
console plist in TC-MAC-005).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`prisma generate` invokes the `prisma-client-py` generator as a `/bin/sh`
subprocess, which is resolved via PATH. Even though the role calls the
absolute venv `prisma` binary, the generator console script lives in the
same venv bin dir that is not on the default command PATH, so generation
failed with "prisma-client-py: command not found" on macOS.
Add an `environment.PATH` that prepends the venv bin dir (plus Homebrew
prefixes) so the generator subprocess resolves.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Inspect installed LiteLLM dependency versions" probe was written as a
multi-line Python program under YAML `>-` folding, which collapses every
newline into a space. The resulting single logical line contained a
`for ... : try: ... except:` block, which is a SyntaxError. With
`failed_when: false` the failure was swallowed, leaving stdout empty, and
the subsequent `set_fact` crashed in `from_json('')` with
"Expecting value: line 1 column 1 (char 0)".
Rewrite the probe as a genuinely single-line program (dict/list
comprehensions over importlib.metadata.distributions(), joined by `;`),
and harden the decision `set_fact` with `default('{}', true)` so an empty
or malformed probe degrades to "install required" instead of aborting the
play.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
litellm[proxy] pulls large wheels (polars-runtime ~46MB) that break mid-stream over slow/mirrored links with IncompleteRead, failing the deploy. Add pip --retries/--resume-retries (resumes partial downloads) + longer timeout, tunable via litellm_pip_* vars, and upgrade pip in the venv first so --resume-retries (pip>=25.1) exists.