From f250ab6d845914a0ad710c2b742e9608781a9a95 Mon Sep 17 00:00:00 2001 From: che cheng Date: Thu, 2 Jul 2026 17:24:00 +0800 Subject: [PATCH 1/3] feat: macdoc CLI binary auto-install via session-start hook (#114) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLI v0.5.0 released (Developer ID signed + notarized, arm64-only — the CLI depends on MLX which is Apple Silicon exclusive; universal build is structurally impossible, not a compromise) - scripts/release-cli.sh: #119 hardened template adapted (single-arch gate) for future CLI releases - plugins/macdoc/hooks/session-start.sh: compares macdoc --version to plugin.json binary_version; missing/mismatch -> download + MANDATORY sha256 + Developer ID requirement verify (same ruler as wrappers and release gate) -> install to ~/bin (deliberate: CLI is a PATH tool, #117's .bin-cache rationale doesn't apply). Session fail-soft, artifact fail-closed. E2E: fresh install / 44ms fast path / fail-soft - shell 1.1.0 -> 1.2.0 + binary_version 0.5.0; CHANGELOG; SKILL.md; marketplace sync Refs #114 --- .claude-plugin/marketplace.json | 7 +- plugins/macdoc/.claude-plugin/plugin.json | 5 +- plugins/macdoc/CHANGELOG.md | 6 ++ plugins/macdoc/hooks/session-start.sh | 59 ++++++++++++++ plugins/macdoc/skills/macdoc/SKILL.md | 2 +- scripts/release-cli.sh | 97 +++++++++++++++++++++++ 6 files changed, 170 insertions(+), 6 deletions(-) create mode 100755 plugins/macdoc/hooks/session-start.sh create mode 100755 scripts/release-cli.sh diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 9c780fa..847ada4 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -20,13 +20,14 @@ }, { "name": "macdoc", - "version": "1.1.0", - "description": "macOS 原生文件處理 CLI — 格式轉換、VLM OCR(含 host profile 設定)、SRT 處理", + "version": "1.2.0", + "description": "macOS 原生文件處理 CLI — 格式轉換、VLM OCR(含 host profile 設定)、SRT 處理。v1.2.0: session-start hook 自動安裝 signed CLI binary(arm64)。", "author": { "name": "Che Cheng" }, "source": "./plugins/macdoc", - "category": "productivity" + "category": "productivity", + "binary_version": "0.5.0" }, { "name": "che-pdf-mcp", diff --git a/plugins/macdoc/.claude-plugin/plugin.json b/plugins/macdoc/.claude-plugin/plugin.json index 2a205cd..8873228 100644 --- a/plugins/macdoc/.claude-plugin/plugin.json +++ b/plugins/macdoc/.claude-plugin/plugin.json @@ -1,7 +1,8 @@ { "name": "macdoc", - "description": "macOS 原生文件處理 CLI — 格式轉換、VLM OCR(含 host profile 設定)、SRT 處理", - "version": "1.1.0", + "description": "macOS 原生文件處理 CLI — 格式轉換、VLM OCR(含 host profile 設定)、SRT 處理。v1.2.0: session-start hook 自動安裝 signed CLI binary(arm64)。", + "version": "1.2.0", + "binary_version": "0.5.0", "author": { "name": "Che Cheng" } diff --git a/plugins/macdoc/CHANGELOG.md b/plugins/macdoc/CHANGELOG.md index e5b1099..a2bf0fb 100644 --- a/plugins/macdoc/CHANGELOG.md +++ b/plugins/macdoc/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 > `plugin.json` description field. Section categorization is best-effort — > review and refine `Added` / `Changed` / `Fixed` etc. as needed. +## [1.2.0] - 2026-07-02 + +### Added + +- **CLI binary 自動安裝(PsychQuant/macdoc#114)**:`hooks/session-start.sh` 於 session 啟動比對 `~/bin/macdoc --version` 與 `binary_version`(=0.5.0,首次 CLI release),缺/不符即下載並以強制 sha256 + Developer ID requirement 驗證後安裝(與 MCP wrappers / release gate 同一把尺)。Session fail-soft(任何失敗警告即止、絕不擋 session)、artifact fail-closed(未驗過不裝)。arm64-only(CLI 依賴 MLX)。 + ## [Unreleased] ## [1.1.0] - (date unknown — please fill in) diff --git a/plugins/macdoc/hooks/session-start.sh b/plugins/macdoc/hooks/session-start.sh new file mode 100755 index 0000000..763b4f2 --- /dev/null +++ b/plugins/macdoc/hooks/session-start.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# macdoc plugin SessionStart hook — ensure ~/bin/macdoc matches the plugin's +# pinned binary_version (PsychQuant/macdoc#114). +# +# Contract: FAIL-SOFT for the session (any problem → one stderr line, exit 0 — +# never break session start), FAIL-CLOSED for the artifact (mandatory sha256 + +# Developer ID requirement — the same ruler as the MCP wrappers and the +# release gate — before anything is installed to ~/bin). +# +# Install target is the shared ~/bin ON PURPOSE (unlike the MCP wrappers' +# plugin-scoped .bin-cache, #117): the CLI is a user-facing PATH tool and +# ~/bin IS its destination; the cross-marketplace binary collision problem +# does not apply to a CLI the user invokes by name. + +set -u + +REPO="PsychQuant/macdoc" +BINARY_NAME="macdoc" +INSTALL_DIR="${MACDOC_INSTALL_DIR:-$HOME/bin}" # override for tests +BINARY="$INSTALL_DIR/$BINARY_NAME" +REQUIREMENT='=anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = "6W377FS7BS"' + +note() { echo "macdoc plugin: $1" >&2; } +soft_exit() { note "$1"; exit 0; } # fail-soft: never break session start + +[ "$(uname -m)" = "arm64" ] || exit 0 # arm64-only release; Intel builds from source (silent — not an error) + +PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PLUGIN_JSON="$PLUGIN_ROOT/.claude-plugin/plugin.json" +[ -f "$PLUGIN_JSON" ] || exit 0 + +WANT=$(grep -oE '"binary_version"[[:space:]]*:[[:space:]]*"[^"]+"' "$PLUGIN_JSON" 2>/dev/null \ + | head -1 | sed -E 's/.*"([^"]+)"$/\1/' || true) +[ -n "$WANT" ] || exit 0 # no pinned CLI version — nothing to manage + +HAVE=$("$BINARY" --version 2>/dev/null || true) +[ "$HAVE" = "$WANT" ] && exit 0 # fast path: version matches, zero network + +mkdir -p "$INSTALL_DIR" 2>/dev/null || soft_exit "cannot create $INSTALL_DIR — skipping auto-install" +TMP=$(mktemp "$INSTALL_DIR/.${BINARY_NAME}.download.XXXXXX" 2>/dev/null) || soft_exit "mktemp failed — skipping auto-install" +trap 'rm -f "$TMP"' EXIT + +URL="https://github.com/$REPO/releases/download/v$WANT/$BINARY_NAME" +curl -fsSL --proto '=https' --tlsv1.2 --max-time 300 "$URL" -o "$TMP" 2>/dev/null \ + || soft_exit "download failed for v$WANT (keeping existing ${HAVE:-none}); manual: https://github.com/$REPO/releases" + +EXPECTED=$(curl -fsSL --proto '=https' --tlsv1.2 --max-time 30 "$URL.sha256" 2>/dev/null | head -1 | awk '{print $1}') +[[ "$EXPECTED" =~ ^[0-9a-fA-F]{64}$ ]] \ + || soft_exit "missing/malformed .sha256 asset — refusing to install unverified binary" +[[ "$(shasum -a 256 "$TMP" | awk '{print $1}')" == "$EXPECTED" ]] \ + || soft_exit "sha256 mismatch — refusing to install" +codesign --verify --strict -R "$REQUIREMENT" "$TMP" 2>/dev/null \ + || soft_exit "code-signature verification failed (not Developer ID Team 6W377FS7BS) — refusing to install" + +chmod +x "$TMP" || soft_exit "chmod failed" +mv "$TMP" "$BINARY" || soft_exit "install mv failed" +trap - EXIT +note "installed macdoc v$WANT to $INSTALL_DIR (sha256 + Developer ID verified)" +exit 0 diff --git a/plugins/macdoc/skills/macdoc/SKILL.md b/plugins/macdoc/skills/macdoc/SKILL.md index c07f6a8..1277e55 100644 --- a/plugins/macdoc/skills/macdoc/SKILL.md +++ b/plugins/macdoc/skills/macdoc/SKILL.md @@ -10,7 +10,7 @@ description: | # macdoc — macOS 原生文件處理 CLI -安裝位置:`~/bin/macdoc`(或 `/usr/local/bin/macdoc`) +安裝位置:`~/bin/macdoc` — **plugin 自動安裝**(session-start hook 下載 signed release 並驗證 sha256 + Developer ID;arm64。Intel 從原始碼建置) 原始碼:https://github.com/PsychQuant/macdoc ## 子命令總覽 diff --git a/scripts/release-cli.sh b/scripts/release-cli.sh new file mode 100755 index 0000000..a1f0466 --- /dev/null +++ b/scripts/release-cli.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# release-cli.sh — signed + notarized release pipeline for the macdoc CLI. +# +# arm64-only: the CLI depends on MLX (Apple Silicon exclusive), so a +# universal build is neither possible nor meaningful. Intel users build +# from source (README). +# +# Usage: scripts/release-cli.sh # e.g. scripts/release-cli.sh 0.6.0 +# +# Pipeline: universal build → Developer ID codesign (hardened runtime + +# timestamp) → PRE-UPLOAD SIGNATURE GATE → notarize (must be Accepted) → +# universal check → sha256 → git tag → gh release with binary + .sha256. +# +# The gate exists because v3.20.0 of CheWordMCP shipped ad-hoc signed +# (che-word-mcp#165): the marketplace wrappers verify the SAME requirement +# on install/exec (PsychQuant/macdoc#112), so an unsigned asset means new +# installs hard-fail. Gate requirement string MUST stay in lockstep with +# the wrapper's verify_binary() (macdoc plugins/*/bin/*-wrapper.sh). +# +# SCOPE HONESTY (verify DA-1): this gate protects releases made THROUGH +# this script. A manual `gh release upload` bypasses it — the backstops +# for that path are process discipline and the wrappers' fail-closed +# install gate (which turns an unsigned asset into a loud install failure +# rather than a silent compromise). +# +# Refs PsychQuant/macdoc#119. + +set -euo pipefail + +BINARY_NAME="macdoc" +REPO="PsychQuant/macdoc" +DEVELOPER_ID="${DEVELOPER_ID:-F2523DCF6D02BE99B67C7D27F633119292DA4934}" +NOTARY_PROFILE="${NOTARY_PROFILE:-che-mcps-notary}" +REQUIREMENT='=anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = "6W377FS7BS"' + +VERSION="${1:-}" +[[ -n "$VERSION" ]] || { echo "usage: scripts/release-cli.sh (e.g. 0.6.0, no leading v)" >&2; exit 2; } +[[ "$VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z][0-9A-Za-z.-]*)?$ ]] || { echo "error: version '$VERSION' is not semver (MAJOR.MINOR.PATCH[-prerelease], no leading zeros)" >&2; exit 2; } + +cd "$(dirname "${BASH_SOURCE[0]}")/.." + +echo "→ [0/7] pre-flight: notary profile alive?" +xcrun notarytool history --keychain-profile "$NOTARY_PROFILE" >/dev/null 2>&1 \ + || { echo "error: notary profile '$NOTARY_PROFILE' unusable — run: xcrun notarytool store-credentials $NOTARY_PROFILE (interactive, user-only)" >&2; exit 3; } +[[ -z "$(git status --porcelain)" ]] \ + || { echo "error: working tree not clean (including untracked files — they could leak into the build) — commit, stash, or clean first" >&2; exit 3; } +if git rev-parse -q --verify "refs/tags/v$VERSION" >/dev/null 2>&1; then + echo "error: local tag v$VERSION already exists" >&2; exit 3 +fi +if [[ -n "$(git ls-remote --tags origin "refs/tags/v$VERSION" 2>/dev/null)" ]]; then + echo "error: remote tag v$VERSION already exists" >&2; exit 3 +fi +if gh release view "v$VERSION" --repo "$REPO" >/dev/null 2>&1; then + echo "error: release v$VERSION already exists on $REPO" >&2; exit 3 +fi + +echo "→ [1/7] release build (arm64 — MLX is Apple Silicon only)" +swift build -c release +BIN=".build/release/$BINARY_NAME" +[[ -f "$BIN" ]] || { echo "error: built binary not found at $BIN" >&2; exit 4; } + +echo "→ [2/7] codesign (Developer ID, hardened runtime, timestamp)" +codesign --force --options runtime --timestamp --sign "$DEVELOPER_ID" "$BIN" + +echo "→ [3/7] PRE-UPLOAD SIGNATURE GATE (requirement-based, matches wrapper)" +codesign --verify --strict -R "$REQUIREMENT" "$BIN" \ + || { echo "error: GATE FAILED — asset is not a Developer ID Application binary of Team 6W377FS7BS; refusing to release (this is the che-word-mcp#165 guard)" >&2; exit 5; } +ARCHS=" $(lipo -archs "$BIN" 2>/dev/null) " +[[ "$ARCHS" == *" arm64 "* ]] \ + || { echo "error: GATE FAILED — binary lacks arm64 (got:$ARCHS)" >&2; exit 5; } + +echo "→ [4/7] notarize (must be Accepted)" +WORKDIR=$(mktemp -d) +trap 'rm -rf "$WORKDIR"' EXIT +ditto -c -k --keepParent "$BIN" "$WORKDIR/$BINARY_NAME.zip" +NOTARY_OUT=$(xcrun notarytool submit "$WORKDIR/$BINARY_NAME.zip" --keychain-profile "$NOTARY_PROFILE" --wait 2>&1) +echo "$NOTARY_OUT" | grep -q "status: Accepted" \ + || { echo "error: notarization not Accepted:" >&2; echo "$NOTARY_OUT" | tail -5 >&2; exit 6; } + +echo "→ [5/7] sha256 asset" +cp "$BIN" "$WORKDIR/$BINARY_NAME" +shasum -a 256 "$WORKDIR/$BINARY_NAME" | awk '{print $1}' > "$WORKDIR/$BINARY_NAME.sha256" + +echo "→ [6/7] FINAL GATE — re-verify the exact upload artifact (TOCTOU guard)" +codesign --verify --strict -R "$REQUIREMENT" "$WORKDIR/$BINARY_NAME" \ + || { echo "error: FINAL GATE FAILED — upload artifact no longer passes the signature requirement (mutated after step 3?)" >&2; exit 5; } +[[ "$(shasum -a 256 "$WORKDIR/$BINARY_NAME" | awk '{print $1}')" == "$(cat "$WORKDIR/$BINARY_NAME.sha256")" ]] \ + || { echo "error: FINAL GATE FAILED — sha256 asset does not match upload artifact" >&2; exit 5; } + +echo "→ [7/7] gh release create (creates tag v$VERSION at HEAD — no pre-pushed tag, so a create failure leaves no dead-end state)" +gh release create "v$VERSION" --repo "$REPO" \ + --target "$(git rev-parse HEAD)" \ + --title "v$VERSION" \ + --notes "Developer ID signed + Apple notarized arm64 binary (CLI depends on MLX — Apple Silicon only; Intel builds from source). Released via scripts/release.sh (pre-upload signature gate, PsychQuant/macdoc#119)." \ + "$WORKDIR/$BINARY_NAME" "$WORKDIR/$BINARY_NAME.sha256" + +echo "✓ released $BINARY_NAME v$VERSION (signed, notarized, gated, sha256 attached)" From 6fec1dd1bd892878511488e0659b29f7b7f80942 Mon Sep 17 00:00:00 2001 From: che cheng Date: Thu, 2 Jul 2026 19:55:08 +0800 Subject: [PATCH 2/3] fix: register session-start hook via hooks/hooks.json (#114) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verify requirements BLOCKING: hooks are NOT auto-discovered by filename — without hooks/hooks.json the auto-install hook was dead code (format per che-ical-mcp precedent, the plugin whose SessionStart demonstrably fires). Also release-cli.sh notes self-reference nit. Refs #114 --- plugins/macdoc/hooks/hooks.json | 14 ++++++++++++++ scripts/release-cli.sh | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 plugins/macdoc/hooks/hooks.json diff --git a/plugins/macdoc/hooks/hooks.json b/plugins/macdoc/hooks/hooks.json new file mode 100644 index 0000000..d087c3a --- /dev/null +++ b/plugins/macdoc/hooks/hooks.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh" + } + ] + } + ] + } +} diff --git a/scripts/release-cli.sh b/scripts/release-cli.sh index a1f0466..08f05e3 100755 --- a/scripts/release-cli.sh +++ b/scripts/release-cli.sh @@ -91,7 +91,7 @@ echo "→ [7/7] gh release create (creates tag v$VERSION at HEAD — no pre-push gh release create "v$VERSION" --repo "$REPO" \ --target "$(git rev-parse HEAD)" \ --title "v$VERSION" \ - --notes "Developer ID signed + Apple notarized arm64 binary (CLI depends on MLX — Apple Silicon only; Intel builds from source). Released via scripts/release.sh (pre-upload signature gate, PsychQuant/macdoc#119)." \ + --notes "Developer ID signed + Apple notarized arm64 binary (CLI depends on MLX — Apple Silicon only; Intel builds from source). Released via scripts/release-cli.sh (pre-upload signature gate, PsychQuant/macdoc#119)." \ "$WORKDIR/$BINARY_NAME" "$WORKDIR/$BINARY_NAME.sha256" echo "✓ released $BINARY_NAME v$VERSION (signed, notarized, gated, sha256 attached)" From bcb08fd8951169c91dc39d62be0fa94ae9fb8128 Mon Sep 17 00:00:00 2001 From: che cheng Date: Thu, 2 Jul 2026 20:14:25 +0800 Subject: [PATCH 3/3] fix: hook hardening from verify round (#114) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex V114 findings: HIGH-1 --version probe now runs under a 5s perl alarm writing to a FILE (a killed probe's grandchildren holding an inherited pipe fd stalled command substitution far past the alarm — empirically reproduced with a sleep-300 plant; file redirection makes the hang self-heal into a verified reinstall). M-2 semver-token normalize + loop-guard sidecar prevents banner-format re-download loops. M-3 hooks.json command quoted. M-4 release-cli.sh header pipeline text aligned to arm64. M-5 unset-HOME guard before set -u expansion. Job-control alarm noise suppressed. Refs #114 --- plugins/macdoc/hooks/hooks.json | 2 +- plugins/macdoc/hooks/session-start.sh | 22 +++++++++++++++++++++- scripts/release-cli.sh | 4 ++-- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/plugins/macdoc/hooks/hooks.json b/plugins/macdoc/hooks/hooks.json index d087c3a..7de73c0 100644 --- a/plugins/macdoc/hooks/hooks.json +++ b/plugins/macdoc/hooks/hooks.json @@ -5,7 +5,7 @@ "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh" + "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/session-start.sh" } ] } diff --git a/plugins/macdoc/hooks/session-start.sh b/plugins/macdoc/hooks/session-start.sh index 763b4f2..3fa8d32 100755 --- a/plugins/macdoc/hooks/session-start.sh +++ b/plugins/macdoc/hooks/session-start.sh @@ -16,6 +16,7 @@ set -u REPO="PsychQuant/macdoc" BINARY_NAME="macdoc" +[ -n "${HOME:-}" ] || exit 0 # no HOME (exotic env) — nothing sane to do, never break session INSTALL_DIR="${MACDOC_INSTALL_DIR:-$HOME/bin}" # override for tests BINARY="$INSTALL_DIR/$BINARY_NAME" REQUIREMENT='=anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = "6W377FS7BS"' @@ -33,9 +34,27 @@ WANT=$(grep -oE '"binary_version"[[:space:]]*:[[:space:]]*"[^"]+"' "$PLUGIN_JSON | head -1 | sed -E 's/.*"([^"]+)"$/\1/' || true) [ -n "$WANT" ] || exit 0 # no pinned CLI version — nothing to manage -HAVE=$("$BINARY" --version 2>/dev/null || true) +# --version with a 5s alarm (a hung/planted binary must not stall every +# session start — fail-soft covers errors, not hangs; codex V114 HIGH-1). +# Probe writes to a FILE, not a pipe: a killed probe may leave grandchildren +# holding an inherited pipe fd, and command substitution would then wait on +# the pipe far past the alarm (empirically reproduced with a sleep-300 fake). +# Normalize to the semver token so banner-style output doesn't force a +# re-download loop (codex V114 M-2). +HAVE="" +PROBE=$(mktemp "${TMPDIR:-/tmp}/.macdoc.probe.XXXXXX" 2>/dev/null) || PROBE="" +if [ -n "$PROBE" ]; then + { perl -e 'alarm 5; exec @ARGV' -- "$BINARY" --version "$PROBE" 2>/dev/null; } 2>/dev/null || true + HAVE=$(head -1 "$PROBE" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true) + rm -f "$PROBE" +fi [ "$HAVE" = "$WANT" ] && exit 0 # fast path: version matches, zero network +# Loop-guard sidecar: if a previous session already installed WANT but the +# binary self-reports an unparsable/odd version, do not re-download forever. +GUARD="$INSTALL_DIR/.${BINARY_NAME}.installed_version" +[ -x "$BINARY" ] && [ "$(cat "$GUARD" 2>/dev/null)" = "$WANT" ] && exit 0 + mkdir -p "$INSTALL_DIR" 2>/dev/null || soft_exit "cannot create $INSTALL_DIR — skipping auto-install" TMP=$(mktemp "$INSTALL_DIR/.${BINARY_NAME}.download.XXXXXX" 2>/dev/null) || soft_exit "mktemp failed — skipping auto-install" trap 'rm -f "$TMP"' EXIT @@ -55,5 +74,6 @@ codesign --verify --strict -R "$REQUIREMENT" "$TMP" 2>/dev/null \ chmod +x "$TMP" || soft_exit "chmod failed" mv "$TMP" "$BINARY" || soft_exit "install mv failed" trap - EXIT +echo "$WANT" > "$GUARD" 2>/dev/null || true note "installed macdoc v$WANT to $INSTALL_DIR (sha256 + Developer ID verified)" exit 0 diff --git a/scripts/release-cli.sh b/scripts/release-cli.sh index 08f05e3..f41a896 100755 --- a/scripts/release-cli.sh +++ b/scripts/release-cli.sh @@ -7,9 +7,9 @@ # # Usage: scripts/release-cli.sh # e.g. scripts/release-cli.sh 0.6.0 # -# Pipeline: universal build → Developer ID codesign (hardened runtime + +# Pipeline: arm64 release build → Developer ID codesign (hardened runtime + # timestamp) → PRE-UPLOAD SIGNATURE GATE → notarize (must be Accepted) → -# universal check → sha256 → git tag → gh release with binary + .sha256. +# arm64 arch check → sha256 → gh release (creates tag) with binary + .sha256. # # The gate exists because v3.20.0 of CheWordMCP shipped ad-hoc signed # (che-word-mcp#165): the marketplace wrappers verify the SAME requirement