[🇺🇸 English](../../../README.md) | [🇨🇳 中文](../../../README.zh.md)

# Progress Report · TC-MAC-012

| Item | Content |
|------|------|
| **Test Case ID** | TC-MAC-012 |
| **Report Time** | 2026-06-18 19:00 (Asia/Shanghai) |
| **Associated Commit** | `cf97344` fix: relocate xworkmate-bridge base dir under $HOME on macOS |
| **Platform** | macOS (Darwin) Local Deployment |
| **Status** | ✅ Fixed and committed (Pending re-verification on Mac) |

## 1. Issue Phenomenon

`curl -sfL .../setup-ai-workspace-all-in-one.sh | bash -` interrupted during local deployment on macOS:

```
TASK [roles/vhosts/xworkmate_bridge/ : Ensure xworkmate-bridge base directory exists]
fatal: [127.0.0.1]: FAILED! => {"changed": false,
  "msg": "There was an issue creating /opt/cloud-neutral as requested:
          [Errno 13] Permission denied: b'/opt/cloud-neutral'",
  "path": "/opt/cloud-neutral/xworkmate-bridge"}
PLAY RECAP
127.0.0.1 : ok=145  changed=12  unreachable=0  failed=1  skipped=176
```

## 2. Root Cause Analysis

- In `roles/vhosts/xworkmate_bridge/defaults/main.yml`, `xworkmate_bridge_base_dir` is hardcoded to `/opt/cloud-neutral/xworkmate-bridge`.
- macOS deployment runs with `ansible_become=false` (see TC-MAC-002), and a regular user has no permission to create directories under `/opt`, triggering `Errno 13`.
- This base dir is also referenced by `config.yaml` and the `WorkingDirectory` in the launchd plist, so it must point to a user-writable path.

## 3. Fix Solution

Following the existing macOS adaptation conventions in this repository: we do not modify the independent playbooks repository. Instead, we inject an override using `-e` in the Darwin branch of `setup-ai-workspace-all-in-one.sh` (handled similarly to `gateway_openclaw_home`, `agent_skills_home`, `xworkspace_console_root`, etc.).

```bash
ANSIBLE_EXTRA_VARS+=("-e" "xworkmate_bridge_base_dir=$HOME/.local/state/cloud-neutral/xworkmate-bridge")
```

Ansible extra-vars have a higher priority than role defaults, so they take effect for both online (`curl | bash`) and offline installation paths; `config.yaml` and the plist `WorkingDirectory` are migrated along with it because they are derived from this variable.

## 4. Verification Status

- Passed `bash -n` syntax check.
- Confirmed that the `-e` override exists in the commit and behaves correctly (extra-vars override role defaults).
- Reviewed other system paths in the bridge role, confirming failure won't be pushed down:
  - `xworkmate_bridge_binary_path` (`/usr/local/bin/...`) is only read via `stat`, not written to.
  - All `/etc/systemd` tasks already have the `when: ansible_os_family != 'Darwin'` guard.
  - Tasks related to `/etc/caddy` are identical to `gateway_openclaw`, and that role already passed within the 145 OK tasks, indicating `/etc/caddy` is writable on that machine.
- Limitations: Currently lacking a Darwin host environment, a complete deployment run could not be executed; verification is based on variable priority deduction + path auditing.

## 5. Deliverables

- `scripts/setup-ai-workspace-all-in-one.sh`: Added base dir override in the Darwin branch (+4 lines).
- `docs/case/macos_compatibility_tests.md`: Added TC-MAC-012 and updated the fix dimension summary.
- `docs/case/{combination_tests,test_prompts}.md`: Included in version control along with the commit.

## 6. Follow-up Recommendations

1. Rerun `COMBO-001` on Mac to verify the bridge tasks pass.
2. If the next blockage point is `/etc/caddy` permissions, it can be handled similarly (Darwin guard or `-e` redirection).
3. Not included in this commit: Pre-existing and unrelated `create-ai-workspace-offline-package.sh` changes (added `npm`), and the generated offline package directories.

---

## 7. Retrospective Update (19:09 Second Failure)

**Phenomenon**: The user re-executed `curl -sfL .../main/scripts/setup-ai-workspace-all-in-one.sh | bash -` and it still reported the same `/opt/cloud-neutral` `Errno 13`.

**Troubleshooting**:
- `git log origin/main..HEAD` shows that commits `cf97344` and `d094c27` are **only local, not pushed**.
- `curl | bash` pulls the script on the GitHub `main` branch, i.e., the unfixed version; hence the `-e` override never entered this run.
- Evidence: Ansible extra-vars have the highest priority. If `-e xworkmate_bridge_base_dir` was executed, it would never fall back to the role's default `/opt`. Therefore, we can determine the unfixed remote script was executed.
- This sandbox cannot push (SSH port 22 to github.com is blocked), so the push must be done on the user's machine.

**Strategy Adjustment (Differentiate platforms per user request + use Apple standard directories)**:
- Linux: Keep `/opt/cloud-neutral/xworkmate-bridge`.
- macOS: Switch to the Apple standard user-level application data directory `~/Library/Application Support/cloud-neutral/xworkmate-bridge` (replacing the previous version's `~/.local/state/...`).
- Implementation: The script's Darwin branch `-e` override is updated to the above path; meanwhile, the default value in role `defaults/main.yml` is changed to a ternary expression based on `ansible_os_family` (so offline/local playbook paths are also correct). Jinja rendering and `bash -n` have both been verified.

**Two ways to make the fix take effect immediately (choose one)**:

```bash
# Method A: Local repo already contains the fix, run the local script directly (no need to wait for push)
bash "/Users/shenlan/workspaces/ai-workspace-lab/xworkspace-console/scripts/setup-ai-workspace-all-in-one.sh"

# Method B: Push the fix to main, then curl | bash will take effect
cd /Users/shenlan/workspaces/ai-workspace-lab/xworkspace-console
git push origin main
curl -sfL https://raw.githubusercontent.com/ai-workspace-lab/xworkspace-console/main/scripts/setup-ai-workspace-all-in-one.sh | bash -
```

> Note: Method A bypasses `curl | bash` to use local execution, where the script-side `-e` override directly takes effect; the role's default ternary expression covers the offline `PLAYBOOK_DIR` scenario.

---

## 8. Cont.: vault role blockage (TC-MAC-013, 19:24)

**Progress**: After the bridge fix took effect, deployment advanced from 145 → 154 tasks, and the new blockage point is vault:

```
TASK [roles/vhosts/vault/ : Ensure standalone Vault directories exist]
failed (item=/etc/vault.d): Permission denied: b'/etc/vault.d'
failed (item=/opt/vault/data): Permission denied: b'/opt/vault'
```

**Root Cause**: This directory creation task uses `owner: root` to create `/etc/vault.d` and `/opt/vault/data`, and lacks the `ansible_os_family != 'Darwin'` guard that other standalone tasks in vault have. Under `become=false`, there is no permission to write to `/etc` or `/opt`, and chown to `owner: root` fails.

**Key difference from bridge**: The bridge directory owner is the service user (on macOS, the current user), so it can be fixed with the script's `-e xworkmate_bridge_base_dir=...`; but `owner: root` here in vault is **hardcoded**, extra-vars cannot override it, and the role logic must be changed. However, the role is in an independent playbooks repository, not this one.

**Solution (still in this repo, effective for both `curl|bash` and local execution)**: Reuse the script's existing "post-clone patch" mechanism (`patch_playbook_user_systemd` is a precedent). Add `patch_playbook_vault_macos()` to do the following only under Darwin on the cloned vault role:
1. Append the `ansible_os_family != 'Darwin'` guard to "Ensure standalone Vault directories exist";
2. Change `vault_config_dir`/`vault_data_dir`/`vault_binary_path` to OS-based ternary expressions (Linux remains unchanged; macOS uses `~/Library/Application Support/vault[/data]` and `/opt/homebrew/bin/vault`);
3. Pre-create the data directory owned by the user in `macos.yml` (including the launchd log directory `~/.local/state/xworkspace`).

**Directory Strategy (Confirmed by User)**: Linux → `/opt/vault/data`; macOS → `~/Library/Application Support/vault/data`.

**Verification**: Passed `bash -n`; running the patch with a real vault role copy resulted in valid YAML, the Darwin guard count went from 5→6, secondary execution is idempotent, Linux renders original system paths, and macOS renders Apple standard paths.

**Note (Execution Mode)**: The pipeline in the previous `bash scripts/setup-...sh | bash -` was misused (it fed the script log to a second bash). Local execution should drop the pipeline: `bash scripts/setup-ai-workspace-all-in-one.sh`.

---

## 9. Cont.: common role Linux baseline (TC-MAC-014, 19:41)

**Progress**: After the vault fix, deployment pushed to 161 tasks, and the new blockage point is `common : Base | set timezone`:

```
fatal: timedatectl set-timezone Asia/Shanghai -> [Errno 2] No such file or directory: b'timedatectl'
```

**Root Cause**: `Base | *` in `common` are Linux server baselines (timezone, hostname, /etc/hosts, SSH hardening, fail2ban, limits, firewall), all of which require `become: true` and Linux-specific tools/paths. `set timezone` is just the first one, the rest will fail sequentially.

**Solution**: Added `patch_playbook_common_macos()` (still via post-clone patch), which adds the `ansible_os_family != 'Darwin'` guard to the entire Base block under Darwin (9 locations). The `when` on `import_tasks` automatically propagates to subtasks, so ssh/fail2ban/limits/firewall are skipped together.

**Verification**: Passed `bash -n`; running the patch against a real `common/tasks/main.yml` yields valid YAML, 9 guard counts, idempotent secondary execution, and unchanged Linux sections (Debian/RedHat/addon).

**Note**: The user only specifically mentioned timezone, but to avoid back-and-forth, all homologous Base tasks that would fail have been guarded together.

---

## 10. Cont.: Vault Admin Initialization Script (TC-MAC-015, 19:50)

**Progress**: After common guarding, deployment advanced, vault has successfully started (passed health check), and the new blockage point is `vault : Bootstrap Vault admin userpass auth` (`no_log: true`, details hidden).

**Root Cause**: This task executes `files/init_vault_admin.sh`, and the script relies on `require_cmd vault/jq/curl/base64`. macOS does not have `jq` by default, and the apt dependency installation task for it was skipped on Darwin; furthermore, the minimal PATH for `ansible.builtin.script` does not include `/opt/homebrew/bin`, so brew-installed vault/jq might not be found.

**Solution**: Extended `patch_playbook_vault_macos()`:
1. `macos.yml` added `brew install jq` (`creates: /opt/homebrew/bin/jq`);
2. Bootstrap task appended `environment: PATH: "/opt/homebrew/bin:/usr/local/bin:{{ ansible_env.PATH }}"`.

**Verification**: Passed `bash -n`; running the patch on the vault role showed 1 jq task and 1 environment insertion, valid YAML, idempotent secondary execution, and correct bootstrap structure (environment is located between script parameters and `no_log`).

**Fallback**: If it still fails, temporarily disable `no_log` on the task to see the actual stderr from the script for further troubleshooting.

---

## 11. Cont.: True Cause of Vault Initialization Non-Idempotency (TC-MAC-016, 20:29)

**Investigation**: After temporarily disabling `no_log` on the bootstrap task and writing the output to a file in a mounted directory, the true error was captured:

```
Code: 400 ... * missing entityID
"A login request was issued that is subject to MFA validation ..."
PUT /v1/identity/mfa/method/totp/admin-generate
```

**True Cause (Not macOS specific)**: `init_vault_admin.sh` uses "login as that user" to retrieve `entity_id`, but the script also creates a login-MFA enforcement for userpass. Since the dev Vault persists across multiple deployments, during the second startup the login is blocked by MFA → no `entity_id` → `admin-generate` reports `missing entityID`. This is a re-run idempotency defect (jq/PATH had long ceased to be the issue).

**Fix**: Changed to parsing `entity_id` via identity **entity-alias** (iterate through aliases to match name+mount_accessor and retrieve canonical_id; on first run, explicitly create entity+alias), and removed the redundant token revoke. Real repo `init_vault_admin.sh` fixed; clone path gets synchronized patch via `patch_playbook_vault_macos()`. Diagnostic scaffolding removed, restoring `no_log: true`.

**Verification**: Both repository scripts passed `bash -n`; running the patch with git HEAD pristine vault role correctly rewrote the init script, zeroed out `bootstrap_token`, idempotent secondary execution, and the tasks YAML is valid with no diagnostic residue.

**Execution Recommendation**: Using the real repository as the source is the cleanest:
`PLAYBOOK_DIR=/Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks bash scripts/setup-ai-workspace-all-in-one.sh`
(Or push both repositories to their respective mains, then `curl | bash`).

---

## 12. Cont.: PostgreSQL Mode Selection (TC-MAC-017, 20:43)

**Progress**: Vault bootstrap has passed (appeared on the duration board, no longer failing). New blockage point: `postgres : Materialize PostgreSQL admin password`.

**Root Cause**: `postgresql_deploy_mode` defaults to `compose` (Docker path), which doesn't apply to macOS; and the password by default is generated via password lookup at `/root/.ai_workspace_postgres_password`, but macOS lacks permission to write to `/root` → empty password → assert fails. The role actually has a `native`+`macos.yml` path prepared (brew postgresql@16).

**Strategy (Confirmed by User)**: macOS → `native`; Linux → default `compose`.

**Fix**: The Darwin branch of the script injects `-e postgresql_deploy_mode=native` + `append_secret_var postgresql_admin_password=$UNIFIED_AUTH_TOKEN` (directly provides the password, bypassing the `/root` lookup). Pure script change, Linux remains unchanged. Passed `bash -n`, `append_secret_var` is defined before use.

**Follow-up Observation**: The native path `macos.yml` uses the `community.general.homebrew` module. If this collection isn't installed, it might be the next issue; verifying this step first.

---

## 13. Cont.: postgres native installation Homebrew crash (TC-MAC-018, 20:50)

**Progress**: The native mode is active (postgres entered `macos.yml`). New error:

```
/usr/local/Homebrew/.../macos_version.rb: unknown or unsupported macOS version: "27.0"
```

**Root Cause (Not a general brew failure)**: The `community.general.homebrew` module auto-detected an expired **Intel Homebrew** (`/usr/local/Homebrew`), which doesn't recognize macOS 27.0 and crashed on startup; whereas vault/openclaw using `command: brew` (via the available brew on PATH) worked fine—it was the module choosing the wrong brew.

**Fix**: postgres `macos.yml` installation changed to `command: brew install postgresql@16` + `environment.PATH` prepended with `/opt/homebrew/bin:/usr/local/bin` + `HOMEBREW_NO_AUTO_UPDATE=1`, matching vault/openclaw. Real repo updated; clone path synchronized via new `patch_playbook_postgres_macos()`.

**Verification**: Script passed `bash -n`; running the patch with git HEAD pristine `macos.yml` replaced the module with command:brew, yielded valid YAML, and idempotent secondary execution.

**Note**: If the machine only has a single expired brew (pure Intel), it's an environment issue requiring `brew update` or Homebrew reinstallation.
