diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index cc817c3..940c171 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -40,13 +40,14 @@ }, { "name": "che-apple-mail-mcp", - "version": "2.18.1", - "description": "Apple Mail MCP server — 47 tools, reply-as-draft + IDD task enforcement + NSQL confirmation + .claude/.mail/ namespace (shell v2.18.1, binary v2.7.2)。Shell v2.18.1 ships binary v2.7.2: #71 get_email_metadata SQLite fallback parity (last of 8 read tools to gain do/catch wrapper) + cluster #61-64 attachmentFragment hardening (indent normalization, dead-helper deletion, 50-attachment count cap, env-configurable CHE_MAIL_ATTACHMENT_DELAY_BETWEEN/_TRAILING delays)。疊加 v2.18.0 (#76) Staleness Detection — wrapper writes runtime state file on spawn (pid/started_at/version_at_spawn);新增 hooks/session-start.sh,session 啟動時偵測 plugin.json 與 in-memory binary 版本 drift,SIGTERM(+5s grace,SIGKILL fallback)stale PID 讓 Claude Code 重 spawn 取新 binary,徹底解決「plugin update 後當下 session 仍跑舊 binary」silent staleness 問題。疊加 v2.17.0 (#49) Workspace Layout Detection;v2.16.1 ship 鏈到 binary v2.7.1 (#72 base64 fix + #69 SQLite fast-path stderr logging + #66 .partial.emlx attachment fix);v2.16.0 config schema .md → .yaml (#47);v2.15.0 inline cid: 圖片保留 (#45);v2.14.0 dedup_strategy opt-in (#18);v2.13.0 markdown template 簡化 (#17);v2.12.0 zero-arg mode (#13/#21);v2.11.0 binary v2.7.0 multi-attachment race fix (#60);v2.6.0 marathon (8 PRs);v2.9.0 task enforcement;v2.8.0 namespace;v2.7.0 NSQL confirmation。", + "version": "2.19.0", + "description": "Apple Mail MCP server — 48 tools, reply-as-draft + IDD task enforcement + NSQL confirmation + .claude/.mail/ namespace (shell v2.19.0, binary v2.8.0)。Shell v2.19.0 ships binary v2.8.0: (#19) `sanitize_links` opt-in URL scheme allowlist for markdown mode (defends against `[click](javascript:alert('xss'))` and `data:`/`file:`/`vbscript:` XSS vectors with closed allowlist `{http, https, mailto, tel}`; default `false` preserves backwards compat) + (#85) formal `openspec/specs/message-composition/spec.md` Requirement+Scenarios codifying the allowlist contract + builder-layer wiring contract tests pinning `sanitizeLinks` forwarding across `buildComposeEmailScript/buildCreateDraftScript/buildReplyEmailScript/buildForwardEmailScript` + cluster A hygiene (#20 dead spec scenario delete / count-free CHANGELOG / `assertOrdered` helper, #21 reply/forward AppleScript-html-denial docs, #25 `list_attachments_batch` SQLite+.emlx cross-validation parity with single-message handler, #27 `EmlxParser.attachmentNames` <200ms latency budget regression test, #32 `attachmentNames`↔`saveAttachment` matcher parity invariant test across 4 fixtures) + (#73) `extractHTMLBody` base64+UTF-8 quoted-printable decoding fixes。疊加 v2.18.1 (#71 get_email_metadata SQLite fallback parity + cluster #61-64 attachmentFragment hardening);v2.18.0 (#76) Staleness Detection — wrapper writes runtime state file on spawn (pid/started_at/version_at_spawn);新增 hooks/session-start.sh,session 啟動時偵測 plugin.json 與 in-memory binary 版本 drift,SIGTERM(+5s grace,SIGKILL fallback)stale PID 讓 Claude Code 重 spawn 取新 binary,徹底解決「plugin update 後當下 session 仍跑舊 binary」silent staleness 問題。疊加 v2.17.0 (#49) Workspace Layout Detection;v2.16.1 ship 鏈到 binary v2.7.1 (#72 base64 fix + #69 SQLite fast-path stderr logging + #66 .partial.emlx attachment fix);v2.16.0 config schema .md → .yaml (#47);v2.15.0 inline cid: 圖片保留 (#45);v2.14.0 dedup_strategy opt-in (#18);v2.13.0 markdown template 簡化 (#17);v2.12.0 zero-arg mode (#13/#21);v2.11.0 binary v2.7.0 multi-attachment race fix (#60);v2.6.0 marathon (8 PRs);v2.9.0 task enforcement;v2.8.0 namespace;v2.7.0 NSQL confirmation。", "author": { "name": "Che Cheng" }, "source": "./plugins/che-apple-mail-mcp", - "category": "productivity" + "category": "productivity", + "binary_version": "2.8.0" }, { "name": "che-apple-notes-mcp", diff --git a/plugins/che-apple-mail-mcp/.claude-plugin/plugin.json b/plugins/che-apple-mail-mcp/.claude-plugin/plugin.json index 78670ca..2961f22 100644 --- a/plugins/che-apple-mail-mcp/.claude-plugin/plugin.json +++ b/plugins/che-apple-mail-mcp/.claude-plugin/plugin.json @@ -1,8 +1,8 @@ { "name": "che-apple-mail-mcp", - "version": "2.18.1", - "binary_version": "2.7.2", - "description": "Apple Mail MCP server — 47 tools, reply-as-draft + IDD task enforcement + NSQL confirmation + .claude/.mail/ namespace (shell v2.18.1, binary v2.7.2)。Shell v2.18.1 ships binary v2.7.2: #71 get_email_metadata SQLite fallback parity (last of 8 read tools to gain do/catch wrapper) + cluster #61-64 attachmentFragment hardening (indent normalization, dead-helper deletion, 50-attachment count cap, env-configurable CHE_MAIL_ATTACHMENT_DELAY_BETWEEN/_TRAILING delays)。疊加 v2.18.0 (#76) Staleness Detection — wrapper writes runtime state file on spawn (pid/started_at/version_at_spawn);新增 hooks/session-start.sh,session 啟動時偵測 plugin.json 與 in-memory binary 版本 drift,SIGTERM(+5s grace,SIGKILL fallback)stale PID 讓 Claude Code 重 spawn 取新 binary,徹底解決「plugin update 後當下 session 仍跑舊 binary」silent staleness 問題。疊加 v2.17.0 (#49) Workspace Layout Detection;v2.16.1 ship 鏈到 binary v2.7.1 (#72 base64 fix + #69 SQLite fast-path stderr logging + #66 .partial.emlx attachment fix);v2.16.0 config schema .md → .yaml (#47);v2.15.0 inline cid: 圖片保留 (#45);v2.14.0 dedup_strategy opt-in (#18);v2.13.0 markdown template 簡化 (#17);v2.12.0 zero-arg mode (#13/#21);v2.11.0 binary v2.7.0 multi-attachment race fix (#60);v2.6.0 marathon (8 PRs);v2.9.0 task enforcement;v2.8.0 namespace;v2.7.0 NSQL confirmation。", + "version": "2.19.0", + "binary_version": "2.8.0", + "description": "Apple Mail MCP server — 48 tools, reply-as-draft + IDD task enforcement + NSQL confirmation + .claude/.mail/ namespace (shell v2.19.0, binary v2.8.0)。Shell v2.19.0 ships binary v2.8.0: (#19) `sanitize_links` opt-in URL scheme allowlist for markdown mode (defends against `[click](javascript:alert('xss'))` and `data:`/`file:`/`vbscript:` XSS vectors with closed allowlist `{http, https, mailto, tel}`; default `false` preserves backwards compat) + (#85) formal `openspec/specs/message-composition/spec.md` Requirement+Scenarios codifying the allowlist contract + builder-layer wiring contract tests pinning `sanitizeLinks` forwarding across `buildComposeEmailScript/buildCreateDraftScript/buildReplyEmailScript/buildForwardEmailScript` + cluster A hygiene (#20 dead spec scenario delete / count-free CHANGELOG / `assertOrdered` helper, #21 reply/forward AppleScript-html-denial docs, #25 `list_attachments_batch` SQLite+.emlx cross-validation parity with single-message handler, #27 `EmlxParser.attachmentNames` <200ms latency budget regression test, #32 `attachmentNames`↔`saveAttachment` matcher parity invariant test across 4 fixtures) + (#73) `extractHTMLBody` base64+UTF-8 quoted-printable decoding fixes。疊加 v2.18.1 (#71 get_email_metadata SQLite fallback parity + cluster #61-64 attachmentFragment hardening);v2.18.0 (#76) Staleness Detection — wrapper writes runtime state file on spawn (pid/started_at/version_at_spawn);新增 hooks/session-start.sh,session 啟動時偵測 plugin.json 與 in-memory binary 版本 drift,SIGTERM(+5s grace,SIGKILL fallback)stale PID 讓 Claude Code 重 spawn 取新 binary,徹底解決「plugin update 後當下 session 仍跑舊 binary」silent staleness 問題。疊加 v2.17.0 (#49) Workspace Layout Detection;v2.16.1 ship 鏈到 binary v2.7.1 (#72 base64 fix + #69 SQLite fast-path stderr logging + #66 .partial.emlx attachment fix);v2.16.0 config schema .md → .yaml (#47);v2.15.0 inline cid: 圖片保留 (#45);v2.14.0 dedup_strategy opt-in (#18);v2.13.0 markdown template 簡化 (#17);v2.12.0 zero-arg mode (#13/#21);v2.11.0 binary v2.7.0 multi-attachment race fix (#60);v2.6.0 marathon (8 PRs);v2.9.0 task enforcement;v2.8.0 namespace;v2.7.0 NSQL confirmation。", "author": { "name": "Che Cheng" }, diff --git a/plugins/che-apple-mail-mcp/README.md b/plugins/che-apple-mail-mcp/README.md index 9f98158..6d4d408 100644 --- a/plugins/che-apple-mail-mcp/README.md +++ b/plugins/che-apple-mail-mcp/README.md @@ -199,6 +199,7 @@ https://github.com/kiki830621/che-apple-mail-mcp ## Version History +- **v2.19.0 shell + binary v2.8.0**(2026-05-11)— **`sanitize_links` security feature + formal spec coverage** ([release notes](https://github.com/PsychQuant/che-apple-mail-mcp/releases/tag/v2.8.0)). Opt-in URL scheme allowlist for markdown mode ([#19](https://github.com/PsychQuant/che-apple-mail-mcp/issues/19)) defends against `[click](javascript:alert('xss'))` and `data:`/`file:`/`vbscript:` URLs via closed allowlist `{http, https, mailto, tel}`; default `false` preserves backwards compat. Formal `openspec/specs/message-composition/spec.md` Requirement+Scenarios codifying the contract + builder-layer wiring contract tests ([#85](https://github.com/PsychQuant/che-apple-mail-mcp/issues/85)) pinning `sanitizeLinks` forwarding across the 4 script-builder functions. Cluster A hygiene: dead spec scenario delete + count-free CHANGELOG + `assertOrdered` helper ([#20](https://github.com/PsychQuant/che-apple-mail-mcp/issues/20)); reply/forward AppleScript-html-denial docs ([#21](https://github.com/PsychQuant/che-apple-mail-mcp/issues/21)); `list_attachments_batch` SQLite+.emlx cross-validation parity ([#25](https://github.com/PsychQuant/che-apple-mail-mcp/issues/25)); `attachmentNames` <200ms latency budget test ([#27](https://github.com/PsychQuant/che-apple-mail-mcp/issues/27)) + parity invariant ([#32](https://github.com/PsychQuant/che-apple-mail-mcp/issues/32)). `extractHTMLBody` base64+UTF-8-QP decoding fixes ([#73](https://github.com/PsychQuant/che-apple-mail-mcp/issues/73)). 48 tools (was 47). swift test → 313/0/8 (was 309). - **v2.10.3 shell + binary v2.6.0**(2026-05-03)— **Marathon release: 16 issues across 8 PRs** ([release notes](https://github.com/PsychQuant/che-apple-mail-mcp/releases/tag/v2.6.0))。安全強化:id 注入防護 ([#50](https://github.com/PsychQuant/che-apple-mail-mcp/issues/50))、附件路徑 deny-list + opt-in `MAIL_MCP_ATTACHMENT_ROOTS` allow-list ([#38](https://github.com/PsychQuant/che-apple-mail-mcp/issues/38))、email 地址驗證 ([#41](https://github.com/PsychQuant/che-apple-mail-mcp/issues/41))、type-strict handler params ([#35](https://github.com/PsychQuant/che-apple-mail-mcp/issues/35))、新 SECURITY.md ([#48](https://github.com/PsychQuant/che-apple-mail-mcp/issues/48))。Bug fix:`forward_email` plain mode 也嵌入 quoted original ([#44](https://github.com/PsychQuant/che-apple-mail-mcp/issues/44))。Quality:cc_additional case-insensitive dedup ([#34](https://github.com/PsychQuant/che-apple-mail-mcp/issues/34))、schema test type assertions ([#42](https://github.com/PsychQuant/che-apple-mail-mcp/issues/42))、indent cleanup ([#39](https://github.com/PsychQuant/che-apple-mail-mcp/issues/39))、README updates ([#36](https://github.com/PsychQuant/che-apple-mail-mcp/issues/36))、large-thread script size tests ([#49](https://github.com/PsychQuant/che-apple-mail-mcp/issues/49))。Tests:gated integration tests for reply runtime ([#37 + #45](https://github.com/PsychQuant/che-apple-mail-mcp/issues/37))。Smoke matrix templates ([#46 + #47](https://github.com/PsychQuant/che-apple-mail-mcp/issues/46))。**279 tests pass** (+45 from 234)。 - **v2.10.2 shell + binary v2.5.0**(2026-05-03)— **Reply quoted-original fix** ([issue #43](https://github.com/PsychQuant/che-apple-mail-mcp/issues/43))。`reply_email` plain mode 終於把 quoted original 嵌入 draft body — 自 `b8a4a89`(initial release)以來每次 plain reply 都靜默 drop 掉 quoted original,因為 AppleScript `& content` 對 freshly-created outgoing message 讀為空。改用 Swift-side `composeReplyPlainText` helper 預先 fetch + RFC 3676 `> ` prefix 組合 quoted body。Round-1 hardening 涵蓋 CRLF/CR normalization、trailing newline trim、空行 stuffing(`>` 不含 trailing space)、pre-fetch 失敗 graceful degrade。**Wire-output 行為改變**:plain reply body 從 `` 變成 `\n\n> `。HTML branch 不變(原本就走對的 architecture)。234 tests pass。Follow-ups #44–#50 已開。 - **v2.10.1 shell + binary v2.4.1**(2026-05-02)— **Save-as-draft popup + path validation fix** ([issue #33 verify findings A+B](https://github.com/PsychQuant/che-apple-mail-mcp/issues/33))。`reply_email` `save_as_draft=true` 不再彈出 Mail.app 視窗(windowClause 改成條件式 with/without opening window),`replyEmail` 加 `validateFilePaths` 鏡像 composeEmail / createDraft 行為。 diff --git a/plugins/che-apple-mail-mcp/commands/archive-mail.md b/plugins/che-apple-mail-mcp/commands/archive-mail.md index 9d565b7..2a1cadf 100644 --- a/plugins/che-apple-mail-mcp/commands/archive-mail.md +++ b/plugins/che-apple-mail-mcp/commands/archive-mail.md @@ -332,16 +332,24 @@ mkdir -p "${output_dir}" # archive markdown 目的地(不變) 當 `${output_dir}` 下有 symlinked subdirectory(典型情境:transitioned-project pattern,例如 chchen_lab 把 `email/application/` symlink 到 `applications/completed/.../emails/`),掃描其下 markdown 的 YAML frontmatter,把 `message_id:` 值併入 in-memory dedup set。**讀取 only,從不寫入 symlink target**。 ```bash +# === INVARIANT: this block is READ-ONLY w.r.t. $symlink_dir === +# Allowed: find / head / awk / cat / grep +# FORBIDDEN: mv / rm / cp / > / >> / tee / chmod (against $symlink_dir or its target) +# If you need to write, do it OUTSIDE this block. +# (v2.17.x #56 — invariant marker; future hardening: bash trap-based check or pre-commit scan, +# both deferred. This comment is the load-bearing contract for future maintainers.) + # Only run when dedup_strategy uses index (skip if last_archived only) if [ "$DEDUP_STRATEGY" = "index" ] || [ "$DEDUP_STRATEGY" = "both" ]; then # Find symlinked subdirectories in output_dir (1 level deep) EXTENDED_COUNT=0 EXTENDED_SOURCES=() - while IFS= read -r symlink_dir; do + # Null-safe: -print0 + read -d '' handles filenames containing newlines / unusual chars (#57) + while IFS= read -r -d '' symlink_dir; do [ -z "$symlink_dir" ] && continue # Bound search depth + use -P to NOT follow symlinks recursively (we follow once into the symlinked dir, then stay) ENTRIES_THIS_DIR=0 - while IFS= read -r mdfile; do + while IFS= read -r -d '' mdfile; do [ -z "$mdfile" ] && continue # Read just the YAML frontmatter (first ~30 lines is generous) mid=$(head -30 "$mdfile" 2>/dev/null | awk '/^message_id:[ \t]*/{sub(/^message_id:[ \t]*"?/,"");sub(/"?$/,"");print;exit}') @@ -351,13 +359,13 @@ if [ "$DEDUP_STRATEGY" = "index" ] || [ "$DEDUP_STRATEGY" = "both" ]; then EXTENDED_DEDUP_IDS+=("$mid") ENTRIES_THIS_DIR=$((ENTRIES_THIS_DIR + 1)) fi - done < <(find -P "$symlink_dir/" -maxdepth 2 -name "*.md" -type f 2>/dev/null) + done < <(find -P "$symlink_dir/" -maxdepth 2 -name "*.md" -type f -print0 2>/dev/null) if [ "$ENTRIES_THIS_DIR" -gt 0 ]; then EXTENDED_COUNT=$((EXTENDED_COUNT + ENTRIES_THIS_DIR)) EXTENDED_SOURCES+=("$symlink_dir ($ENTRIES_THIS_DIR entries)") fi - done < <(find -P "${output_dir}" -maxdepth 1 -type l 2>/dev/null) + done < <(find -P "${output_dir}" -maxdepth 1 -type l -print0 2>/dev/null) if [ "$EXTENDED_COUNT" -gt 0 ]; then echo "🔗 Extended dedup with $EXTENDED_COUNT entries from sibling archives:" diff --git a/plugins/plugin-tools/skills/plugin-update/SKILL.md b/plugins/plugin-tools/skills/plugin-update/SKILL.md index f172ba4..3f6d9c7 100644 --- a/plugins/plugin-tools/skills/plugin-update/SKILL.md +++ b/plugins/plugin-tools/skills/plugin-update/SKILL.md @@ -261,6 +261,10 @@ if [ -z "${TARGET_PLUGIN:-}" ]; then fi # 收集 unpushed commits touch 到的 plugin 名(去重) +# Note (#70): 以 inline subshell `$(...)` capture 到 process-local 變數, +# **不**經過 /tmp tempfile,故無 TOCTOU / shared-/tmp race condition。 +# 若未來重構需要持久化 list,改用 `mktemp -t plugin-update-touched.XXXXXX` +# + `trap "rm -f \$TMPFILE" EXIT` 而**禁用**hardcoded /tmp/touched-plugins.txt。 TOUCHED=$(git log --name-only --pretty=format: "$UPSTREAM"..HEAD \ | grep '^plugins/' | cut -d/ -f2 | sort -u) TOUCHED_COUNT=$(echo -n "$TOUCHED" | grep -c . || true) @@ -407,6 +411,12 @@ options: | `plugin-deploy` Step 2.5 | 偶爾(發版時)| **BLOCK** | Release 沒 binary = 新使用者裝 plugin 就壞,不能放過 | | `plugin-update` Phase 1.5 | 頻繁(日常同步)| **ASK + AUTO-SYNC** | 開發者通常想要一次更新完,但要尊重「只改 shell 不動 binary」的情境 | +> **Default-option policy alignment with Phase 0.5**(audit per #68): +> +> Phase 0.5 rule:**`abort` is the default for any state with multiple sensible actions;active default is reserved for unambiguous happy-path / reversible action / frequent dev flow**(per Phase 0.5 Step 3 explicit rule)。 +> +> Phase 1.5 default 「順便更新」**符合 exception clause** — binary-out-of-sync 在 daily dev flow 是 unambiguous happy path(開發者通常確實想要 binary + shell 一起更新),而且 binary install 是 reversible(可重跑 mcp-deploy / cli-upgrade 換版本)。本 phase 跟 Phase 0.5 的 abort-default 不衝突,各自服務不同 lifecycle moment(release-time push 不可逆 vs dev-time binary sync 可逆)。 + ### Step 5: 執行 auto-sync(若使用者選擇) **MCP 情境**: @@ -680,6 +690,17 @@ options: | `plugin-update` Phase 2.5 | 頻繁(日常同步)| **ASK** | 有時純修 typo / hook / internal refactor,不需要動 README | | `plugin-deploy` Step 2 | 偶爾(發版時)| **列入 checklist 並 offer 修復** | 正式發布時使用者第一眼看 README,stale 就是差的第一印象 | +> **Default-option policy alignment with Phase 0.5**(audit per #68): +> +> Phase 0.5 rule(canonical 3-clause):**active default 保留給 unambiguous happy-path / reversible action / frequent dev flow**(per Phase 0.5 Step 3 explicit rule)。 +> +> Phase 2.5 default 「更新 README」**符合 exception clause(2/3 criteria + substitution)**: +> - **unambiguous happy-path** ✓ — 偵測到 README 與當前 plugin.json version 不同步,起草更新是 unambiguous 的方向(scope 是「重新生成 stale 段落」,不是 design choice) +> - **reversible action** ✓ — 該選項不直接 mutate README,而是 propose diff(read CHANGELOG + git log → 起草 → user 審閱後才 commit);User-confirmation gate 還在,只是把 draft 起草的 friction 從 user 轉到 skill +> - **frequent dev flow** ✗ → **substituted with non-destructive** — README update 並非 daily dev flow(只在 substantive change 後 fire),故 frequent-dev criterion 不適用;以 non-destructive(propose-only,不 mutate disk)替代,作為 risk-profile 等價的 fallback criterion +> +> 對比 Phase 0.5 abort-default 的 `git push` 情境:push to public marketplace 是 irreversible(回滾需要 force-push,在 public branch 是 bad day),所以即使用戶在 keyboard 也不該 default push。各 phase 的 default 反映其 action 的 risk profile。 + --- ## Phase 3: 同步 Marketplace Cache