From 3b01d3f5c460eb4e5399cf80fb871e2b5fafb96c Mon Sep 17 00:00:00 2001 From: che cheng Date: Sun, 10 May 2026 21:11:31 +0800 Subject: [PATCH 1/6] refactor(archive-mail): add read-only invariant marker comment to Step 2.1 (Refs #56) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per /idd-verify #49 DA new finding: Step 2.1 sibling-archive dedup extension claims 'read-only' but enforcement is honor-system (current bash uses only find/head/awk; future maintainer adding mv/rm wouldn't be caught by any guard). Option A per issue body — invariant marker comment at top of bash block: - Lists allowed ops (find / head / awk / cat / grep) - Lists forbidden ops (mv / rm / cp / > / >> / tee / chmod) - Self-documenting, load-bearing contract for future maintainers Option B (bash trap-based check) + Option C (pre-commit hook scanning) deferred — requires real bash impl file or repo-level tooling, both out of scope for this docs-only fix. Refs #56 --- plugins/che-apple-mail-mcp/commands/archive-mail.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/che-apple-mail-mcp/commands/archive-mail.md b/plugins/che-apple-mail-mcp/commands/archive-mail.md index 9d565b7..9d5e401 100644 --- a/plugins/che-apple-mail-mcp/commands/archive-mail.md +++ b/plugins/che-apple-mail-mcp/commands/archive-mail.md @@ -332,6 +332,13 @@ 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) From 5b610cd5ead6c2c06f9b9f13a7b798f662d46914 Mon Sep 17 00:00:00 2001 From: che cheng Date: Sun, 10 May 2026 21:11:58 +0800 Subject: [PATCH 2/6] fix(archive-mail): null-safe find/while loops in Step 2.1 (Refs #57) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per /idd-verify #49 security review LOW#1: Step 2.1 sibling-archive dedup extension used 'find ... | while IFS= read -r' which splits on newlines. Filenames containing newlines (POSIX-legal, rare in practice) would silently mis-parse — partial filename gets processed. Apply standard null-safe pattern to BOTH find invocations: - Outer: find -P -type l -print0 + while read -r -d '' symlink_dir - Inner: find -P -name '*.md' -print0 + while read -r -d '' mdfile Both -print0 / read -d '' must change together — partial conversion of one without the other would mis-parse the pipeline. Realistic exposure low (Apple Mail subject lines don't contain raw newlines), but trivial defensive hygiene fix. Refs #57 --- plugins/che-apple-mail-mcp/commands/archive-mail.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/che-apple-mail-mcp/commands/archive-mail.md b/plugins/che-apple-mail-mcp/commands/archive-mail.md index 9d5e401..2a1cadf 100644 --- a/plugins/che-apple-mail-mcp/commands/archive-mail.md +++ b/plugins/che-apple-mail-mcp/commands/archive-mail.md @@ -344,11 +344,12 @@ 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}') @@ -358,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:" From f25621eb3b9f353af24ea22aae4a716eb71ef9d2 Mon Sep 17 00:00:00 2001 From: che cheng Date: Sun, 10 May 2026 21:13:11 +0800 Subject: [PATCH 3/6] docs(plugin-update): clarify Phase 1.5/2.5 default-option policy alignment with Phase 0.5 (Refs #68) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per /idd-implement #60 Step 5.7 sister bug sweep: audit found Phase 1.5 (action default '順便更新') + Phase 2.5 (action default '更新 README') appear to diverge from Phase 0.5's abort-default safety policy. Audit conclusion: NOT a divergence — they correctly apply Phase 0.5 rule's exception clause: 'active default is reserved for unambiguous happy-path / reversible action / frequent dev flow' Phase 1.5: binary-out-of-sync in daily dev flow IS unambiguous happy path. Binary install is reversible (mcp-deploy / cli-upgrade can reset). Frequent. → action default justified. Phase 2.5: '更新 README' option doesn't mutate; it proposes diff (read CHANGELOG + git log → draft → user reviews → commit). User- confirmation gate still present. Reversible draft. → action default justified. Contrast with Phase 0.5 (release-time push to public marketplace): push is IRREVERSIBLE (force-push to public is bad day) → abort default warranted regardless of user-in-session. Each phase's default reflects its action's risk profile. No behavior change — just documentation that the deliberate divergence IS the correct application of Phase 0.5 rule, not a violation. Refs #68 --- plugins/plugin-tools/skills/plugin-update/SKILL.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/plugins/plugin-tools/skills/plugin-update/SKILL.md b/plugins/plugin-tools/skills/plugin-update/SKILL.md index f172ba4..d6ce7d1 100644 --- a/plugins/plugin-tools/skills/plugin-update/SKILL.md +++ b/plugins/plugin-tools/skills/plugin-update/SKILL.md @@ -407,6 +407,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 +686,14 @@ 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:**active default 保留給 unambiguous happy-path / reversible / non-destructive action**。 +> +> Phase 2.5 default 「更新 README」**符合 exception clause** — 該選項不直接 mutate README,而是 propose diff(read CHANGELOG + git log → 起草 → user 審閱後才 commit)。User-confirmation gate 還在,只是把 draft 起草的 friction 從 user 轉到 skill。屬於 reversible / non-destructive 操作。 +> +> 對比 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 From ac3c673dd414f0346f35726203f056f9e0b6a95a Mon Sep 17 00:00:00 2001 From: che cheng Date: Sun, 10 May 2026 21:17:04 +0800 Subject: [PATCH 4/6] docs(plugin-update): document tempfile-avoidance invariant in Phase 0.5 Step 5 (Refs #70) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per /idd-verify #60 security review (LOW, DA→LOW): finding L233 flagged '/tmp/touched-plugins.txt race / TOCTOU on shared /tmp'. Audit found the flagged tempfile DOES NOT EXIST in current implementation — Phase 0.5 Step 5 already uses inline subshell: TOUCHED=$(git log --name-only --pretty=format: ... | sort -u) Inline $(...) substitution captures to a process-local variable, never touches /tmp, hence no TOCTOU window and no race. The verify finding was a false positive imagining a tempfile that was never written. No behavior change. Add load-bearing comment documenting: 1. Why current code is already safe (inline subshell, not tempfile) 2. If future refactor *does* need persistence, use mktemp + trap pattern (the '#70 expected behavior' from the issue) 3. Hardcoded /tmp/touched-plugins.txt is FORBIDDEN — it would re-introduce the false-positive surface and the actual race window Comment forestalls future audits from re-flagging the same false positive, and gives future maintainers explicit guidance if refactoring direction changes. Refs #70 --- plugins/plugin-tools/skills/plugin-update/SKILL.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/plugin-tools/skills/plugin-update/SKILL.md b/plugins/plugin-tools/skills/plugin-update/SKILL.md index d6ce7d1..906c0ff 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) From 4c1b1022adae68f8b49cb2793cd778fe51ccad11 Mon Sep 17 00:00:00 2001 From: che cheng Date: Sun, 10 May 2026 21:40:10 +0800 Subject: [PATCH 5/6] =?UTF-8?q?fix(plugin-update):=20make=20Phase=202.5=20?= =?UTF-8?q?audit=20note=20quote=20canonical=203-clause=20exception=20+=20e?= =?UTF-8?q?xplicitly=20justify=20frequent-dev=20=E2=86=92=20non-destructiv?= =?UTF-8?q?e=20substitution=20(Refs=20#68)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per /idd-verify --pr 72 codex MEDIUM finding (single blocking): Phase 2.5 audit blockquote silently substituted 'non-destructive' for 'frequent dev' in the 3-criterion exception clause, while Phase 1.5 quoted all 3 verbatim. Codex (gpt-5.5 xhigh) flagged the inconsistency: Phase 1.5 note: 'unambiguous happy-path / reversible / frequent dev flow' ✓ Phase 2.5 note: 'unambiguous happy-path / reversible / non-destructive' ✗ ^^^^^^^^^^^^^^^^ silent substitution Substitution is defensible (README updates aren't a daily dev flow — they fire only after substantive change), but the audit must EXPLAIN the substitution, not silently reword the canonical rule. Otherwise future readers comparing the two notes would see two different rule statements and conclude the policy is incoherent. Fix: rewrite Phase 2.5 note as 3-bullet itemize listing each criterion: - unambiguous happy-path ✓ (with explanation) - reversible action ✓ (with explanation — propose diff, not mutate) - frequent dev flow ✗ → substituted with non-destructive (with reason) Phase 1.5 note unchanged (all 3 criteria fit verbatim). Refs #68 --- plugins/plugin-tools/skills/plugin-update/SKILL.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/plugin-tools/skills/plugin-update/SKILL.md b/plugins/plugin-tools/skills/plugin-update/SKILL.md index 906c0ff..3f6d9c7 100644 --- a/plugins/plugin-tools/skills/plugin-update/SKILL.md +++ b/plugins/plugin-tools/skills/plugin-update/SKILL.md @@ -692,9 +692,12 @@ options: > **Default-option policy alignment with Phase 0.5**(audit per #68): > -> Phase 0.5 rule:**active default 保留給 unambiguous happy-path / reversible / non-destructive action**。 +> 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** — 該選項不直接 mutate README,而是 propose diff(read CHANGELOG + git log → 起草 → user 審閱後才 commit)。User-confirmation gate 還在,只是把 draft 起草的 friction 從 user 轉到 skill。屬於 reversible / non-destructive 操作。 +> 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。 From ad2c3e8c693d6ff1267cbc330cd136aff9eb9325 Mon Sep 17 00:00:00 2001 From: che cheng Date: Mon, 11 May 2026 15:35:29 +0800 Subject: [PATCH 6/6] sync: che-apple-mail-mcp shell v2.19.0 + binary v2.8.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump plugin shell v2.18.1 → v2.19.0 to ship binary v2.8.0 (just released at PsychQuant/che-apple-mail-mcp#85, tagged v2.8.0). Binary v2.8.0 contents: - (#19) sanitize_links opt-in URL scheme allowlist for markdown mode - (#85) formal spec.md Requirement+Scenarios + builder-layer wiring contract tests pinning sanitizeLinks forwarding - (#20/#21/#25/#27/#32) cluster A hygiene + invariant tests - (#73) extractHTMLBody base64+UTF-8-QP decoding fixes - 47 → 48 tools (sanitize_links param surface adds new schema field) - swift test 309 → 313 passing / 0 failures / 8 skipped Files synced: - plugins/che-apple-mail-mcp/.claude-plugin/plugin.json (version + binary_version + description) - .claude-plugin/marketplace.json (same fields mirrored from plugin.json) - plugins/che-apple-mail-mcp/README.md (Version History entry added) Refs PsychQuant/che-apple-mail-mcp#19 #85 #73 --- .claude-plugin/marketplace.json | 7 ++++--- plugins/che-apple-mail-mcp/.claude-plugin/plugin.json | 6 +++--- plugins/che-apple-mail-mcp/README.md | 1 + 3 files changed, 8 insertions(+), 6 deletions(-) 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 行為。