diff --git a/plugins/issue-driven-dev/rules/tagging-collaborators.md b/plugins/issue-driven-dev/rules/tagging-collaborators.md index bdf788f..10f6d87 100644 --- a/plugins/issue-driven-dev/rules/tagging-collaborators.md +++ b/plugins/issue-driven-dev/rules/tagging-collaborators.md @@ -111,7 +111,7 @@ User picks from the **actual list**. The "Other" free-text option is fine for ge - **Unconditional scan(v2.92.0+, #117)**:這個 pre-post 掃描**不是** intent-gated — 即使沒有 `--mention` flag、沒有 tagging 意圖,AI-generated body 中**附帶**的 `@xxx` token(內部 codename 如 `@codex`、引用對話原文、動態值如 `@assignee`)一樣會通知同名真實 user。每個 raw token 二擇一: 1. **不是 mention** → backtick-escape 成 `` `@xxx` ``(inline code 對 GitHub notification 惰性)或放進 code fence 2. **是 mention** → 走完 5-step 協定後,經 `scripts/gh-egress.sh` 派送時帶 `--mention-attested `(涵蓋**每一個**意圖 mention;部分涵蓋一樣 refuse) -- **機械 backstop**:`scripts/gh-egress.sh` 的 mention net(#117)會在派送前剝除 fence/inline-code 後掃 `@login` token,未被 `--mention-attested` 涵蓋者 refuse(exit 4)。此網對 email-like `user@host` 不誤觸(prefix guard)。未接線 gh-egress 的 skill 依本 rule 自律(prose 層),機械覆蓋隨 gh-egress rollout 生效。 +- **機械 backstop**:`scripts/gh-egress.sh` 的 mention net(#117)會在派送前剝除 fence/inline-code 後掃 `@login` token,未被 `--mention-attested` 涵蓋者 refuse(exit 11 — #227 refusal 碼帶 ≥10,<10 為 gh 原生碼透傳)。此網對 email-like `user@host` 不誤觸(prefix guard)。未接線 gh-egress 的 skill 依本 rule 自律(prose 層),機械覆蓋隨 gh-egress rollout 生效。 ```bash # Verification step diff --git a/plugins/issue-driven-dev/scripts/gh-egress.sh b/plugins/issue-driven-dev/scripts/gh-egress.sh index 583a18b..73b62db 100755 --- a/plugins/issue-driven-dev/scripts/gh-egress.sh +++ b/plugins/issue-driven-dev/scripts/gh-egress.sh @@ -57,10 +57,17 @@ # byte-for-byte the same as calling `gh issue ...` directly (backward # compat: callers that capture `URL=$(... )` are unaffected). # -# EXIT CODES -# 0 dispatched (exec gh) 2 usage error (bad/missing verb / malformed args) -# 5 unscannable --body-file (not a readable regular file, #203 item 3) -# 3 attestation missing/invalid 4 mechanical net hit (refused) +# EXIT CODES — wrapper-origin codes live in a dedicated refusal band >=10 (#227). +# INVARIANT: this wrapper never exits with its own code below 10; any exit <10 +# observed by a caller is gh's OWN code flowing through the final `exec` (so an +# unattended caller can mechanically split "gate refusal -> fix content/args" +# from "gh failure -> fix auth/network" on $? alone). +# 0 dispatched (exec gh -- gh's exit codes flow through from here, all <10) +# 10 privacy net hit (absolute /Users/ path / verbatim ~/.claude.json content) +# 11 mention net hit (unattested @login token / entity-encoded @ form) +# 12 unscannable --body-file (not a readable regular file, #203 item 3) +# 13 attestation missing/invalid (--scrub-attested absent or bad level) +# 14 usage error (bad/missing verb, malformed/split-token args, flag missing its value) # # TEST OVERRIDES (test-only; never set in production) # IDD_GH_BIN gh binary to exec (default: gh) @@ -76,8 +83,8 @@ usage() { VERB="${1:-}" case "$VERB" in create|comment|edit) shift ;; - "") echo "✗ gh-egress: missing egress verb." >&2; usage; exit 2 ;; - *) echo "✗ gh-egress: unknown egress verb '$VERB' (only create|comment|edit route through this gate)." >&2; usage; exit 2 ;; + "") echo "✗ gh-egress: missing egress verb." >&2; usage; exit 14 ;; + *) echo "✗ gh-egress: unknown egress verb '$VERB' (only create|comment|edit route through this gate)." >&2; usage; exit 14 ;; esac # --- parse: pull out --scrub-attested, forward everything else verbatim ------- @@ -89,7 +96,7 @@ require_scannable_bodyfile() { echo "✗ gh-egress: REFUSED — --body-file '$1' is not a readable regular file." >&2 echo " stdin ('-'), FIFOs and process substitutions cannot be scanned without consuming the stream." >&2 echo " Write the body to a regular file first, then re-dispatch." >&2 - exit 5 + exit 12 fi } @@ -107,11 +114,11 @@ while [ $# -gt 0 ]; do # Same malformed-shape guard as --scrub-attested (#203 item 6 / #117). if [ -n "$next_is" ]; then echo "✗ gh-egress: malformed args — '--mention-attested' found where a value for --body/--title/--body-file was expected." >&2 - exit 2 + exit 14 fi case "$arg" in --mention-attested) - [ $# -ge 2 ] || { echo "✗ gh-egress: --mention-attested needs a value." >&2; exit 3; } + [ $# -ge 2 ] || { echo "✗ gh-egress: --mention-attested needs a value." >&2; exit 14; } MENTION_ATTESTED="$2"; shift 2; continue ;; *) MENTION_ATTESTED="${arg#--mention-attested=}"; shift; continue ;; @@ -123,11 +130,11 @@ while [ $# -gt 0 ]; do # prose would silently escape the scan. Refuse as a usage error. if [ -n "$next_is" ]; then echo "✗ gh-egress: malformed args — '--scrub-attested' found where a value for --body/--title/--body-file was expected (split-token attestation)." >&2 - exit 2 + exit 14 fi case "$arg" in --scrub-attested) - [ $# -ge 2 ] || { echo "✗ gh-egress: --scrub-attested needs a value." >&2; exit 3; } + [ $# -ge 2 ] || { echo "✗ gh-egress: --scrub-attested needs a value." >&2; exit 14; } ATTESTED="$2"; shift 2; continue ;; *) ATTESTED="${arg#--scrub-attested=}"; shift; continue ;; @@ -169,9 +176,9 @@ case "$ATTESTED" in "") echo "✗ gh-egress: REFUSED — privacy self-review attestation missing." >&2 echo " Run the privacy-scrubbing self-review (rules/privacy-scrubbing.md), then pass" >&2 echo " --scrub-attested (the resolved repo-visibility strictness)." >&2 - exit 3 ;; + exit 13 ;; *) echo "✗ gh-egress: REFUSED — invalid attestation level '$ATTESTED' (expected enforce|warn|light)." >&2 - exit 3 ;; + exit 13 ;; esac # --- (b) mechanical last-resort net (3 zero-tolerance mechanical items) ------- @@ -185,7 +192,7 @@ for p in "${SCAN_PARTS[@]:-}"; do SCAN+="$p"$'\n'; done net_refuse() { echo "✗ gh-egress: REFUSED — mechanical net caught $1." >&2 echo " Zero-tolerance literal leak (belt-and-suspenders backstop; the LLM self-review normally catches this)." >&2 echo " Redact it (e.g. /Users/ → ~, drop the ~/.claude.json excerpt), then re-dispatch." >&2 - exit 4; } + exit 10; } # 1. absolute macOS home path /Users/ — require a real name char right # after the slash so the /Users/ placeholder (angle bracket) and a bare @@ -200,26 +207,62 @@ fi # this gate itself, e.g. #202/#203). # A project-path string copied verbatim out of the user's actual # ~/.claude.json `projects` object (the "project basename leaks local folder -# structure" threat). When jq is available the extraction is scoped to the -# projects object so PUBLIC tool paths (mcpServers[].command etc.) are not -# false-flagged (#203 item 2); without jq, fall back to the original -# whole-file wide net (fail-closed: over-refusing beats leaking). +# structure" threat), or a path-shaped value under a sensitive key name +# (mcpServers[].env secret files etc. — #225 taxonomy). Extraction is ONE +# python3 parser (#225) so every machine scans the same set; PUBLIC tool +# paths (mcpServers[].command etc.) are not false-flagged (#203 item 2). # ${HOME:-} guard: both IDD_CLAUDE_JSON and HOME unset must not crash under # set -u — the probe just skips (#203 item 4). +# #225 unified scan: ONE python3 parser replaces the old jq/no-jq dual path +# whose results diverged per machine (jq: projects-only; no-jq: whole-file +# wide — a secret path under mcpServers[].env dispatched on one machine and +# refused on the other). Taxonomy: projects keys ∪ path-shaped string values +# whose key (or any ancestor key) matches the tight sensitive-name set +# (key|token|secret|credential|password|auth|env). Public tool paths +# (mcpServers[].command / args) stay un-flagged (#203 item 2 preserved). +# python3 absent OR parse failure → fail CLOSED to the bash-only whole-file +# wide net (over-refusing beats leaking, #203 verify sec-2) — the degraded +# path only ever over-matches, never under-matches the unified set. CJSON="${IDD_CLAUDE_JSON:-${HOME:-}/.claude.json}" if [ -n "$SCAN" ] && [ -r "$CJSON" ]; then - JQ_OK=0 - if command -v jq >/dev/null 2>&1; then - if KEYS_RAW="$(jq -r '(.projects // {}) | keys[]' "$CJSON" 2>/dev/null)"; then - JQ_OK=1 + PY_OK=0 + if command -v python3 >/dev/null 2>&1; then + if KEYS_RAW="$(python3 - "$CJSON" <<'PYEOF' 2>/dev/null +import json, re, sys +try: + cfg = json.load(open(sys.argv[1])) + if not isinstance(cfg, dict): + raise ValueError("top-level not an object") +except Exception: + sys.exit(3) # parse failure -> bash falls CLOSED to the wide net +SENS = re.compile(r"(?i)(key|token|secret|credential|password|auth|env)") +out = set() +proj = cfg.get("projects") +if isinstance(proj, dict): + out.update(k for k in proj if isinstance(k, str)) +def walk(node, sens): + if isinstance(node, dict): + for k, v in node.items(): + walk(v, sens or bool(SENS.search(str(k)))) + elif isinstance(node, list): + for v in node: + walk(v, sens) + elif isinstance(node, str) and sens: + out.add(node) +walk({k: v for k, v in cfg.items() if k != "projects"}, False) +for s_ in sorted(out): + print(s_) +PYEOF +)"; then + PY_OK=1 KEYS="$(printf '%s\n' "$KEYS_RAW" \ | grep -E '^/.{11,}$' \ | grep -E '/[^/]+/' \ | sort -u)" fi fi - if [ "$JQ_OK" -eq 0 ]; then - # jq absent OR jq parse failure (malformed config) — fail CLOSED to the + if [ "$PY_OK" -eq 0 ]; then + # python3 absent OR parse failure (malformed config) — fail CLOSED to the # whole-file wide net; a silently disabled net would leak (#203 verify sec-2). KEYS="$(grep -oE '"(/[^"]{11,})"' "$CJSON" 2>/dev/null \ | sed -E 's/^"//; s/"$//' \ @@ -263,7 +306,7 @@ MSCAN="$(printf '%s' "$MBODY" | awk '/^ ? ? ?```/{infence=!infence; next} !infen if printf '%s' "$MSCAN" | grep -qiE '�*64;([a-z0-9-]|&#)|�*40;([a-z0-9-]|&#)|@([a-z0-9-]|&#)'; then echo "✗ gh-egress: REFUSED — entity-encoded @-mention (e.g. @login) in body." >&2 echo " Encoded forms can decode into live mentions on GitHub. Spell it as literal text in backticks instead." >&2 - exit 4 + exit 11 fi UNATTESTED_MENTIONS="" while IFS= read -r login; do @@ -283,7 +326,7 @@ if [ -n "$UNATTESTED_MENTIONS" ]; then echo " - escape non-mention tokens in backticks (\`@name\`) — inert on GitHub, or" >&2 echo " - run the rules/tagging-collaborators.md 5-step protocol, then re-dispatch with" >&2 echo " --mention-attested covering every intended mention." >&2 - exit 4 + exit 11 fi # --- dispatch: byte-for-byte identical to raw `gh issue ...` ----------- diff --git a/plugins/issue-driven-dev/scripts/tests/gh-egress-rollout/test.sh b/plugins/issue-driven-dev/scripts/tests/gh-egress-rollout/test.sh new file mode 100755 index 0000000..010387b --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/gh-egress-rollout/test.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# test.sh — drift-guard for the #226 gh-egress wider rollout (Phase 2 of #202). +# +# Phase 1 wired only idd-issue; every other skill's comment/edit egress went +# raw `gh issue comment|edit` — no attestation gate, no privacy/mention nets, +# leaving #117's headline scenario (comment @mention) mechanically unenforced. +# This suite locks the Phase-2 wiring: each rolled-out SKILL routes its +# executable comment/edit call sites through scripts/gh-egress.sh with a +# --scrub-attested level, and the exact pre-rollout raw call lines are GONE +# (refuted verbatim, so a revert or a copy-paste regression turns RED). +# +# Whitelist (deliberately NOT wired / not egress): +# - `gh issue close` — not a content egress verb (wrapper scope is +# create|comment|edit per #202 D2) +# - `gh api ... PATCH comments` — idd-edit / audit-block PATCH surgery goes +# through gh api (comment-id scoped), tracked +# separately; wrapper wraps `gh issue` verbs +# - prose/table MENTIONS of `gh issue comment` (rules text, rationale) — only +# executable call lines were wired +# +# Usage: bash test.sh (exit 0 = all pass, 1 = any fail) + +set -u + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PLUGIN_ROOT="$HERE/../../.." +SK="$PLUGIN_ROOT/skills" + +HELPERS="$HERE/../../lib/assert-helpers.sh" +[ -f "$HELPERS" ] || { echo "✗ missing $HELPERS — cannot run suite" >&2; exit 1; } +. "$HELPERS" + +# ── every rolled-out skill routes egress through the wrapper ── +for sk in idd-comment idd-diagnose idd-implement idd-verify idd-close idd-update; do + assert_output_grep "$sk: routes egress via gh-egress.sh" "gh-egress.sh" "$SK/$sk/SKILL.md" + assert_output_grep "$sk: carries a scrub attestation" "--scrub-attested" "$SK/$sk/SKILL.md" +done + +# ── the exact pre-rollout raw call lines are gone (verbatim refutes) ── +refute_output_grep "idd-comment: raw body-file comment gone" \ + 'gh issue comment $NUMBER --repo $GITHUB_REPO --body-file /tmp/idd-comment-$$.md' \ + "$SK/idd-comment/SKILL.md" +refute_output_grep "idd-diagnose: raw diagnosis-report comment gone" \ + 'gh issue comment $NUMBER --repo $GITHUB_REPO --body "$DIAGNOSIS_REPORT"' \ + "$SK/idd-diagnose/SKILL.md" +refute_output_grep "idd-implement: raw implementation-plan comment gone" \ + 'gh issue comment $NUMBER --repo $GITHUB_REPO --body "$IMPLEMENTATION_PLAN"' \ + "$SK/idd-implement/SKILL.md" +refute_output_grep "idd-verify: raw merged-findings comment gone" \ + 'gh issue comment $NUMBER --repo $GITHUB_REPO --body "$MERGED_FINDINGS"' \ + "$SK/idd-verify/SKILL.md" +refute_output_grep "idd-close: raw closing-comment capture gone" \ + 'CLOSING_COMMENT_URL=$(gh issue comment $NUMBER --repo $GITHUB_REPO --body "$CLOSING_COMMENT")' \ + "$SK/idd-close/SKILL.md" +refute_output_grep "idd-update: raw body edit gone" \ + 'gh issue edit $NUMBER --repo $GITHUB_REPO --body "$UPDATED_BODY"' \ + "$SK/idd-update/SKILL.md" + +# ── level resolution is cited, not re-invented per skill ── +for sk in idd-comment idd-diagnose idd-implement idd-verify idd-close idd-update; do + assert_output_grep "$sk: cites privacy-scrubbing rule for \$SCRUB_LEVEL" "privacy-scrubbing" "$SK/$sk/SKILL.md" +done + +print_summary "gh-egress-rollout (#226)" diff --git a/plugins/issue-driven-dev/scripts/tests/gh-egress/test.sh b/plugins/issue-driven-dev/scripts/tests/gh-egress/test.sh index fe3fb9a..4c4ebe3 100644 --- a/plugins/issue-driven-dev/scripts/tests/gh-egress/test.sh +++ b/plugins/issue-driven-dev/scripts/tests/gh-egress/test.sh @@ -47,14 +47,14 @@ export FAKE_GH_ARGV="$WORK/argv" # a non-/Users absolute path so the .claude.json content-overlap net can be # proven to fire INDEPENDENTLY of the home-path net. export IDD_CLAUDE_JSON="$WORK/fixture-claude.json" -printf '%s' '{"projects":{"/Users/fixtureuser/secret-lab":{"x":1},"/opt/priv/hidden-proj-xyz":{"y":2}},"mcpServers":{"tool":{"command":"/opt/homebrew/bin/uvx-tool"}}}' \ +printf '%s' '{"projects":{"/Users/fixtureuser/secret-lab":{"x":1},"/opt/priv/hidden-proj-xyz":{"y":2}},"mcpServers":{"tool":{"command":"/opt/homebrew/bin/uvx-tool","env":{"KEY_FILE":"/srv/secret-vault/che-key-abc"}}}}' \ > "$IDD_CLAUDE_JSON" ATT=(--scrub-attested warn) # ── §2.1 attestation enforcement ──────────────────────────────────────────── bash "$SCRIPT" create --repo o/r --title T --body "fix parser in src/main.rs" >/dev/null 2>&1 -assert_exit "attestation absent → refuse dispatch (exit 3)" 3 $? +assert_exit "attestation absent → refuse dispatch (exit 13)" 13 $? OUT="$(bash "$SCRIPT" create --repo o/r --title T --body "fix parser in src/main.rs" "${ATT[@]}" 2>/dev/null)" RC=$? @@ -62,7 +62,7 @@ assert_exit "attestation present + clean → dispatch (exit 0)" 0 "$RC" assert_grep "dispatch stdout is gh output, byte-preserved" "issues/123" "$OUT" bash "$SCRIPT" create --repo o/r --title T --body "clean" --scrub-attested bogus >/dev/null 2>&1 -assert_exit "invalid attestation level → refuse (exit 3)" 3 $? +assert_exit "invalid attestation level → refuse (exit 13)" 13 $? # attestation flag is consumed by the wrapper, never forwarded to gh bash "$SCRIPT" comment 5 --repo o/r --body "hello world" "${ATT[@]}" >/dev/null 2>&1 @@ -76,12 +76,12 @@ assert_grep "gh argv keeps the body value" "hello world" "$ARGV" # literal /Users/ caught even WITH a valid attestation (LLM missed it) bash "$SCRIPT" create --repo o/r --title T \ --body "the script at /Users/alice/proj/run.sh keeps failing" "${ATT[@]}" >/dev/null 2>&1 -assert_exit "literal /Users/alice home path caught (exit 4)" 4 $? +assert_exit "literal /Users/alice home path caught (exit 10)" 10 $? # home path can also hide in the title bash "$SCRIPT" create --repo o/r --title "crash in /Users/bob/tool" \ --body "clean body" "${ATT[@]}" >/dev/null 2>&1 -assert_exit "literal home path in --title caught (exit 4)" 4 $? +assert_exit "literal home path in --title caught (exit 10)" 10 $? # bare ~/.claude.json filename mention is PUBLIC info (#203 item 1) — the private # thing is its CONTENT (covered by the projects-key net below). Must dispatch. @@ -93,7 +93,7 @@ assert_exit "bare .claude.json filename mention NOT caught (#203 item 1) → dis # proves the content-overlap net fires independently of the home-path net bash "$SCRIPT" comment 5 --repo o/r \ --body "reindex blew up on /opt/priv/hidden-proj-xyz this morning" "${ATT[@]}" >/dev/null 2>&1 -assert_exit "verbatim .claude.json project-key content caught (exit 4)" 4 $? +assert_exit "verbatim .claude.json project-key content caught (exit 10)" 10 $? # ── NOT over-matching (the wrapper must not do semantic matching) ──────────── # /Users/ PLACEHOLDER (angle-bracketed) is documentation, not a real path @@ -129,33 +129,31 @@ assert_eq "pass-through argv is byte-identical to raw gh (attestation stripped, # ── verb / usage validation ───────────────────────────────────────────────── bash "$SCRIPT" delete 5 "${ATT[@]}" >/dev/null 2>&1 -assert_exit "unknown verb → usage error (exit 2)" 2 $? +assert_exit "unknown verb → usage error (exit 14)" 14 $? bash "$SCRIPT" >/dev/null 2>&1 -assert_exit "missing verb → usage error (exit 2)" 2 $? +assert_exit "missing verb → usage error (exit 14)" 14 $? # ── #203 mechanical-net precision + edge-case hardening ───────────────────── # item 2: public tool path from mcpServers must NOT be treated as a leak # (content net scoped to the projects object when jq is available) -if command -v jq >/dev/null 2>&1; then - bash "$SCRIPT" comment 5 --repo o/r \ - --body "run it via /opt/homebrew/bin/uvx-tool instead" "${ATT[@]}" >/dev/null 2>&1 - assert_exit "public mcpServers tool path NOT caught (#203 item 2, jq path) → dispatch (exit 0)" 0 $? -fi +bash "$SCRIPT" comment 5 --repo o/r \ + --body "run it via /opt/homebrew/bin/uvx-tool instead" "${ATT[@]}" >/dev/null 2>&1 +assert_exit "public mcpServers tool path NOT caught (#203 item 2 / #225 unified) → dispatch (exit 0)" 0 $? # item 2 regression: projects key still caught (both jq and fallback paths) bash "$SCRIPT" comment 5 --repo o/r \ --body "reindex blew up on /opt/priv/hidden-proj-xyz again" "${ATT[@]}" >/dev/null 2>&1 -assert_exit "projects key still caught after item-2 scoping (exit 4)" 4 $? +assert_exit "projects key still caught after item-2 scoping (exit 10)" 10 $? # item 3: non-regular body-file (stdin dash / FIFO / process-sub) → refuse exit 5 bash "$SCRIPT" create --repo o/r --title T --body-file - "${ATT[@]}" >/dev/null 2>&1 -assert_exit "body-file '-' (stdin) refused — unscannable (#203 item 3, exit 5)" 5 $? +assert_exit "body-file '-' (stdin) refused — unscannable (#203 item 3, exit 12)" 12 $? # No writer process needed: the gate refuses on the not-a-regular-file check # WITHOUT opening the FIFO (a background writer would block forever on open() # and hold the test's stdout pipe hostage). FIFO="$WORK/fifo"; mkfifo "$FIFO" bash "$SCRIPT" create --repo o/r --title T --body-file "$FIFO" "${ATT[@]}" >/dev/null 2>&1 -assert_exit "body-file FIFO refused — unscannable (#203 item 3, exit 5)" 5 $? +assert_exit "body-file FIFO refused — unscannable (#203 item 3, exit 12)" 12 $? # regular body-file still scanned + dispatched printf 'a clean body from file' > "$WORK/body.txt" bash "$SCRIPT" create --repo o/r --title T --body-file "$WORK/body.txt" "${ATT[@]}" >/dev/null 2>&1 @@ -163,7 +161,7 @@ assert_exit "regular body-file still dispatches (exit 0)" 0 $? # regular body-file with a leak is still caught (scan path intact) printf 'log at /Users/carol/x.log' > "$WORK/leak.txt" bash "$SCRIPT" create --repo o/r --title T --body-file "$WORK/leak.txt" "${ATT[@]}" >/dev/null 2>&1 -assert_exit "regular body-file leak still caught (exit 4)" 4 $? +assert_exit "regular body-file leak still caught (exit 10)" 10 $? # item 4: HOME + IDD_CLAUDE_JSON both unset → no set -u crash, clean body dispatches env -u HOME -u IDD_CLAUDE_JSON IDD_GH_BIN="$STUB" FAKE_GH_ARGV="$FAKE_GH_ARGV" \ @@ -180,14 +178,14 @@ assert_eq "zero-arg dispatch has no phantom empty positional (#203 item 5, argv # item 6: --scrub-attested where a --body value is expected → malformed, usage refuse bash "$SCRIPT" create --repo o/r --body --scrub-attested warn >/dev/null 2>&1 -assert_exit "split-token attestation refused (#203 item 6, exit 2)" 2 $? +assert_exit "split-token attestation refused (#203 item 6, exit 14)" 14 $? # ── #117 unattested @-mention net ──────────────────────────────────────────── # raw @login token without attestation → refuse (GitHub notification is irreversible) bash "$SCRIPT" comment 5 --repo o/r \ --body "ping @nonexistentuser123 for a second look" "${ATT[@]}" >/dev/null 2>&1 -assert_exit "raw unattested @-mention refused (#117, exit 4)" 4 $? +assert_exit "raw unattested @-mention refused (#117, exit 11)" 11 $? # backtick-escaped token is inert on GitHub → dispatch bash "$SCRIPT" comment 5 --repo o/r \ @@ -214,41 +212,39 @@ refute_grep "wrapper strips --mention-attested from gh argv (#117)" "--mention-a # attestation must cover EVERY token — partial coverage still refuses bash "$SCRIPT" comment 5 --repo o/r \ --body "cc @kiki830621 and also @stranger99" "${ATT[@]}" --mention-attested kiki830621 >/dev/null 2>&1 -assert_exit "partially-attested mentions refused (#117, exit 4)" 4 $? +assert_exit "partially-attested mentions refused (#117, exit 11)" 11 $? # split-token guard extends to --mention-attested bash "$SCRIPT" create --repo o/r --body --mention-attested kiki830621 "${ATT[@]}" >/dev/null 2>&1 -assert_exit "split-token --mention-attested refused (#117, exit 2)" 2 $? +assert_exit "split-token --mention-attested refused (#117, exit 14)" 14 $? # ── cluster verify in-scope fixes (R1 findings) ────────────────────────────── # fix 1 (sec 203-2 / logic LOW): jq PRESENT but ~/.claude.json malformed → # content net must fail CLOSED to the grep fallback, not silently disable -if command -v jq >/dev/null 2>&1; then - export IDD_CLAUDE_JSON="$WORK/malformed-claude.json" - printf '%s' '{"projects":{"/opt/priv/hidden-mal-xyz":{"x":1},}' > "$IDD_CLAUDE_JSON" # trailing comma = invalid JSON - bash "$SCRIPT" comment 5 --repo o/r \ - --body "crash traced to /opt/priv/hidden-mal-xyz build" "${ATT[@]}" >/dev/null 2>&1 - assert_exit "malformed claude.json + jq present → fallback net still catches (fix1, exit 4)" 4 $? - export IDD_CLAUDE_JSON="$WORK/fixture-claude.json" -fi +export IDD_CLAUDE_JSON="$WORK/malformed-claude.json" +printf '%s' '{"projects":{"/opt/priv/hidden-mal-xyz":{"x":1},}' > "$IDD_CLAUDE_JSON" # trailing comma = invalid JSON +bash "$SCRIPT" comment 5 --repo o/r \ + --body "crash traced to /opt/priv/hidden-mal-xyz build" "${ATT[@]}" >/dev/null 2>&1 +assert_exit "malformed claude.json → parse-failure falls CLOSED to wide net (fix1/#225, exit 10)" 10 $? +export IDD_CLAUDE_JSON="$WORK/fixture-claude.json" # fix 2 (sec 117-1): entity-encoded @ forms refused (decode-side notification risk) bash "$SCRIPT" comment 5 --repo o/r \ --body "ping @realuser about this" "${ATT[@]}" >/dev/null 2>&1 -assert_exit "entity-encoded @login refused (fix2, exit 4)" 4 $? +assert_exit "entity-encoded @login refused (fix2, exit 11)" 11 $? bash "$SCRIPT" comment 5 --repo o/r \ --body "or @realuser even" "${ATT[@]}" >/dev/null 2>&1 -assert_exit "entity-encoded @login refused (fix2, exit 4)" 4 $? +assert_exit "entity-encoded @login refused (fix2, exit 11)" 11 $? # fix 3 (logic 117-1): attached short-form values are scanned like their spaced forms bash "$SCRIPT" create --repo o/r --title T "-blog at /Users/alice/secret.sh" "${ATT[@]}" >/dev/null 2>&1 -assert_exit "attached -b home-path leak caught (fix3, exit 4)" 4 $? +assert_exit "attached -b home-path leak caught (fix3, exit 10)" 10 $? bash "$SCRIPT" create --repo o/r --title T "-bcc @sneakymention" "${ATT[@]}" >/dev/null 2>&1 -assert_exit "attached -b raw mention caught (fix3, exit 4)" 4 $? +assert_exit "attached -b raw mention caught (fix3, exit 11)" 11 $? printf 'leak at /Users/dave/y.log' > "$WORK/att.txt" bash "$SCRIPT" create --repo o/r --title T "-F$WORK/att.txt" "${ATT[@]}" >/dev/null 2>&1 -assert_exit "attached -F content scanned (fix3, exit 4)" 4 $? +assert_exit "attached -F content scanned (fix3, exit 10)" 10 $? # fix 4 (logic 117-2): mention net is BODY-only — title @tokens neither notify nor can be escaped bash "$SCRIPT" create --repo o/r --title "responsive @media breakpoints" \ @@ -257,13 +253,13 @@ assert_exit "title @token NOT fed to mention net (fix4) → dispatch (exit 0)" 0 # privacy nets still scan titles (regression guard) bash "$SCRIPT" create --repo o/r --title "crash in /Users/eve/tool" \ --body "clean" "${ATT[@]}" >/dev/null 2>&1 -assert_exit "title home-path still caught by privacy net (exit 4)" 4 $? +assert_exit "title home-path still caught by privacy net (exit 10)" 10 $? # fix 5 (logic 117-3): 4-space-indented ``` is GFM literal code, NOT a fence — # must not toggle fence state and swallow a later real mention BODY_IND=$'see:\n ```\ncc @realafteripseudofence' bash "$SCRIPT" comment 5 --repo o/r --body "$BODY_IND" "${ATT[@]}" >/dev/null 2>&1 -assert_exit "indented pseudo-fence does not hide later mention (fix5, exit 4)" 4 $? +assert_exit "indented pseudo-fence does not hide later mention (fix5, exit 11)" 11 $? # ── R2 fixes (DA-r2 findings on 4d2e87b) ───────────────────────────────────── @@ -279,23 +275,23 @@ assert_exit "markdown link target @handle NOT caught (DA-117-B) → dispatch (ex # regression guard: raw mention NEXT TO a URL still caught bash "$SCRIPT" comment 5 --repo o/r \ --body "see https://example.com/docs and ping @realperson about it" "${ATT[@]}" >/dev/null 2>&1 -assert_exit "raw mention adjacent to URL still caught (exit 4)" 4 $? +assert_exit "raw mention adjacent to URL still caught (exit 11)" 11 $? # ── R3 fixes (R2-round findings) ───────────────────────────────────────────── # 117-A: URL-strip must only exempt autolink-eligible hosts (dot required); # no-dot/malformed hosts render as literal text where /@name IS a live mention bash "$SCRIPT" comment 5 --repo o/r --body "see https://@realuser now" "${ATT[@]}" >/dev/null 2>&1 -assert_exit "no-host URL @mention still caught (117-A, exit 4)" 4 $? +assert_exit "no-host URL @mention still caught (117-A, exit 11)" 11 $? bash "$SCRIPT" comment 5 --repo o/r --body "at http://localhost/@realuser today" "${ATT[@]}" >/dev/null 2>&1 -assert_exit "no-dot-host URL @mention still caught (117-A, exit 4)" 4 $? +assert_exit "no-dot-host URL @mention still caught (117-A, exit 11)" 11 $? # regression: dotted-host URLs stay exempt bash "$SCRIPT" comment 5 --repo o/r --body "docs https://unpkg.com/@angular/core here" "${ATT[@]}" >/dev/null 2>&1 assert_exit "dotted-host URL @handle stays exempt (exit 0)" 0 $? # 117-B: fully-encoded username (@realuser) must also refuse bash "$SCRIPT" comment 5 --repo o/r --body "try @realuser form" "${ATT[@]}" >/dev/null 2>&1 -assert_exit "double-encoded entity mention refused (117-B, exit 4)" 4 $? +assert_exit "double-encoded entity mention refused (117-B, exit 11)" 11 $? # 117-C: template expansion under zsh — the =-form must reach the wrapper as # ONE recognizable arg and dispatch a vetted mention (end-to-end) @@ -313,4 +309,28 @@ fi bash "$SCRIPT" create --repo o/r --body "-binline body" --title T "${ATT[@]}" >/dev/null 2>&1 assert_exit "attached form after pending --body keeps parser state sane (exit 0)" 0 $? + +# ── #227 exit-code band: wrapper-origin ≥10; gh native codes pass through <10 ── +# stub gh exiting 4 (e.g. auth failure) must surface as 4 — NOT be confused with +# a gate refusal (which now lives at ≥10). This is the caller-disambiguation +# contract: rc<10 ⇒ gh's own; rc≥10 ⇒ wrapper refusal. +FAILGH="$WORK/fail-gh" +printf '#!/usr/bin/env bash\nexit 4\n' > "$FAILGH"; chmod +x "$FAILGH" +IDD_GH_BIN="$FAILGH" bash "$SCRIPT" comment 5 --repo o/r --body "clean prose" "${ATT[@]}" >/dev/null 2>&1 +assert_exit "gh native exit 4 passes through UNCHANGED (#227 band disambiguation)" 4 $? + + +# ── #225 unified scan (python3) — non-projects sensitive paths, same everywhere ─ +# The motivating divergence: a secret-file path under mcpServers[].env dispatched +# on jq machines (projects-only scope) but refused on no-jq machines (wide net). +# The unified python3 parser scans projects keys ∪ path-shaped values under +# sensitive key names — SAME result on every machine. +bash "$SCRIPT" comment 5 --repo o/r \ + --body "loader reads /srv/secret-vault/che-key-abc at boot" "${ATT[@]}" >/dev/null 2>&1 +assert_exit "mcpServers env secret path caught (#225 unified taxonomy, exit 10)" 10 $? +# non-sensitive prose sharing no config path still dispatches (no over-match) +bash "$SCRIPT" comment 5 --repo o/r \ + --body "we store vault material under /srv/other-vault/keyring generally" "${ATT[@]}" >/dev/null 2>&1 +assert_exit "similar-but-absent path NOT caught (#225 no fuzzy matching) → dispatch (exit 0)" 0 $? + print_summary "gh-egress" diff --git a/plugins/issue-driven-dev/skills/.impeccable/hook.cache.json b/plugins/issue-driven-dev/skills/.impeccable/hook.cache.json new file mode 100644 index 0000000..ebc7c5d --- /dev/null +++ b/plugins/issue-driven-dev/skills/.impeccable/hook.cache.json @@ -0,0 +1 @@ +{"version":1,"sessions":{}} \ No newline at end of file diff --git a/plugins/issue-driven-dev/skills/idd-close/SKILL.md b/plugins/issue-driven-dev/skills/idd-close/SKILL.md index 8698394..44a60b3 100644 --- a/plugins/issue-driven-dev/skills/idd-close/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-close/SKILL.md @@ -226,7 +226,7 @@ TaskCreate(name="draft_closing_comment", description="起草 closing summary:c TaskCreate(name="review_with_user", description="顯示 closing comment 給使用者確認(若已明確 /idd-close 可省略此步)") TaskCreate(name="closing_followup_keyword_scan", description="Step 3.5: scan drafted closing summary for trigger phrases (follow-up / deferred / future / 之後 / 順便 etc); orphan mentions without #NNN cross-link → AskUserQuestion 3-option per canonical references/ic-r011-checkpoint.md; PATCH closing summary inline + add `### Closing Follow-ups Filed` audit trail (advisory, non-blocking, per IC_R011 #527)") TaskCreate(name="residue_acknowledgement", description="Step 3.6 (v2.66.0+, #105): read latest ## Diagnosis ### Residue section; if non-empty (not `(none)`), AskUserQuestion 3-option (still residue / file follow-up / skip); silent skip when residue is `(none)` or section missing. Audit trail PATCH to closing summary. Non-blocking, IC_R011 rollback respected. Closes the F3 write-only loop from #103.") -TaskCreate(name="publish_and_close", description="gh issue comment + gh issue close") +TaskCreate(name="publish_and_close", description="經 gh-egress.sh comment 派送 closing summary(#226,--scrub-attested)+ gh issue close") TaskCreate(name="auto_update_body", description="跑 /idd-update #NNN 把 issue body 的 Current Status phase 改 closed(Step 6,常被漏)") TaskCreate(name="doc_sync_sweep", description="Step 6.3 (v2.92+, #220): git ls-files case-insensitive 枚舉 README*/CLAUDE.md(含 localized 變體)→ per-file staleness 語意判定(以本 issue diff+summary 為據)→ 更新 commit 或 surface 到 closing summary「### Doc-sync 檢查」段;結果必留痕(N files checked 一行)") TaskCreate(name="distribution_sync_chain_detection", description="Step 6.5 (v2.56.0+, #45): infer_distribution_type detection per references/distribution-detection.md; if hit → AskUserQuestion 3-option (chain to plugin-update/mcp-deploy/cli-deploy / skip — manual later / not applicable); patch closing comment with ### Distribution Sync section. Silent skip for non-distribution repos. IDD_DISTRIBUTION_SYNC_PROMPT=false env var bypasses prompt entirely (still 1-line audit).") @@ -612,7 +612,10 @@ The closing summary (Step 2 已 drafted but not yet posted, same pattern as Step # deprecation/warning messages can interleave with the URL, leading to a garbage ID. # `gh issue comment` prints ONLY the comment URL to stdout on success;errors go to stderr. # `set -e` (or explicit `|| abort`) guarantees we don't proceed with empty URL on failure. -CLOSING_COMMENT_URL=$(gh issue comment $NUMBER --repo $GITHUB_REPO --body "$CLOSING_COMMENT") || { +# (#226)egress 經 gh-egress.sh 派送:$SCRUB_LEVEL 依 rules/privacy-scrubbing.md 解析 +# (third-party=enforce / own-public=warn / private=light),派送前先跑 LLM 隱私自審; +# 有 @mention 時另帶 --mention-attested(rules/tagging-collaborators.md 5-step 後)。 +CLOSING_COMMENT_URL=$(bash "$CLAUDE_PLUGIN_ROOT/scripts/gh-egress.sh" comment $NUMBER --repo $GITHUB_REPO --body "$CLOSING_COMMENT" --scrub-attested "$SCRUB_LEVEL") || { echo "ERROR: gh issue comment failed for #$NUMBER" >&2 exit 1 } diff --git a/plugins/issue-driven-dev/skills/idd-comment/SKILL.md b/plugins/issue-driven-dev/skills/idd-comment/SKILL.md index 8e76914..a021030 100644 --- a/plugins/issue-driven-dev/skills/idd-comment/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-comment/SKILL.md @@ -79,7 +79,7 @@ TaskCreate(name="validate_type_requirements", description="依 type 檢查必填 TaskCreate(name="resolve_mentions", description="Step 1.5: 若有 --mention 或 body 含 @xxx,強制走 rules/tagging-collaborators.md 協定(gh api 取 collaborators → fuzzy match → AskUserQuestion fallback → 用 @login 不用 display name)") TaskCreate(name="build_comment_body", description="按 type 對應 template 組 markdown(emoji header + blockquote + body + metadata marker),插入已驗證的 @login mentions") TaskCreate(name="verify_mentions", description="post 前 grep body 的 @\\w+ 全部 cross-check 已驗證的 collaborator set;未驗證 token 直接 abort") -TaskCreate(name="post_comment", description="gh issue comment #NNN 用 --body-file 避免 escape 問題;errata type 額外 auto-call idd-edit") +TaskCreate(name="post_comment", description="經 gh-egress.sh comment 派送(#226;--scrub-attested 依 rules/privacy-scrubbing.md 解析,mention 帶 --mention-attested),--body-file 避免 escape 問題;errata type 額外 auto-call idd-edit") TaskCreate(name="report_result", description="輸出 ✓ Comment posted + URL;errata type 加報 idd-edit 結果") TaskCreate(name="auto_update_body", description="跑 /idd-update #NNN 同步 issue body Current Status(強制,常被漏;同 idd-close Step 6 模式)") TaskCreate(name="spectra_bridge_resume", description="Step 7: 若 SPECTRA_BRIDGE_ACTIVE,寫 .claude/.idd/state/bridge.json bookmark + 輸出 ↩ Resume spectra-discuss 區塊(rules/spectra-bridge.md)") @@ -342,7 +342,11 @@ done ```bash # 用 --body-file 避免 backtick / 多行 escape 問題 echo "$COMMENT_BODY" > /tmp/idd-comment-$$.md -gh issue comment $NUMBER --repo $GITHUB_REPO --body-file /tmp/idd-comment-$$.md +# (#226)egress 經 gh-egress.sh 派送:$SCRUB_LEVEL 依 rules/privacy-scrubbing.md 解析 +# (third-party=enforce / own-public=warn / private=light),派送前先跑 LLM 隱私自審; +# 有 @mention 時另帶 --mention-attested(rules/tagging-collaborators.md 5-step 後)。 +bash "$CLAUDE_PLUGIN_ROOT/scripts/gh-egress.sh" comment $NUMBER --repo $GITHUB_REPO --body-file /tmp/idd-comment-$$.md \ + --scrub-attested "$SCRUB_LEVEL" ${MENTION_ATTESTED:+--mention-attested="$MENTION_ATTESTED"} rm /tmp/idd-comment-$$.md ``` diff --git a/plugins/issue-driven-dev/skills/idd-diagnose/SKILL.md b/plugins/issue-driven-dev/skills/idd-diagnose/SKILL.md index b1033dc..9af5dcb 100644 --- a/plugins/issue-driven-dev/skills/idd-diagnose/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-diagnose/SKILL.md @@ -312,7 +312,10 @@ Exit code: ``` ```bash -gh issue comment $NUMBER --repo $GITHUB_REPO --body "$DIAGNOSIS_REPORT" +# (#226)egress 經 gh-egress.sh 派送:$SCRUB_LEVEL 依 rules/privacy-scrubbing.md 解析 +# (third-party=enforce / own-public=warn / private=light),派送前先跑 LLM 隱私自審; +# 有 @mention 時另帶 --mention-attested(rules/tagging-collaborators.md 5-step 後)。 +bash "$CLAUDE_PLUGIN_ROOT/scripts/gh-egress.sh" comment $NUMBER --repo $GITHUB_REPO --body "$DIAGNOSIS_REPORT" --scrub-attested "$SCRUB_LEVEL" ``` > **`type=meeting` 的 meeting-adapted Diagnosis(#57)**:當 issue type 是 `meeting` 時,`### Strategy` 段改用 **Phase A/B/C 審議模板**,**而非 code-centric 的 Files & Changes / Strategy checklist**(meeting 是使用者驅動的審議 / 決策工作,不是 TDD loop,emit code-centric 計畫沒有意義)。meeting Diagnosis 的 Strategy 段落格式: @@ -426,7 +429,7 @@ AskUserQuestion( (repeat for each Q/A pair) ``` -3. 用 `gh issue edit $NUMBER --repo $GITHUB_REPO --body "$NEW_BODY"` append 到 issue body(放在 `---` separator 上,Current Status 之前) +3. 用 `bash "$CLAUDE_PLUGIN_ROOT/scripts/gh-egress.sh" edit $NUMBER --repo $GITHUB_REPO --body "$NEW_BODY" --scrub-attested "$SCRUB_LEVEL"`(#226)append 到 issue body(放在 `---` separator 上,Current Status 之前) 4. **重跑 Step 3.4**(Layer V 用更新後的 body 重新評分;若仍 trigger 再問,但循環不超過 2 次)+ Step 3.5 **`proceed anyway`**: diff --git a/plugins/issue-driven-dev/skills/idd-implement/SKILL.md b/plugins/issue-driven-dev/skills/idd-implement/SKILL.md index b0b2ee7..d9519de 100644 --- a/plugins/issue-driven-dev/skills/idd-implement/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-implement/SKILL.md @@ -356,7 +356,10 @@ idd-implement 是 **orchestration**(issue tracking、plan、checklist、body s **Comment 到 issue**(留下實作計畫的紀錄): ```bash -gh issue comment $NUMBER --repo $GITHUB_REPO --body "$IMPLEMENTATION_PLAN" +# (#226)egress 經 gh-egress.sh 派送:$SCRUB_LEVEL 依 rules/privacy-scrubbing.md 解析 +# (third-party=enforce / own-public=warn / private=light),派送前先跑 LLM 隱私自審; +# 有 @mention 時另帶 --mention-attested(rules/tagging-collaborators.md 5-step 後)。 +bash "$CLAUDE_PLUGIN_ROOT/scripts/gh-egress.sh" comment $NUMBER --repo $GITHUB_REPO --body "$IMPLEMENTATION_PLAN" --scrub-attested "$SCRUB_LEVEL" ``` ### Step 2.5: Bootstrap TodoList(non-Spectra case) @@ -508,7 +511,7 @@ gh release upload $ATTACHMENTS_RELEASE {figure_files}.png \ **Comment 實作摘要到 issue**(含圖片): ```bash -gh issue comment $NUMBER --repo $GITHUB_REPO --body "$(cat <<'EOF' +bash "$CLAUDE_PLUGIN_ROOT/scripts/gh-egress.sh" comment $NUMBER --repo $GITHUB_REPO --scrub-attested "$SCRUB_LEVEL" --body "$(cat <<'EOF' ## Implementation Complete ### Checklist (synced from TaskList) diff --git a/plugins/issue-driven-dev/skills/idd-update/SKILL.md b/plugins/issue-driven-dev/skills/idd-update/SKILL.md index 6c5b96a..f513416 100644 --- a/plugins/issue-driven-dev/skills/idd-update/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-update/SKILL.md @@ -197,7 +197,10 @@ Managed zone 錨在 **`## Current Status` heading**,**不是**第一個 `---` > **Backward-compat**:既有「`---` 緊鄰 `## Current Status`」的 body,Branch A 與舊「replace below first `---`」產出**同結果**。只有「多個 `---`、audit 夾在中間」的 body 行為改變 —— 那正是修復(舊邏輯誤刪、新邏輯保留)。Fix **strictly safer**:只保留更多、不刪更多。 ```bash -gh issue edit $NUMBER --repo $GITHUB_REPO --body "$UPDATED_BODY" +# (#226)egress 經 gh-egress.sh 派送:$SCRUB_LEVEL 依 rules/privacy-scrubbing.md 解析 +# (third-party=enforce / own-public=warn / private=light),派送前先跑 LLM 隱私自審; +# 有 @mention 時另帶 --mention-attested(rules/tagging-collaborators.md 5-step 後)。 +bash "$CLAUDE_PLUGIN_ROOT/scripts/gh-egress.sh" edit $NUMBER --repo $GITHUB_REPO --body "$UPDATED_BODY" --scrub-attested "$SCRUB_LEVEL" ``` ### Step 6: Report Update diff --git a/plugins/issue-driven-dev/skills/idd-verify/SKILL.md b/plugins/issue-driven-dev/skills/idd-verify/SKILL.md index 4ca8552..d79dfdc 100644 --- a/plugins/issue-driven-dev/skills/idd-verify/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-verify/SKILL.md @@ -825,7 +825,7 @@ sed "s|__MASTER_URL__|$MASTER_URL|g" /tmp/pointer_template.md > /tmp/pointer.md # 3. Post pointer to each ref'd issue in parallel for I in $REFD_ISSUES; do - gh issue comment $I --repo $GITHUB_REPO --body-file /tmp/pointer.md & + bash "$CLAUDE_PLUGIN_ROOT/scripts/gh-egress.sh" comment $I --repo $GITHUB_REPO --body-file /tmp/pointer.md --scrub-attested "$SCRUB_LEVEL" & done wait ``` @@ -845,7 +845,10 @@ This issue's findings: see "#__ISSUE__" section in the linked report. 單 issue:直接貼到 issue。 ```bash -gh issue comment $NUMBER --repo $GITHUB_REPO --body "$MERGED_FINDINGS" +# (#226)egress 經 gh-egress.sh 派送:$SCRUB_LEVEL 依 rules/privacy-scrubbing.md 解析 +# (third-party=enforce / own-public=warn / private=light),派送前先跑 LLM 隱私自審; +# 有 @mention 時另帶 --mention-attested(rules/tagging-collaborators.md 5-step 後)。 +bash "$CLAUDE_PLUGIN_ROOT/scripts/gh-egress.sh" comment $NUMBER --repo $GITHUB_REPO --body "$MERGED_FINDINGS" --scrub-attested "$SCRUB_LEVEL" ``` Cluster(≥2 issue 共用一份 verify report): @@ -857,14 +860,14 @@ Cluster(≥2 issue 共用一份 verify report): **Helper pattern**: ```bash # 1. Post master, capture URL -MASTER_URL=$(gh issue comment $HUB_ISSUE --repo $REPO --body-file /tmp/master.md 2>&1 | tail -1) +MASTER_URL=$(bash "$CLAUDE_PLUGIN_ROOT/scripts/gh-egress.sh" comment $HUB_ISSUE --repo $REPO --body-file /tmp/master.md --scrub-attested "$SCRUB_LEVEL" 2>&1 | tail -1) # 2. Compose pointer body using captured URL sed "s|__MASTER_URL__|$MASTER_URL|g" /tmp/pointer_template.md > /tmp/pointer.md # 3. Post pointers in parallel for I in $POINTER_ISSUES; do - gh issue comment $I --repo $REPO --body-file /tmp/pointer.md & + bash "$CLAUDE_PLUGIN_ROOT/scripts/gh-egress.sh" comment $I --repo $REPO --body-file /tmp/pointer.md --scrub-attested "$SCRUB_LEVEL" & done wait ```