# 进度报告 · TC-MAC-012

| 项目 | 内容 |
|------|------|
| **用例编号** | TC-MAC-012 |
| **报告时间** | 2026-06-18 19:00 (Asia/Shanghai) |
| **关联提交** | `cf97344` fix: relocate xworkmate-bridge base dir under $HOME on macOS |
| **平台** | macOS (Darwin) 本机部署 |
| **状态** | ✅ 已修复并提交（待 Mac 端实跑复验） |

## 1. 问题现象

`curl -sfL .../setup-ai-workspace-all-in-one.sh | bash -` 在 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. 根因分析

- `roles/vhosts/xworkmate_bridge/defaults/main.yml` 中 `xworkmate_bridge_base_dir` 硬编码为 `/opt/cloud-neutral/xworkmate-bridge`。
- macOS 部署以 `ansible_become=false` 运行（见 TC-MAC-002），普通用户无权在 `/opt` 下创建目录，触发 `Errno 13`。
- 该 base dir 同时被 `config.yaml`、launchd plist 的 `WorkingDirectory` 引用，因此必须指向用户可写路径。

## 3. 修复方案

沿用本仓库既有的 macOS 适配约定：不改动独立的 playbooks 仓库，而是在 `setup-ai-workspace-all-in-one.sh` 的 Darwin 分支以 `-e` 注入覆盖（与 `gateway_openclaw_home`、`agent_skills_home`、`xworkspace_console_root` 等同样处理）。

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

Ansible extra-vars 优先级高于 role defaults，因此对在线（`curl | bash`）与离线两种安装路径均生效；`config.yaml` 与 plist 的 `WorkingDirectory` 因派生自该变量而一并迁移。

## 4. 验证情况

- `bash -n` 语法检查通过。
- 已确认提交内 `-e` 覆盖存在且行为正确（extra-vars 覆盖 role 默认值）。
- 复核 bridge 角色其余系统路径，确认不会把失败下移：
  - `xworkmate_bridge_binary_path` (`/usr/local/bin/...`) 仅 `stat` 读取，非写入。
  - 所有 `/etc/systemd` 任务已带 `when: ansible_os_family != 'Darwin'` 守卫。
  - `/etc/caddy` 相关任务与 `gateway_openclaw` 完全一致，而该角色已在本次 145 个 OK 任务中通过，说明该机器 `/etc/caddy` 可写。
- 局限：当前环境无 Darwin 主机，未能实跑完整部署，验证基于变量优先级推导 + 路径审计。

## 5. 交付物

- `scripts/setup-ai-workspace-all-in-one.sh`：Darwin 分支新增 base dir 覆盖（+4 行）。
- `docs/case/macos_compatibility_tests.md`：新增 TC-MAC-012 及修复维度总结更新。
- `docs/case/{combination_tests,test_prompts}.md`：随提交纳入版本管理。

## 6. 后续建议

1. 在 Mac 上重跑 `COMBO-001` 验证 bridge 任务通过。
2. 若下一阻塞点为 `/etc/caddy` 权限，可按同样方式（Darwin 守卫或 `-e` 重定向）处理。
3. 未纳入本次提交：预先存在且无关的 `create-ai-workspace-offline-package.sh`（新增 `npm`）改动，以及生成的离线包目录。

---

## 7. 复盘更新（19:09 二次失败）

**现象**：用户再次执行 `curl -sfL .../main/scripts/setup-ai-workspace-all-in-one.sh | bash -`，仍报相同的 `/opt/cloud-neutral` `Errno 13`。

**定位**：
- `git log origin/main..HEAD` 显示 `cf97344`、`d094c27` 两个提交**仅在本地、未 push**。
- `curl | bash` 拉取的是 GitHub `main` 上的脚本，即未修复版本；因此 `-e` 覆盖根本没进入本次运行。
- 佐证：Ansible extra-vars 优先级最高，若 `-e xworkmate_bridge_base_dir` 已执行，绝不可能回落到 role 默认的 `/opt`。故可判定执行的是未修复的远端脚本。
- 本沙箱无法 push（SSH 22 端口对 github.com 被禁），需在用户机器上 push。

**策略调整（按用户要求区分平台 + 采用苹果标准目录）**：
- Linux：保持 `/opt/cloud-neutral/xworkmate-bridge`。
- macOS：改用 Apple 标准用户级应用数据目录 `~/Library/Application Support/cloud-neutral/xworkmate-bridge`（取代上一版的 `~/.local/state/...`）。
- 实现：脚本 Darwin 分支 `-e` 覆盖更新为上述路径；同时把 role `defaults/main.yml` 默认值改为按 `ansible_os_family` 的三元表达式（离线/本地 playbook 路径亦正确）。Jinja 渲染与 `bash -n` 均已验证。

**让修复立即生效的两种方式（择一）**：

```bash
# 方式 A：本地仓库已含修复，直接跑本地脚本（无需等 push）
bash "/Users/shenlan/workspaces/ai-workspace-lab/xworkspace-console/scripts/setup-ai-workspace-all-in-one.sh"

# 方式 B：把修复 push 到 main，之后 curl | bash 即可生效
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 -
```

> 注：方式 A 走 `curl | bash` 之外的本地执行路径，脚本侧 `-e` 覆盖会直接生效；role 默认的三元表达式则覆盖离线 `PLAYBOOK_DIR` 场景。

---

## 8. 续：vault 角色阻塞（TC-MAC-013，19:24）

**进展**：bridge 修复生效后，部署从 145 → 154 个任务，新的阻塞点为 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'
```

**根因**：该目录创建任务以 `owner: root` 建 `/etc/vault.d`、`/opt/vault/data`，且缺失 vault 其余 standalone 任务都有的 `ansible_os_family != 'Darwin'` 守卫。`become=false` 下既无权写 `/etc`、`/opt`，`owner: root` 也无法 chown。

**与 bridge 的关键差异**：bridge 目录 owner 是服务用户（macOS 即当前用户），故可用脚本 `-e xworkmate_bridge_base_dir=...` 修复；vault 这里的 `owner: root` 是**硬编码**，extra-vars 无法覆盖，必须改 role 逻辑。而 role 在独立 playbooks 仓库，不在本仓库。

**解法（仍落在本仓库、对 `curl|bash` 与本地执行都生效）**：复用脚本既有的“克隆后打补丁”机制（`patch_playbook_user_systemd` 已有先例），新增 `patch_playbook_vault_macos()`，仅在 Darwin 下对克隆出的 vault 角色：
1. 给“Ensure standalone Vault directories exist”追加 `ansible_os_family != 'Darwin'` 守卫；
2. `vault_config_dir`/`vault_data_dir`/`vault_binary_path` 改为按 OS 的三元表达式（Linux 不变；macOS 用 `~/Library/Application Support/vault[/data]` 与 `/opt/homebrew/bin/vault`）；
3. `macos.yml` 前置创建用户属主的数据目录（含 launchd 日志目录 `~/.local/state/xworkspace`）。

**目录策略（按用户确认）**：Linux → `/opt/vault/data`；macOS → `~/Library/Application Support/vault/data`。

**验证**：`bash -n` 通过；用真实 vault 角色副本跑补丁，YAML 合法、Darwin 守卫计数 5→6、二次执行幂等、Linux 渲染仍为原系统路径、macOS 渲染为 Apple 标准路径。

**注意（运行方式）**：上一轮 `bash scripts/setup-...sh | bash -` 的管道是错误用法（会把脚本日志再喂给第二个 bash）。本地执行应去掉管道：`bash scripts/setup-ai-workspace-all-in-one.sh`。

---

## 9. 续：common 角色 Linux 基线（TC-MAC-014，19:41）

**进展**：vault 修复后部署推进到 161 个任务，新阻塞点为 `common : Base | set timezone`：

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

**根因**：`common` 的 `Base | *` 是 Linux 服务器基线（时区、hostname、/etc/hosts、SSH 加固、fail2ban、limits、防火墙），全部 `become: true` + Linux 专有工具/路径。`set timezone` 只是第一个，其余会连环失败。

**解法**：新增 `patch_playbook_common_macos()`（仍走克隆后打补丁），仅在 Darwin 下给整个 Base 块加 `ansible_os_family != 'Darwin'` 守卫（9 处）。`import_tasks` 的 `when` 自动传播到子任务，故 ssh/fail2ban/limits/firewall 一并跳过。

**验证**：`bash -n` 通过；用真实 `common/tasks/main.yml` 跑补丁，YAML 合法、守卫计数 9、二次执行幂等、Linux 段落（Debian/RedHat/addon）不变。

**说明**：用户仅点名 timezone，但为避免逐个往返，已一并守卫同源会失败的 Base 任务。

---

## 10. 续：Vault 管理员初始化脚本（TC-MAC-015，19:50）

**进展**：common 守卫后部署推进，vault 已成功启动（健康检查通过），新阻塞点为 `vault : Bootstrap Vault admin userpass auth`（`no_log: true`，详情被隐藏）。

**根因**：该任务执行 `files/init_vault_admin.sh`，脚本 `require_cmd vault/jq/curl/base64`。macOS 默认无 `jq`，而安装它的 apt 依赖任务在 Darwin 被跳过；且 `ansible.builtin.script` 的最小 PATH 未含 `/opt/homebrew/bin`，brew 装的 vault/jq 可能找不到。

**解法**：扩展 `patch_playbook_vault_macos()`：
1. `macos.yml` 增加 `brew install jq`（`creates: /opt/homebrew/bin/jq`）；
2. Bootstrap 任务追加 `environment: PATH: "/opt/homebrew/bin:/usr/local/bin:{{ ansible_env.PATH }}"`。

**验证**：`bash -n` 通过；对 vault 角色跑补丁，jq 任务/environment 各 1 处、YAML 合法、二次执行幂等、bootstrap 结构正确（environment 位于脚本参数与 `no_log` 之间）。

**兜底**：若仍失败，临时关闭该任务 `no_log` 即可看到脚本真实 stderr 进一步定位。

---

## 11. 续：Vault 初始化非幂等真因（TC-MAC-016，20:29）

**取证**：把 bootstrap 任务 `no_log` 临时关掉并把结果写入挂载目录文件后，拿到真实报错：

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

**真因（非 macOS 特有）**：`init_vault_admin.sh` 用“以该用户登录”取 `entity_id`，但脚本又会创建 userpass 的 login-MFA enforcement。dev Vault 在多次部署间常驻，第二次起该登录被 MFA 拦截 → 无 `entity_id` → `admin-generate` 报 `missing entityID`。属 re-run 幂等缺陷（jq/PATH 早已不是问题）。

**修复**：改用 identity **entity-alias** 解析 `entity_id`（遍历别名匹配 name+mount_accessor 取 canonical_id；首跑则显式建 entity+alias），移除多余的 token revoke。真实仓库 `init_vault_admin.sh` 已修；clone 路径由 `patch_playbook_vault_macos()` 同步补丁。诊断脚手架已移除，恢复 `no_log: true`。

**验证**：两仓库脚本 `bash -n` 通过；用 git HEAD 的 pristine vault 角色跑补丁，init 脚本被正确改写、`bootstrap_token` 清零、二次执行幂等、tasks YAML 合法且无诊断残留。

**运行建议**：用真实仓库作为来源最干净：
`PLAYBOOK_DIR=/Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks bash scripts/setup-ai-workspace-all-in-one.sh`
（或把两仓库 push 到各自 main 后再 `curl | bash`）。

---

## 12. 续：PostgreSQL 模式选择（TC-MAC-017，20:43）

**进展**：vault bootstrap 已通过（出现在耗时榜，不再失败）。新阻塞点 `postgres : Materialize PostgreSQL admin password`。

**根因**：`postgresql_deploy_mode` 默认 `compose`（Docker 路径），macOS 不适用；且密码默认经 `/root/.ai_workspace_postgres_password` 的 password lookup 生成，macOS 无权写 `/root` → 密码空 → assert 失败。角色其实已备 `native`+`macos.yml`（brew postgresql@16）。

**策略（按用户确认）**：macOS → `native`；Linux → 默认 `compose`。

**修复**：脚本 Darwin 分支注入 `-e postgresql_deploy_mode=native` + `append_secret_var postgresql_admin_password=$UNIFIED_AUTH_TOKEN`（直接给密码，绕过 `/root` lookup）。纯脚本改动，Linux 不变。`bash -n` 通过，`append_secret_var` 在用前已定义。

**后续观察**：native 路径 `macos.yml` 使用 `community.general.homebrew` 模块，若该 collection 未安装可能是下一个点；先验证本步。

---

## 13. 续：postgres native 安装 Homebrew 崩溃（TC-MAC-018，20:50）

**进展**：native 模式生效（postgres 进入 `macos.yml`）。新报错：

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

**根因（非 brew 整体不可用）**：`community.general.homebrew` 模块自探测到过期的 **Intel Homebrew**（`/usr/local/Homebrew`），其不认识 macOS 27.0 而启动崩溃；vault/openclaw 用 `command: brew`（走 PATH 上可用的 brew）均正常——是模块选错了 brew。

**修复**：postgres `macos.yml` 安装改为 `command: brew install postgresql@16` + `environment.PATH` 前置 `/opt/homebrew/bin:/usr/local/bin` + `HOMEBREW_NO_AUTO_UPDATE=1`，与 vault/openclaw 一致。真实仓库已改；clone 路径新增 `patch_playbook_postgres_macos()` 同步补丁。

**验证**：脚本 `bash -n` 通过；用 git HEAD pristine `macos.yml` 跑补丁，模块被替换为 command:brew、YAML 合法、二次执行幂等。

**注意**：若该机仅有单一过期 brew（纯 Intel），属环境问题，需 `brew update` 或重装 Homebrew。
