diff --git a/Makefile b/Makefile index ca4d7ab5..76ef180c 100644 --- a/Makefile +++ b/Makefile @@ -61,12 +61,6 @@ uninstall-macos: test-install-macos: ./scripts/test/test-install-macos.sh -deploy-macos-source: - ./scripts/dev/deploy-macos-source.sh - -restart-macos: - ./scripts/dev/restart-macos.sh - docker-build: docker build $(DOCKER_BUILD_ARGS) --target runtime -t $(IMAGE) . diff --git a/packaging/macos/sign-macos.sh b/packaging/macos/sign-macos.sh deleted file mode 100755 index 47da1e8d..00000000 --- a/packaging/macos/sign-macos.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/zsh -set -euo pipefail - -usage() { - cat <<'USAGE' -用法: - AGENTDOCK_CODESIGN_IDENTITY=... packaging/macos/sign-macos.sh /path/to/agentdock - -可选环境变量: - AGENTDOCK_CODESIGN_KEYCHAIN 指定代码签名钥匙串 - AGENTDOCK_CODESIGN_KEYCHAIN_PASSWORD 指定钥匙串密码,默认空密码 - AGENTDOCK_CODESIGN_IDENTIFIER 固定 Bundle Identifier,默认 com.local.agentdock - AGENTDOCK_CODESIGN_HOME codesign/security 使用的 HOME,默认当前 HOME -USAGE -} - -die() { - print -u2 -- "ERROR: $*" - exit 1 -} - -[[ "$(uname -s)" == "Darwin" ]] || die "此脚本只支持 macOS" -(( $# == 1 )) || { usage; exit 2; } - -TARGET="$1" -IDENTITY="${AGENTDOCK_CODESIGN_IDENTITY:-}" -KEYCHAIN="${AGENTDOCK_CODESIGN_KEYCHAIN:-}" -KEYCHAIN_PASSWORD="${AGENTDOCK_CODESIGN_KEYCHAIN_PASSWORD:-}" -IDENTIFIER="${AGENTDOCK_CODESIGN_IDENTIFIER:-com.local.agentdock}" -SIGN_HOME="${AGENTDOCK_CODESIGN_HOME:-$HOME}" - -[[ -f "$TARGET" && ! -L "$TARGET" ]] || die "签名目标必须是普通文件:$TARGET" -[[ -n "$IDENTITY" ]] || die "AGENTDOCK_CODESIGN_IDENTITY 不能为空" -[[ -d "$SIGN_HOME" ]] || die "AGENTDOCK_CODESIGN_HOME 不是目录:$SIGN_HOME" -command -v codesign >/dev/null 2>&1 || die "缺少命令:codesign" -command -v security >/dev/null 2>&1 || die "缺少命令:security" - -export HOME="$SIGN_HOME" -if [[ -n "$KEYCHAIN" ]]; then - [[ -f "$KEYCHAIN" && ! -L "$KEYCHAIN" ]] || die "代码签名钥匙串不存在或不是普通文件:$KEYCHAIN" - security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN" >/dev/null 2>&1 || die "无法解锁代码签名钥匙串:$KEYCHAIN" - identity_output="$(security find-identity -v -p codesigning "$KEYCHAIN")" || die "无法读取指定钥匙串中的签名身份" - [[ "$identity_output" == *"$IDENTITY"* ]] || die "指定钥匙串中不存在签名身份:$IDENTITY" - codesign --force \ - --keychain "$KEYCHAIN" \ - --sign "$IDENTITY" \ - --timestamp=none \ - --options runtime \ - --identifier "$IDENTIFIER" \ - "$TARGET" >/dev/null -else - identity_output="$(security find-identity -v -p codesigning)" || die "无法读取系统钥匙串中的签名身份" - [[ "$identity_output" == *"$IDENTITY"* ]] || die "系统钥匙串中不存在签名身份:$IDENTITY" - codesign --force \ - --sign "$IDENTITY" \ - --timestamp=none \ - --options runtime \ - --identifier "$IDENTIFIER" \ - "$TARGET" >/dev/null -fi - -codesign --verify --strict --verbose=2 "$TARGET" >/dev/null -sign_details="$(codesign -dv --verbose=4 "$TARGET" 2>&1)" || die "无法读取签名详情" -actual_identifier="$(print -r -- "$sign_details" | sed -n 's/^Identifier=//p' | head -n 1)" -[[ "$actual_identifier" == "$IDENTIFIER" ]] || die "签名 Identifier 不匹配:期望 ${IDENTIFIER},实际 $actual_identifier" -print -- "signed: $TARGET" -print -- "identifier: $actual_identifier" diff --git a/scripts/dev/deploy-macos-source.sh b/scripts/dev/deploy-macos-source.sh deleted file mode 100755 index 41ef319a..00000000 --- a/scripts/dev/deploy-macos-source.sh +++ /dev/null @@ -1,248 +0,0 @@ -#!/bin/zsh -set -euo pipefail - -EXECUTION_HOME="$HOME" -EXECUTION_PATH="$PATH" - -[[ "$(uname -s)" == "Darwin" ]] || { print -u2 -- "ERROR: 此脚本只支持 macOS"; exit 1; } - -SCRIPT_DIR="${0:A:h}" -SRC_DIR="${SCRIPT_DIR:h:h}" -LABEL="com.uvwt.agentdock" -TARGET="$HOME/.local/bin/agentdock" -BACKUP_DIR="$HOME/.agentdock/backups/bin" -APP_SUPPORT_DIR="$HOME/Library/Application Support/AgentDock" -AGENTDOCK_ENV="$APP_SUPPORT_DIR/agentdock.env" -PLIST_PATH="$HOME/Library/LaunchAgents/$LABEL.plist" -SIGN_SCRIPT="$SRC_DIR/packaging/macos/sign-macos.sh" -INSTALL_DIR="${TARGET:h}" -STAMP="$(date +%Y%m%d%H%M%S)" -TMP_BIN="$INSTALL_DIR/.agentdock.source.$STAMP.$$" -ROLLBACK_BIN="$INSTALL_DIR/.agentdock.rollback.$STAMP.$$" -CORE_SKILL_TEMP_DIR="" -CORE_SKILL_BUNDLE="" - -EXPLICIT_SIGN_IDENTITY="${AGENTDOCK_CODESIGN_IDENTITY:-}" -EXPLICIT_SIGN_KEYCHAIN="${AGENTDOCK_CODESIGN_KEYCHAIN:-}" -EXPLICIT_SIGN_KEYCHAIN_PASSWORD="${AGENTDOCK_CODESIGN_KEYCHAIN_PASSWORD:-}" -EXPLICIT_SIGN_IDENTIFIER="${AGENTDOCK_CODESIGN_IDENTIFIER:-}" -EXPLICIT_SIGN_HOME="${AGENTDOCK_CODESIGN_HOME:-}" - -cleanup() { - rm -f "$TMP_BIN" "$ROLLBACK_BIN" - [[ -z "$CORE_SKILL_TEMP_DIR" ]] || rm -rf "$CORE_SKILL_TEMP_DIR" -} -trap cleanup EXIT - -die() { - print -u2 -- "ERROR: $*" - exit 1 -} - -require_command() { - command -v "$1" >/dev/null 2>&1 || die "缺少命令:$1" -} - -next_backup_path() { - local base="$BACKUP_DIR/agentdock.$STAMP" - local candidate="$base" - local suffix=1 - while [[ -e "$candidate" ]]; do - candidate="$base.$suffix" - (( suffix++ )) - done - print -r -- "$candidate" -} - -normalize_version() { - print -r -- "${1#v}" -} - -launchd_pid() { - local domain="$1" - local output - output="$(launchctl print "$domain/$LABEL" 2>/dev/null)" || return 1 - print -r -- "$output" | sed -n 's/^[[:space:]]*pid = \([0-9][0-9]*\).*$/\1/p' | head -n 1 -} - -health_host() { - case "$AGENTDOCK_HOST" in - 0.0.0.0|::) print -r -- "127.0.0.1" ;; - *:*) print -r -- "[$AGENTDOCK_HOST]" ;; - *) print -r -- "$AGENTDOCK_HOST" ;; - esac -} - -wait_for_service() { - local domain="$1" - local previous_pid="$2" - local expected_version="$3" - local host="$(health_host)" - local health_url="http://$host:$AGENTDOCK_PORT/healthz" - local attempts=60 - - while (( attempts-- > 0 )); do - local pid="$(launchd_pid "$domain" || true)" - if [[ -n "$pid" && "$pid" != "0" && "$pid" != "$previous_pid" ]]; then - local process_command="$(ps -p "$pid" -o command= 2>/dev/null || true)" - local listeners="$(lsof -nP -iTCP:"$AGENTDOCK_PORT" -sTCP:LISTEN -t 2>/dev/null || true)" - if [[ "$process_command" == "$TARGET" || "$process_command" == "$TARGET "* ]] && print -r -- "$listeners" | grep -qx "$pid"; then - local health_body="$(curl -fsS --max-time 2 "$health_url" 2>/dev/null || true)" - local health_ok=false - local health_version - if print -r -- "$health_body" | grep -Eq '"ok"[[:space:]]*:[[:space:]]*true'; then - health_ok=true - fi - health_version="$(print -r -- "$health_body" | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)" - if [[ "$health_ok" == true && "$(normalize_version "$health_version")" == "$(normalize_version "$expected_version")" ]]; then - print -r -- "$pid" - return 0 - fi - fi - fi - sleep 0.5 - done - - print -u2 -- "未确认 LaunchAgent 新 PID、目标二进制、端口监听和目标版本 healthz" - return 1 -} - -restart_and_verify() { - local expected_version="$1" - local previous_pid="$2" - local domain="gui/$(id -u)" - launchctl kickstart -k "$domain/$LABEL" - wait_for_service "$domain" "$previous_pid" "$expected_version" -} - -restore_previous_binary() { - local backup="$1" - local old_version="$2" - local failed_pid="$(launchd_pid "gui/$(id -u)" || true)" - - cp -p "$backup" "$ROLLBACK_BIN" - chmod 0755 "$ROLLBACK_BIN" - mv -f "$ROLLBACK_BIN" "$TARGET" - local rollback_pid - rollback_pid="$(restart_and_verify "$old_version" "$failed_pid")" || return 1 - print -u2 -- "已恢复旧二进制并重新启动:pid=$rollback_pid version=$old_version" -} - -for command_name in curl git go gofmt grep launchctl lsof plutil ps python3 sed; do - require_command "$command_name" -done - -[[ -d "$SRC_DIR/.git" ]] || die "源码目录不是 Git 仓库:$SRC_DIR" -[[ -f "$AGENTDOCK_ENV" && ! -L "$AGENTDOCK_ENV" ]] || die "缺少标准服务配置:$AGENTDOCK_ENV" -[[ -f "$PLIST_PATH" && ! -L "$PLIST_PATH" ]] || die "缺少标准 LaunchAgent:$PLIST_PATH" -[[ -x "$SIGN_SCRIPT" && ! -L "$SIGN_SCRIPT" ]] || die "缺少仓库签名脚本:$SIGN_SCRIPT" -[[ -x "$TARGET" && ! -L "$TARGET" ]] || die "缺少当前生产二进制:$TARGET" -plutil -lint "$PLIST_PATH" >/dev/null - -source_status="$(git -C "$SRC_DIR" status --porcelain --untracked-files=normal)" -[[ -z "$source_status" ]] || die "源码工作区不干净,拒绝部署无法追溯的构建:\n$source_status" - -[[ "$(plutil -extract ProgramArguments.0 raw -o - "$PLIST_PATH")" == "$TARGET" ]] || die "LaunchAgent 未直接指向标准二进制:$PLIST_PATH" -[[ "$(plutil -extract ProgramArguments.1 raw -o - "$PLIST_PATH")" == "service" ]] || die "LaunchAgent 缺少 service 命令" -[[ "$(plutil -extract ProgramArguments.2 raw -o - "$PLIST_PATH")" == "launch-core" ]] || die "LaunchAgent 缺少 launch-core 命令" -[[ "$(plutil -extract ProgramArguments.3 raw -o - "$PLIST_PATH")" == "--runtime-root" ]] || die "LaunchAgent 缺少 runtime-root 参数" -[[ "$(plutil -extract ProgramArguments.4 raw -o - "$PLIST_PATH")" == "$APP_SUPPORT_DIR" ]] || die "LaunchAgent runtime-root 不正确" -domain="gui/$(id -u)" -old_pid="$(launchd_pid "$domain" || true)" -[[ -n "$old_pid" && "$old_pid" != "0" ]] || die "LaunchAgent 未运行:$domain/$LABEL" - -set -a -source "$AGENTDOCK_ENV" -set +a -export HOME="$EXECUTION_HOME" -export PATH="$EXECUTION_PATH" -: "${AGENTDOCK_HOST:=127.0.0.1}" -: "${AGENTDOCK_PORT:=8765}" -: "${AGENTDOCK_LOG_LEVEL:=info}" - -# 命令行环境用于一次性部署时优先级最高;agentdock.env 负责让后续自更新继承同一配置。 -[[ -n "$EXPLICIT_SIGN_IDENTITY" ]] && export AGENTDOCK_CODESIGN_IDENTITY="$EXPLICIT_SIGN_IDENTITY" -[[ -n "$EXPLICIT_SIGN_KEYCHAIN" ]] && export AGENTDOCK_CODESIGN_KEYCHAIN="$EXPLICIT_SIGN_KEYCHAIN" -[[ -n "$EXPLICIT_SIGN_KEYCHAIN_PASSWORD" ]] && export AGENTDOCK_CODESIGN_KEYCHAIN_PASSWORD="$EXPLICIT_SIGN_KEYCHAIN_PASSWORD" -[[ -n "$EXPLICIT_SIGN_IDENTIFIER" ]] && export AGENTDOCK_CODESIGN_IDENTIFIER="$EXPLICIT_SIGN_IDENTIFIER" -[[ -n "$EXPLICIT_SIGN_HOME" ]] && export AGENTDOCK_CODESIGN_HOME="$EXPLICIT_SIGN_HOME" -[[ -n "${AGENTDOCK_CODESIGN_IDENTITY:-}" ]] || die "AGENTDOCK_CODESIGN_IDENTITY 不能为空" -[[ "$AGENTDOCK_PORT" == <1-65535> ]] || die "agentdock.env 中端口无效:$AGENTDOCK_PORT" - -mkdir -p "$INSTALL_DIR" "$BACKUP_DIR" -chmod 0700 "$BACKUP_DIR" - -cd "$SRC_DIR" -printf '==> source: %s\n' "$SRC_DIR" -printf '==> target: %s\n' "$TARGET" -printf '==> backup_dir: %s\n' "$BACKUP_DIR" - -printf '==> running gofmt check\n' -unformatted="$(gofmt -l ./cmd ./internal)" -[[ -z "$unformatted" ]] || die "以下 Go 文件未格式化:\n$unformatted" - -printf '==> running tests\n' -go test ./... - -printf '==> running go vet\n' -go vet ./... - -printf '==> building temporary binary\n' -BUILD_COMMIT="$(git rev-parse --short=12 HEAD 2>/dev/null || printf unknown)" -BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" -go build -trimpath \ - -ldflags "-X github.com/uvwt/agentdock/internal/buildinfo.Commit=$BUILD_COMMIT -X github.com/uvwt/agentdock/internal/buildinfo.BuildDate=$BUILD_DATE" \ - -o "$TMP_BIN" ./cmd/agentdock -chmod 0755 "$TMP_BIN" - -printf '==> building official core Skill Bundle\n' -CORE_SKILL_TEMP_DIR="$(mktemp -d)" -CORE_SKILL_BUNDLE="$CORE_SKILL_TEMP_DIR/core-skills" -python3 "$SRC_DIR/packaging/build-core-skill-bundle.py" \ - --repo-root "$SRC_DIR" \ - --output "$CORE_SKILL_BUNDLE" - -printf '==> signing temporary binary\n' -"$SIGN_SCRIPT" "$TMP_BIN" - -target_version="$("$TMP_BIN" --version | sed -n '1s/^AgentDock[[:space:]][[:space:]]*//p')" -old_version="$("$TARGET" --version | sed -n '1s/^AgentDock[[:space:]][[:space:]]*//p')" -[[ -n "$target_version" ]] || die "无法读取新二进制版本" -[[ -n "$old_version" ]] || die "无法读取旧二进制版本" - -backup="$(next_backup_path)" -cp -p "$TARGET" "$backup" -printf '==> backed up current binary: %s\n' "$backup" - -# 临时二进制与目标位于同一目录,签名和版本验证完成后才原子替换生产路径。 -mv -f "$TMP_BIN" "$TARGET" -installed_version="$("$TARGET" --version | sed -n '1s/^AgentDock[[:space:]][[:space:]]*//p')" -if [[ "$(normalize_version "$installed_version")" != "$(normalize_version "$target_version")" ]]; then - if ! restore_previous_binary "$backup" "$old_version"; then - die "原子替换后的版本不匹配,且旧版本恢复验证失败;备份保留在 $backup" - fi - die "原子替换后的版本不匹配:期望 ${target_version},实际 ${installed_version};已恢复旧版本" -fi - -printf '==> restarting %s\n' "$LABEL" -if ! new_pid="$(restart_and_verify "$target_version" "$old_pid")"; then - print -u2 -- "新版本服务验证失败,开始恢复旧二进制" - if ! restore_previous_binary "$backup" "$old_version"; then - die "新版本验证失败,且旧版本恢复验证失败;备份保留在 $backup" - fi - die "新版本服务验证失败,已恢复旧版本" -fi - -printf '==> installing official core Skills\n' -if ! "$TARGET" skill bootstrap --bundle "$CORE_SKILL_BUNDLE"; then - print -u2 -- "核心 Skill 初始化失败,开始恢复旧二进制" - if ! restore_previous_binary "$backup" "$old_version"; then - die "核心 Skill 初始化失败,且旧版本恢复验证失败;备份保留在 $backup" - fi - die "核心 Skill 初始化失败,已恢复旧版本" -fi - -printf 'installed: %s\n' "$TARGET" -printf 'backup: %s\n' "$backup" -printf 'launchd: %s pid=%s\n' "$LABEL" "$new_pid" -printf 'healthz: host=%s port=%s version=%s\n' "$AGENTDOCK_HOST" "$AGENTDOCK_PORT" "$target_version" diff --git a/scripts/dev/restart-macos.sh b/scripts/dev/restart-macos.sh deleted file mode 100755 index 38647ce8..00000000 --- a/scripts/dev/restart-macos.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/zsh -set -euo pipefail - -EXECUTION_HOME="$HOME" -EXECUTION_PATH="$PATH" -LABEL="com.uvwt.agentdock" -AGENTDOCK_ENV="$HOME/Library/Application Support/AgentDock/agentdock.env" -TARGET="$HOME/.local/bin/agentdock" - -[[ "$(uname -s)" == "Darwin" ]] || { print -u2 -- "ERROR: 此脚本只支持 macOS"; exit 1; } -[[ -f "$AGENTDOCK_ENV" && ! -L "$AGENTDOCK_ENV" ]] || { print -u2 -- "ERROR: 缺少服务配置:$AGENTDOCK_ENV"; exit 1; } -[[ -x "$TARGET" && ! -L "$TARGET" ]] || { print -u2 -- "ERROR: 缺少二进制:$TARGET"; exit 1; } - -set -a -source "$AGENTDOCK_ENV" -set +a -export HOME="$EXECUTION_HOME" -export PATH="$EXECUTION_PATH" -: "${AGENTDOCK_HOST:=127.0.0.1}" -: "${AGENTDOCK_PORT:=8765}" -[[ "$AGENTDOCK_PORT" == <1-65535> ]] || { print -u2 -- "ERROR: 端口无效:$AGENTDOCK_PORT"; exit 1; } - -normalize_version() { - print -r -- "${1#v}" -} - -launchd_pid() { - local output - output="$(launchctl print "gui/$(id -u)/$LABEL" 2>/dev/null)" || return 1 - print -r -- "$output" | sed -n 's/^[[:space:]]*pid = \([0-9][0-9]*\).*$/\1/p' | head -n 1 -} - -case "$AGENTDOCK_HOST" in - 0.0.0.0|::) HEALTH_HOST="127.0.0.1" ;; - *:*) HEALTH_HOST="[$AGENTDOCK_HOST]" ;; - *) HEALTH_HOST="$AGENTDOCK_HOST" ;; -esac -HEALTH_URL="http://$HEALTH_HOST:$AGENTDOCK_PORT/healthz" -EXPECTED_VERSION="$("$TARGET" --version | sed -n '1s/^AgentDock[[:space:]][[:space:]]*//p')" -[[ -n "$EXPECTED_VERSION" ]] || { print -u2 -- "ERROR: 无法读取目标二进制版本:$TARGET"; exit 1; } -OLD_PID="$(launchd_pid || true)" -[[ -n "$OLD_PID" && "$OLD_PID" != "0" ]] || { print -u2 -- "ERROR: LaunchAgent 未运行:$LABEL"; exit 1; } - -launchctl kickstart -k "gui/$(id -u)/$LABEL" -attempts=60 -while (( attempts-- > 0 )); do - NEW_PID="$(launchd_pid || true)" - if [[ -n "$NEW_PID" && "$NEW_PID" != "0" && "$NEW_PID" != "$OLD_PID" ]]; then - PROCESS_COMMAND="$(ps -p "$NEW_PID" -o command= 2>/dev/null || true)" - LISTENERS="$(lsof -nP -iTCP:"$AGENTDOCK_PORT" -sTCP:LISTEN -t 2>/dev/null || true)" - if [[ "$PROCESS_COMMAND" == "$TARGET" || "$PROCESS_COMMAND" == "$TARGET "* ]] && print -r -- "$LISTENERS" | grep -qx "$NEW_PID"; then - HEALTH_BODY="$(curl -fsS --max-time 2 "$HEALTH_URL" 2>/dev/null || true)" - HEALTH_OK=false - if print -r -- "$HEALTH_BODY" | grep -Eq '"ok"[[:space:]]*:[[:space:]]*true'; then - HEALTH_OK=true - fi - HEALTH_VERSION="$(print -r -- "$HEALTH_BODY" | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)" - if [[ "$HEALTH_OK" == true && "$(normalize_version "$HEALTH_VERSION")" == "$(normalize_version "$EXPECTED_VERSION")" ]]; then - print -- "restarted: $LABEL" - print -- "pid: $NEW_PID" - print -- "healthz: $HEALTH_URL" - print -- "version: $EXPECTED_VERSION" - exit 0 - fi - fi - fi - sleep 0.5 -done - -print -u2 -- "ERROR: 重启后未确认新 PID、目标二进制、端口监听和目标版本 healthz" -exit 1 diff --git a/scripts/governance/inventory.yaml b/scripts/governance/inventory.yaml index dbdb6a33..36db56d4 100644 --- a/scripts/governance/inventory.yaml +++ b/scripts/governance/inventory.yaml @@ -45,18 +45,6 @@ scripts: deletable: false notes: Windows Setup 的只读 DPAPI 可用性探针;只返回状态码,不输出凭据明文。 - - path: scripts/dev/deploy-macos-source.sh - class: dev - owner: desktop - public_contract: false - deletable: true - - - path: scripts/dev/restart-macos.sh - class: dev - owner: desktop - public_contract: false - deletable: true - - path: scripts/test/check-scripts.sh class: test owner: ci @@ -179,13 +167,6 @@ scripts: legacy_oversize: true oversize_justification: macOS App/DMG 构建。签名和 bundle 布局必须用平台工具;版本与 checksum 归 tools/release。 - - path: packaging/macos/sign-macos.sh - class: packaging - owner: release - public_contract: false - deletable: false - notes: 调用 codesign / notarytool,保留为薄适配。 - - path: packaging/windows/build-windows-offline-setup.ps1 class: packaging owner: release