#!/bin/bash
#
# Jiejingyun Workforce - OpenClaw Shadow installer (macOS)
#
# 管理员在已授权的试点设备上运行，用于启用 OpenClaw Shadow Read Only。
# 前置条件：该设备已经完成基础安装（install-macos.sh），ActivityWatch 与
# Sync Agent 正常运行。缺少 OpenClaw 或兼容 Node/npm 时可按提示自动安装。
#
# 本脚本不采集数据、不修改事实数据，只部署只读旁路：
#   - 本地快照 API（127.0.0.1:5761，仅回环）
#   - 受限 MCP Server（4 个只读工具）
#   - Shadow Bridge（每 30 分钟）
#   - OpenClaw gateway（loopback，模型锁定到服务端代理）
#
# 用法：
#   ./install-openclaw-macos.sh              安装或修复
#   ./install-openclaw-macos.sh --rotate     同时轮换 Agent Token
#   ./install-openclaw-macos.sh --uninstall  停用并移除 OpenClaw 旁路
#

set -Eeuo pipefail
IFS=$'\n\t'
umask 077

SERVER_URL="${SERVER_URL:-https://cc.jiejingyun.org}"
AGENT_ID="${AGENT_ID:-workforce-shadow}"
AGENT_VERSION="${AGENT_VERSION:-openclaw-shadow/v1}"

# 必须与服务端 AGENT_AI_MODEL 一致，否则 OpenClaw 声明的模型与实际计费模型不符。
AGENT_MODEL_ID="${AGENT_MODEL_ID:-gpt-5.4-mini}"
AGENT_MODEL_NAME="${AGENT_MODEL_NAME:-Workforce Agent Mini}"

LOCAL_API_PORT="${LOCAL_API_PORT:-5761}"
BRIDGE_INTERVAL_SECONDS="${BRIDGE_INTERVAL_SECONDS:-1800}"
OPENCLAW_VERSION="${OPENCLAW_VERSION:-2026.7.1-2}"
NODE_RUNTIME_VERSION="${NODE_RUNTIME_VERSION:-24.15.0}"

STORE_SHA256="a65bd56a6b94fbf00efad6ea34122d298f15a4ded736a8482fd6721ff3a65157"
LOCAL_API_SHA256="d5e8f6b8835e02b9e3067589b97b107a5ac768a92952afc2437f6edf356e9e66"
LOCAL_MCP_SHA256="94c0d5be80a77ccc7bbc82342da19d3162b73507272b7d41b190836ce50809b5"
BRIDGE_SHA256="0e38ad95d25502b765d6b8832dda6ade9377575f543f6b73707849e4eb46bf34"
BOOTSTRAP_SHA256="36f642802b7726431f90f959759a92301fb76f4faca23d20395c1069ffa0b9ff"
NODE_DARWIN_ARM64_SHA256="372331b969779ab5d15b949884fc6eaf88d5afe87bde8ba881d6400b9100ffc4"
NODE_DARWIN_X64_SHA256="ffd5ee293467927f3ee731a553eb88fd1f48cf74eebc2d74a6babe4af228673b"

INSTALL_DIR="${HOME}/Library/Application Support/JiejingyunUsageCollector"
AGENT_DIR="${HOME}/Library/Application Support/JiejingyunWorkforceAgent"
LAUNCH_AGENT_DIR="${HOME}/Library/LaunchAgents"
OPENCLAW_DIR="${OPENCLAW_DIR:-${HOME}/.openclaw}"
RUNTIME_DIR="${AGENT_DIR}/runtime"

CONFIG_FILE="${INSTALL_DIR}/config.json"
STORE_FILE="${INSTALL_DIR}/workforce_snapshot_store.py"
LOCAL_API_FILE="${INSTALL_DIR}/workforce-local-api.py"
LOCAL_MCP_FILE="${INSTALL_DIR}/workforce-local-mcp.py"
BRIDGE_FILE="${INSTALL_DIR}/workforce-agent-bridge.py"
BOOTSTRAP_FILE="${INSTALL_DIR}/workforce-agent-bootstrap.py"

LOCAL_TOKEN_FILE="${INSTALL_DIR}/local-api-token"
AGENT_TOKEN_FILE="${INSTALL_DIR}/agent-token"
GATEWAY_TOKEN_FILE="${AGENT_DIR}/gateway-token"
WRAPPER_FILE="${AGENT_DIR}/openclaw-workforce.sh"
OPENCLAW_CONFIG="${OPENCLAW_DIR}/openclaw.json"

LOCAL_API_LABEL="org.jiejingyun.workforce-local-api"
BRIDGE_LABEL="org.jiejingyun.workforce-shadow-bridge"
GATEWAY_LABEL="ai.openclaw.workforce-shadow"
LOCAL_API_PLIST="${LAUNCH_AGENT_DIR}/${LOCAL_API_LABEL}.plist"
BRIDGE_PLIST="${LAUNCH_AGENT_DIR}/${BRIDGE_LABEL}.plist"
GATEWAY_PLIST="${LAUNCH_AGENT_DIR}/${GATEWAY_LABEL}.plist"

ROTATE_AGENT_TOKEN=0
TEMP_DIR=""

cleanup() {
    local exit_code=$?
    if [[ -n "${TEMP_DIR}" && -d "${TEMP_DIR}" ]]; then
        /bin/rm -rf "${TEMP_DIR}"
    fi
    return "${exit_code}"
}

on_error() {
    local exit_code=$?
    local line_number="${1:-unknown}"
    echo >&2
    echo "部署失败：脚本第 ${line_number} 行执行出错，退出码 ${exit_code}。" >&2
    echo "请保留当前终端输出用于排查。" >&2
    exit "${exit_code}"
}

trap cleanup EXIT
trap 'on_error $LINENO' ERR

require_macos() {
    if [[ "$(/usr/bin/uname -s)" != "Darwin" ]]; then
        echo "此脚本只能在 macOS 上运行。" >&2
        exit 1
    fi
    if [[ "${EUID}" -eq 0 ]]; then
        echo "请不要使用 sudo 运行此脚本，请使用该设备的日常登录账户。" >&2
        exit 1
    fi
}

python_is_compatible() {
    local candidate="$1"
    [[ -x "${candidate}" ]] || return 1
    "${candidate}" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else 1)' \
        >/dev/null 2>&1
}

find_python() {
    local candidate=""
    for candidate in \
        "/opt/homebrew/bin/python3" \
        "/usr/local/bin/python3" \
        "/usr/local/bin/python3.12" \
        "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3.12"
    do
        if python_is_compatible "${candidate}"; then
            printf '%s' "${candidate}"
            return 0
        fi
    done
    candidate="$(command -v python3 2>/dev/null || true)"
    if [[ -n "${candidate}" ]] && python_is_compatible "${candidate}"; then
        printf '%s' "${candidate}"
        return 0
    fi
    return 1
}

find_openclaw() {
    local candidate=""
    if [[ -n "${OPENCLAW_BIN:-}" ]]; then
        if [[ -x "${OPENCLAW_BIN}" ]]; then
            printf '%s' "${OPENCLAW_BIN}"
            return 0
        fi
        echo "OPENCLAW_BIN 指向的文件不可执行：${OPENCLAW_BIN}" >&2
        return 1
    fi
    for candidate in \
        "${RUNTIME_DIR}/openclaw-${OPENCLAW_VERSION}/bin/openclaw" \
        "${HOME}/.npm-global/bin/openclaw" \
        "/opt/homebrew/bin/openclaw" \
        "/usr/local/bin/openclaw"
    do
        if [[ -x "${candidate}" ]]; then
            printf '%s' "${candidate}"
            return 0
        fi
    done
    candidate="$(command -v openclaw 2>/dev/null || true)"
    if [[ -n "${candidate}" && -x "${candidate}" ]]; then
        printf '%s' "${candidate}"
        return 0
    fi
    return 1
}

node_is_compatible() {
    local candidate="$1"
    [[ -x "${candidate}" ]] || return 1
    "${candidate}" -e '
const [major, minor, patch] = process.versions.node.split(".").map(Number);
const ok = (major === 22 && (minor > 22 || (minor === 22 && patch >= 3))) ||
           (major === 24 && (minor > 15 || (minor === 15 && patch >= 0))) ||
           (major > 25 || (major === 25 && (minor > 9 || (minor === 9 && patch >= 0))));
process.exit(ok ? 0 : 1);
' >/dev/null 2>&1
}

find_compatible_node() {
    local candidate=""
    for candidate in \
        "${RUNTIME_DIR}/node-v${NODE_RUNTIME_VERSION}-darwin-arm64/bin/node" \
        "${RUNTIME_DIR}/node-v${NODE_RUNTIME_VERSION}-darwin-x64/bin/node" \
        "/opt/homebrew/bin/node" \
        "/usr/local/bin/node" \
        "${HOME}"/.nvm/versions/node/*/bin/node
    do
        if node_is_compatible "${candidate}"; then
            printf '%s' "${candidate}"
            return 0
        fi
    done
    candidate="$(command -v node 2>/dev/null || true)"
    if [[ -n "${candidate}" ]] && node_is_compatible "${candidate}"; then
        printf '%s' "${candidate}"
        return 0
    fi
    return 1
}

find_npm_for_node() {
    local node_bin="$1"
    local candidate="$(/usr/bin/dirname "${node_bin}")/npm"
    if [[ -x "${candidate}" ]] && PATH="$(/usr/bin/dirname "${node_bin}"):${PATH}" "${candidate}" --version >/dev/null 2>&1; then
        printf '%s' "${candidate}"
        return 0
    fi
    return 1
}

confirm_runtime_install() {
    local detail="$1"
    local answer=""

    while true; do
        echo "${detail}" >&2
        IFS= read -r -p "是否自动安装到当前用户目录？[Y/n]: " answer
        case "${answer}" in
            ""|[Yy]|[Yy][Ee][Ss]) return 0 ;;
            [Nn]|[Nn][Oo]) return 1 ;;
            *) echo "请输入 y 或 n。" >&2 ;;
        esac
    done
}

install_bundled_node() {
    local machine_arch=""
    local archive_arch=""
    local expected_sha256=""
    local archive_name=""
    local archive_file=""
    local extracted_dir=""
    local destination=""
    local node_bin=""

    machine_arch="$(/usr/bin/uname -m)"
    case "${machine_arch}" in
        arm64)
            archive_arch="arm64"
            expected_sha256="${NODE_DARWIN_ARM64_SHA256}"
            ;;
        x86_64)
            archive_arch="x64"
            expected_sha256="${NODE_DARWIN_X64_SHA256}"
            ;;
        *)
            echo "暂不支持自动安装此 CPU 架构的 Node：${machine_arch}" >&2
            return 1
            ;;
    esac

    archive_name="node-v${NODE_RUNTIME_VERSION}-darwin-${archive_arch}"
    destination="${RUNTIME_DIR}/${archive_name}"
    node_bin="${destination}/bin/node"
    if node_is_compatible "${node_bin}" && [[ -x "${destination}/bin/npm" ]]; then
        printf '%s' "${node_bin}"
        return 0
    fi
    if [[ -e "${destination}" ]]; then
        echo "Node 运行时目录已存在但不完整：${destination}" >&2
        echo "请保留该目录并联系管理员处理，安装器不会直接覆盖。" >&2
        return 1
    fi

    echo "正在下载并校验 Node ${NODE_RUNTIME_VERSION}..." >&2
    archive_file="${TEMP_DIR}/${archive_name}.tar.gz"
    download_verified \
        "https://nodejs.org/dist/v${NODE_RUNTIME_VERSION}/${archive_name}.tar.gz" \
        "${archive_file}" \
        "${expected_sha256}"
    /usr/bin/tar -xzf "${archive_file}" -C "${TEMP_DIR}"
    extracted_dir="${TEMP_DIR}/${archive_name}"
    if ! node_is_compatible "${extracted_dir}/bin/node" || [[ ! -x "${extracted_dir}/bin/npm" ]]; then
        echo "下载的 Node 运行时校验后不可用。" >&2
        return 1
    fi
    /bin/mkdir -p "${RUNTIME_DIR}"
    /bin/mv "${extracted_dir}" "${destination}"
    printf '%s' "${node_bin}"
}

install_openclaw_runtime() {
    local node_bin="$1"
    local npm_bin=""
    local node_bin_dir=""
    local destination="${RUNTIME_DIR}/openclaw-${OPENCLAW_VERSION}"
    local staged="${TEMP_DIR}/openclaw-${OPENCLAW_VERSION}"
    local openclaw_bin="${destination}/bin/openclaw"

    if [[ -x "${openclaw_bin}" ]]; then
        printf '%s' "${openclaw_bin}"
        return 0
    fi
    if [[ -e "${destination}" ]]; then
        echo "OpenClaw 运行时目录已存在但不完整：${destination}" >&2
        echo "请保留该目录并联系管理员处理，安装器不会直接覆盖。" >&2
        return 1
    fi
    if ! npm_bin="$(find_npm_for_node "${node_bin}")"; then
        echo "兼容的 Node 已找到，但缺少同目录 npm。" >&2
        return 1
    fi

    node_bin_dir="$(/usr/bin/dirname "${node_bin}")"
    echo "正在安装 OpenClaw ${OPENCLAW_VERSION}..." >&2
    PATH="${node_bin_dir}:${PATH}" "${npm_bin}" install \
        --global \
        --prefix "${staged}" \
        --no-audit \
        --no-fund \
        "openclaw@${OPENCLAW_VERSION}" >&2
    if [[ ! -x "${staged}/bin/openclaw" ]]; then
        echo "npm 已结束，但没有生成 OpenClaw 可执行文件。" >&2
        return 1
    fi
    PATH="${node_bin_dir}:${PATH}" "${staged}/bin/openclaw" --version >/dev/null
    /bin/mkdir -p "${RUNTIME_DIR}"
    /bin/mv "${staged}" "${destination}"
    printf '%s' "${openclaw_bin}"
}

ensure_openclaw_runtime() {
    local openclaw_bin=""
    local node_bin=""
    local npm_bin=""
    local detail=""

    openclaw_bin="$(find_openclaw || true)"
    node_bin="$(find_compatible_node || true)"
    if [[ -n "${openclaw_bin}" && -n "${node_bin}" ]]; then
        printf '%s\t%s' "${openclaw_bin}" "$(/usr/bin/dirname "${node_bin}")"
        return 0
    fi

    if [[ -z "${node_bin}" ]] || ! npm_bin="$(find_npm_for_node "${node_bin}" 2>/dev/null)"; then
        detail="未检测到兼容的 Node/npm，将安装用户级 Node ${NODE_RUNTIME_VERSION} 和 OpenClaw ${OPENCLAW_VERSION}。"
    else
        detail="检测到兼容的 Node 和 npm，但尚未安装 OpenClaw ${OPENCLAW_VERSION}。"
    fi
    if ! confirm_runtime_install "${detail}"; then
        echo "已取消自动安装。OpenClaw Shadow 尚未部署。" >&2
        return 1
    fi

    if [[ -z "${node_bin}" ]] || ! npm_bin="$(find_npm_for_node "${node_bin}" 2>/dev/null)"; then
        node_bin="$(install_bundled_node)"
    fi
    if [[ -z "${openclaw_bin}" ]]; then
        openclaw_bin="$(install_openclaw_runtime "${node_bin}")"
    fi
    printf '%s\t%s' "${openclaw_bin}" "$(/usr/bin/dirname "${node_bin}")"
}

download_verified() {
    local url="$1"
    local destination="$2"
    local expected_sha256="$3"
    local actual_sha256=""

    /usr/bin/curl \
        --fail \
        --silent \
        --show-error \
        --location \
        --retry 4 \
        --retry-delay 2 \
        --retry-all-errors \
        --connect-timeout 20 \
        "${url}" \
        --output "${destination}"

    actual_sha256="$(/usr/bin/shasum -a 256 "${destination}" | /usr/bin/awk '{print $1}')"
    if [[ "${actual_sha256}" != "${expected_sha256}" ]]; then
        echo "文件校验失败：${destination}" >&2
        echo "预期：${expected_sha256}" >&2
        echo "实际：${actual_sha256}" >&2
        echo "请从安装中心重新获取最新版部署脚本。" >&2
        exit 1
    fi
}

require_base_install() {
    if [[ ! -f "${CONFIG_FILE}" ]]; then
        echo "未找到基础采集配置：${CONFIG_FILE}" >&2
        echo "请先运行 install-macos.sh 完成基础安装。" >&2
        exit 1
    fi
    if [[ ! -f "${INSTALL_DIR}/usage-sync.py" ]]; then
        echo "未找到 usage-sync.py，基础安装不完整。" >&2
        exit 1
    fi
    if [[ ! -f "${INSTALL_DIR}/canonical-snapshots.sqlite3" ]]; then
        echo "警告：尚未生成 canonical-snapshots.sqlite3。" >&2
        echo "本地快照 API 在首个窗口封存前会返回 no_sealed_snapshot，属正常现象。" >&2
    fi
}

reload_launch_agent() {
    local plist_path="$1"
    local label="$2"
    local domain="gui/${UID}"

    /bin/launchctl bootout "${domain}" "${plist_path}" >/dev/null 2>&1 || true
    /bin/launchctl bootstrap "${domain}" "${plist_path}"
    /bin/launchctl enable "${domain}/${label}" >/dev/null 2>&1 || true
}

unload_launch_agent() {
    local plist_path="$1"
    local domain="gui/${UID}"
    /bin/launchctl bootout "${domain}" "${plist_path}" >/dev/null 2>&1 || true
}

generate_token_file() {
    local python_bin="$1"
    local destination="$2"
    local nbytes="$3"

    if [[ -s "${destination}" ]]; then
        return 0
    fi
    "${python_bin}" -c 'import secrets,sys; sys.stdout.write(secrets.token_urlsafe(int(sys.argv[1])))' \
        "${nbytes}" > "${destination}"
    /bin/chmod 600 "${destination}"
}

download_components() {
    local client_base="${SERVER_URL%/}/admin/client"

    echo "下载 Shadow 组件..."
    download_verified "${client_base}/workforce_snapshot_store.py" "${STORE_FILE}" "${STORE_SHA256}"
    download_verified "${client_base}/workforce-local-api.py" "${LOCAL_API_FILE}" "${LOCAL_API_SHA256}"
    download_verified "${client_base}/workforce-local-mcp.py" "${LOCAL_MCP_FILE}" "${LOCAL_MCP_SHA256}"
    download_verified "${client_base}/workforce-agent-bridge.py" "${BRIDGE_FILE}" "${BRIDGE_SHA256}"
    download_verified "${client_base}/workforce-agent-bootstrap.py" "${BOOTSTRAP_FILE}" "${BOOTSTRAP_SHA256}"
    /bin/chmod 600 \
        "${STORE_FILE}" \
        "${LOCAL_API_FILE}" \
        "${LOCAL_MCP_FILE}" \
        "${BRIDGE_FILE}" \
        "${BOOTSTRAP_FILE}"
}

issue_agent_token() {
    local python_bin="$1"

    if [[ -s "${AGENT_TOKEN_FILE}" && "${ROTATE_AGENT_TOKEN}" -eq 0 ]]; then
        echo "Agent Token 已存在，跳过签发（如需替换请加 --rotate）。"
        return 0
    fi

    echo "向服务端签发 Agent Token..."
    if [[ "${ROTATE_AGENT_TOKEN}" -eq 1 ]]; then
        "${python_bin}" "${BOOTSTRAP_FILE}" \
            --config "${CONFIG_FILE}" \
            --agent-id "${AGENT_ID}" \
            --agent-version "${AGENT_VERSION}" \
            --token-file "${AGENT_TOKEN_FILE}" \
            --rotate
    else
        "${python_bin}" "${BOOTSTRAP_FILE}" \
            --config "${CONFIG_FILE}" \
            --agent-id "${AGENT_ID}" \
            --agent-version "${AGENT_VERSION}" \
            --token-file "${AGENT_TOKEN_FILE}"
    fi

    if [[ ! -s "${AGENT_TOKEN_FILE}" ]]; then
        echo "Agent Token 签发失败。" >&2
        exit 4
    fi
    /bin/chmod 600 "${AGENT_TOKEN_FILE}"
}

write_wrapper() {
    local openclaw_bin="$1"
    local node_bin_dir="$2"

    /bin/cat > "${WRAPPER_FILE}" <<WRAPPER
#!/bin/bash
#
# 由 install-openclaw-macos.sh 生成。
# 只负责注入运行时环境与两个 Token，不在任何配置文件中保存密钥。
#
set -Eeuo pipefail
umask 077

if [[ -s "\${HOME}/.nvm/nvm.sh" ]]; then
    export NVM_DIR="\${HOME}/.nvm"
    # shellcheck disable=SC1091
    . "\${NVM_DIR}/nvm.sh" --no-use
    if [[ -n "\${WORKFORCE_NODE_VERSION:-}" ]]; then
        nvm use --delete-prefix "\${WORKFORCE_NODE_VERSION}" --silent >/dev/null 2>&1 || true
    else
        nvm use --delete-prefix default --silent >/dev/null 2>&1 || true
    fi
fi

export PATH="${node_bin_dir}:\${HOME}/.npm-global/bin:\${PATH}"
export WORKFORCE_AGENT_TOKEN="\$(/bin/cat "${AGENT_TOKEN_FILE}")"
export OPENCLAW_GATEWAY_TOKEN="\$(/bin/cat "${GATEWAY_TOKEN_FILE}")"

exec "${openclaw_bin}" "\$@"
WRAPPER

    /bin/chmod 700 "${WRAPPER_FILE}"
}

write_openclaw_config() {
    local python_bin="$1"

    if [[ -f "${OPENCLAW_CONFIG}" ]]; then
        local backup="${OPENCLAW_CONFIG}.pre-workforce.$(/bin/date +%Y%m%d%H%M%S)"
        /bin/cp -p "${OPENCLAW_CONFIG}" "${backup}"
        echo "已备份原有 OpenClaw 配置：${backup}"
    fi

    JY_PYTHON_BIN="${python_bin}" \
    JY_MCP_FILE="${LOCAL_MCP_FILE}" \
    JY_LOCAL_TOKEN_FILE="${LOCAL_TOKEN_FILE}" \
    JY_INSTALL_DIR="${INSTALL_DIR}" \
    JY_CONFIG_PATH="${OPENCLAW_CONFIG}" \
    JY_SERVER_URL="${SERVER_URL%/}" \
    JY_MODEL_ID="${AGENT_MODEL_ID}" \
    JY_MODEL_NAME="${AGENT_MODEL_NAME}" \
    "${python_bin}" <<'PY'
import json
import os
from pathlib import Path

model_id = os.environ["JY_MODEL_ID"]
tools = [
    "get_component_health",
    "get_latest_snapshot",
    "list_snapshots",
    "queue_shadow_inference",
]

config = {
    "mcp": {
        "servers": {
            "workforce-local-mcp": {
                "command": os.environ["JY_PYTHON_BIN"],
                "args": [
                    os.environ["JY_MCP_FILE"],
                    "--local-token-file",
                    os.environ["JY_LOCAL_TOKEN_FILE"],
                ],
                "cwd": os.environ["JY_INSTALL_DIR"],
                "toolFilter": {"include": list(tools)},
            }
        }
    },
    "secrets": {
        "providers": {
            "workforce_agent_env": {
                "source": "env",
                "allowlist": ["WORKFORCE_AGENT_TOKEN"],
            }
        }
    },
    "models": {
        "providers": {
            "workforce-ai": {
                "baseUrl": f"{os.environ['JY_SERVER_URL']}/api/v1/agent-ai/openai/v1",
                "apiKey": {
                    "source": "env",
                    "provider": "workforce_agent_env",
                    "id": "WORKFORCE_AGENT_TOKEN",
                },
                "api": "openai-responses",
                "auth": "api-key",
                "timeoutSeconds": 60,
                "models": [
                    {
                        "id": model_id,
                        "name": os.environ["JY_MODEL_NAME"],
                        "reasoning": False,
                        "input": ["text"],
                        "contextWindow": 32000,
                        "maxTokens": 1200,
                    }
                ],
            }
        }
    },
    "agents": {
        "defaults": {
            "maxConcurrent": 4,
            "subagents": {"maxConcurrent": 8, "archiveAfterMinutes": 60},
            "model": {"primary": f"workforce-ai/{model_id}"},
        }
    },
    "gateway": {
        "mode": "local",
        "bind": "loopback",
        "auth": {"mode": "token", "token": "${OPENCLAW_GATEWAY_TOKEN}"},
        "tailscale": {"mode": "off"},
        "controlUi": {"enabled": False},
        "remote": {"token": "${OPENCLAW_GATEWAY_TOKEN}"},
    },
    # Shadow Read Only 的主要控制点：允许清单只有 4 个只读工具，
    # shell、文件写入、浏览器、网络、cron、gateway、子会话全部拒绝。
    "tools": {
        "profile": "full",
        "allow": [f"workforce-local-mcp__{name}" for name in tools],
        "deny": [
            "group:fs", "group:runtime", "group:web", "group:sessions", "group:memory",
            "browser", "canvas", "cron", "gateway", "nodes",
            "exec", "process", "write", "edit", "apply_patch",
            "sessions_spawn", "sessions_send", "spawn", "shell",
            "fs_write", "fs_delete", "fs_move",
        ],
    },
    "plugins": {
        "entries": {
            name: {"enabled": False}
            for name in (
                "bonjour", "browser", "canvas", "device-pair", "file-transfer",
                "memory-core", "ollama", "phone-control", "talk-voice",
            )
        }
    },
    "commands": {"native": "auto", "nativeSkills": "auto", "restart": False},
}

path = Path(os.environ["JY_CONFIG_PATH"])
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(config, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
path.chmod(0o600)
PY
}

write_launch_agents() {
    local python_bin="$1"

    JY_PYTHON_BIN="${python_bin}" \
    JY_LOCAL_API_FILE="${LOCAL_API_FILE}" \
    JY_BRIDGE_FILE="${BRIDGE_FILE}" \
    JY_CONFIG_FILE="${CONFIG_FILE}" \
    JY_LOCAL_TOKEN_FILE="${LOCAL_TOKEN_FILE}" \
    JY_AGENT_TOKEN_FILE="${AGENT_TOKEN_FILE}" \
    JY_WRAPPER_FILE="${WRAPPER_FILE}" \
    JY_AGENT_DIR="${AGENT_DIR}" \
    JY_INSTALL_DIR="${INSTALL_DIR}" \
    JY_LOCAL_API_PLIST="${LOCAL_API_PLIST}" \
    JY_BRIDGE_PLIST="${BRIDGE_PLIST}" \
    JY_GATEWAY_PLIST="${GATEWAY_PLIST}" \
    JY_LOCAL_API_LABEL="${LOCAL_API_LABEL}" \
    JY_BRIDGE_LABEL="${BRIDGE_LABEL}" \
    JY_GATEWAY_LABEL="${GATEWAY_LABEL}" \
    JY_LOCAL_API_PORT="${LOCAL_API_PORT}" \
    JY_BRIDGE_INTERVAL="${BRIDGE_INTERVAL_SECONDS}" \
    "${python_bin}" <<'PY'
import os
import plistlib
from pathlib import Path

python_bin = os.environ["JY_PYTHON_BIN"]
install_dir = os.environ["JY_INSTALL_DIR"]
agent_dir = os.environ["JY_AGENT_DIR"]

local_api = {
    "Label": os.environ["JY_LOCAL_API_LABEL"],
    "ProgramArguments": [
        python_bin,
        os.environ["JY_LOCAL_API_FILE"],
        "--config", os.environ["JY_CONFIG_FILE"],
        "--token-file", os.environ["JY_LOCAL_TOKEN_FILE"],
        "--port", os.environ["JY_LOCAL_API_PORT"],
    ],
    "RunAtLoad": True,
    "KeepAlive": True,
    "ProcessType": "Background",
    "StandardOutPath": f"{install_dir}/local-api.log",
    "StandardErrorPath": f"{install_dir}/local-api-error.log",
}

# RunAtLoad 保持 False：开机时快照通常尚未封存，立刻跑一轮只会失败。
bridge = {
    "Label": os.environ["JY_BRIDGE_LABEL"],
    "ProgramArguments": [
        python_bin,
        os.environ["JY_BRIDGE_FILE"],
        "--config", os.environ["JY_CONFIG_FILE"],
        "--agent-token-file", os.environ["JY_AGENT_TOKEN_FILE"],
        "--local-token-file", os.environ["JY_LOCAL_TOKEN_FILE"],
    ],
    "RunAtLoad": False,
    "StartInterval": int(os.environ["JY_BRIDGE_INTERVAL"]),
    "ProcessType": "Background",
    "StandardOutPath": f"{install_dir}/workforce-shadow.log",
    "StandardErrorPath": f"{install_dir}/workforce-shadow-error.log",
}

gateway = {
    "Label": os.environ["JY_GATEWAY_LABEL"],
    "ProgramArguments": [os.environ["JY_WRAPPER_FILE"], "gateway", "run"],
    "WorkingDirectory": agent_dir,
    "RunAtLoad": True,
    "KeepAlive": True,
    "ProcessType": "Background",
    "StandardOutPath": f"{agent_dir}/gateway.log",
    "StandardErrorPath": f"{agent_dir}/gateway.error.log",
}

targets = (
    (Path(os.environ["JY_LOCAL_API_PLIST"]), local_api),
    (Path(os.environ["JY_BRIDGE_PLIST"]), bridge),
    (Path(os.environ["JY_GATEWAY_PLIST"]), gateway),
)
for path, data in targets:
    with path.open("wb") as fh:
        plistlib.dump(data, fh, fmt=plistlib.FMT_XML, sort_keys=True)
PY

    /bin/chmod 600 "${LOCAL_API_PLIST}" "${BRIDGE_PLIST}" "${GATEWAY_PLIST}"
    /usr/bin/plutil -lint "${LOCAL_API_PLIST}" >/dev/null
    /usr/bin/plutil -lint "${BRIDGE_PLIST}" >/dev/null
    /usr/bin/plutil -lint "${GATEWAY_PLIST}" >/dev/null
}

wait_for_local_api() {
    local token=""
    local attempt=0

    token="$(/bin/cat "${LOCAL_TOKEN_FILE}")"
    for attempt in $(/usr/bin/seq 1 20); do
        if /usr/bin/curl \
            --fail \
            --silent \
            --show-error \
            --noproxy "*" \
            --header "Authorization: Bearer ${token}" \
            "http://127.0.0.1:${LOCAL_API_PORT}/v1/workforce/health" \
            >/dev/null 2>&1
        then
            return 0
        fi
        /bin/sleep 1
    done
    return 1
}

do_uninstall() {
    echo "停用 OpenClaw Shadow 旁路..."
    unload_launch_agent "${GATEWAY_PLIST}"
    unload_launch_agent "${BRIDGE_PLIST}"
    unload_launch_agent "${LOCAL_API_PLIST}"
    /bin/rm -f "${GATEWAY_PLIST}" "${BRIDGE_PLIST}" "${LOCAL_API_PLIST}"

    echo
    echo "已停止并移除三个 LaunchAgent。基础采集不受影响。"
    echo
    echo "以下文件保留，未自动删除："
    echo "  ${AGENT_TOKEN_FILE}"
    echo "  ${LOCAL_TOKEN_FILE}"
    echo "  ${GATEWAY_TOKEN_FILE}"
    echo "  ${OPENCLAW_CONFIG}"
    echo
    echo "需要彻底吊销时，请在服务端将该设备的 agent_tokens.is_active 置为 false。"
}

main() {
    local python_bin=""
    local openclaw_bin=""
    local node_bin_dir=""
    local runtime_values=""

    require_macos

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --rotate) ROTATE_AGENT_TOKEN=1 ;;
            --uninstall)
                do_uninstall
                exit 0
                ;;
            *)
                echo "未知参数：$1" >&2
                echo "用法：$0 [--rotate] [--uninstall]" >&2
                exit 2
                ;;
        esac
        shift
    done

    echo "洁净云 Workforce - OpenClaw Shadow 部署"
    echo "仅用于公司已授权的试点设备。"
    echo

    require_base_install

    if ! python_bin="$(find_python)"; then
        echo "未找到 Python 3.10+。请先完成基础安装。" >&2
        exit 1
    fi
    echo "使用 Python：${python_bin}"

    /bin/mkdir -p "${INSTALL_DIR}" "${AGENT_DIR}" "${LAUNCH_AGENT_DIR}" "${OPENCLAW_DIR}" "${RUNTIME_DIR}"
    TEMP_DIR="$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/jiejingyun-openclaw.XXXXXX")"

    if ! runtime_values="$(ensure_openclaw_runtime)"; then
        exit 1
    fi
    IFS=$'\t' read -r openclaw_bin node_bin_dir <<< "${runtime_values}"
    echo "使用 OpenClaw：${openclaw_bin}"
    echo "使用 Node：${node_bin_dir}/node"

    download_components

    generate_token_file "${python_bin}" "${LOCAL_TOKEN_FILE}" 32
    generate_token_file "${python_bin}" "${GATEWAY_TOKEN_FILE}" 48
    echo "本地 Token 已就绪（仅当前用户可读）。"

    issue_agent_token "${python_bin}"

    write_wrapper "${openclaw_bin}" "${node_bin_dir}"
    write_openclaw_config "${python_bin}"
    write_launch_agents "${python_bin}"

    reload_launch_agent "${LOCAL_API_PLIST}" "${LOCAL_API_LABEL}"
    echo "正在等待本地快照 API 启动..."
    if wait_for_local_api; then
        echo "本地快照 API 已在 127.0.0.1:${LOCAL_API_PORT} 就绪。"
    else
        echo "警告：20 秒内未能访问本地快照 API。" >&2
        echo "请检查 ${INSTALL_DIR}/local-api-error.log。" >&2
    fi

    reload_launch_agent "${BRIDGE_PLIST}" "${BRIDGE_LABEL}"
    reload_launch_agent "${GATEWAY_PLIST}" "${GATEWAY_LABEL}"

    echo
    echo "部署完成。"
    echo "  本地快照 API：127.0.0.1:${LOCAL_API_PORT}（仅回环，只读）"
    echo "  Shadow Bridge：每 $((BRIDGE_INTERVAL_SECONDS / 60)) 分钟运行一次"
    echo "  OpenClaw gateway：loopback，模型锁定为 ${AGENT_MODEL_ID}"
    echo
    echo "验收："
    echo "  launchctl list | grep -E \"jiejingyun|openclaw\""
    echo "  tail -20 \"${INSTALL_DIR}/workforce-shadow.log\""
    echo "  在管理后台确认该设备状态为 Shadow Read Only"
    echo
    echo "注意：${AGENT_MODEL_ID} 必须与服务端 AGENT_AI_MODEL 一致。"
    echo "Agent Token 有效期 90 天，到期前请使用 --rotate 轮换。"
}

main "$@"
