Skill Tracer
Skills › gstack › gstack › redact-doc.ts

redact-doc.ts

116 lines · scripts/resolvers/redact-doc.ts at 3f79d81

1/**
2 * redact-doc — resolvers for the shared redaction docs + invocation bash.
3 *
4 * {{REDACT_TAXONOMY_TABLE}} → markdown table of the 3-tier taxonomy,
5 * derived from lib/redact-patterns so /spec
6 * and /cso never drift from the engine.
7 * {{REDACT_INVOCATION_BLOCK:<sink>}} → the canonical scan-at-sink bash + prose
8 * for one enforcement point. <sink> is a
9 * hyphenated label: pre-codex, pre-archive,
10 * pre-commit.
11 *
12 * PR, MR and issue text has no sink here: bin/gstack-post scans the exact
13 * bytes it sends and refuses HIGH itself (CEO-19), so those sites carry no
14 * separate scan.
15 *
16 * DRY: every skill writes one placeholder per enforcement point; UX/threshold
17 * changes land here once. test/redact-doc-resolver.test.ts golden-pins the output.
18 */
19import { toShellPath, type TemplateContext } from './types';
20import { FREE_TEXT_DIR } from './free-text-file';
22interface SinkSpec {
23 /** What is being scanned, for the prose. */
24 noun: string;
25 /** What HIGH blocks, in this skill's verbs. */
26 blockVerb: string;
29const SINKS: Record<string, SinkSpec> = {
30 'pre-codex': { noun: 'the spec body', blockVerb: 'dispatch to the outside reviewer' },
31 'pre-archive': { noun: 'the body about to be archived', blockVerb: 'write the archive' },
32 'pre-commit': { noun: 'the generated docs about to be committed', blockVerb: 'commit' },
33};
34const UNKNOWN_SINK: SinkSpec = { noun: 'the text about to be sent', blockVerb: 'send it' };
36export function generateRedactInvocationBlock(ctx: TemplateContext, args?: string[]): string {
37 const sinkLabel = args?.[0] ?? 'unknown';
38 const brief = args?.[1] === 'brief';
39 const sink = SINKS[sinkLabel] ?? UNKNOWN_SINK;
40 const bin = `${ctx.paths.binDir}/gstack-redact`;
41 const outsideGate = sinkLabel === 'pre-codex';
42 const scan = `REDACT_JSON=$(${outsideGate ? `"${toShellPath(bin)}"` : bin} --from-file "$REDACT_FILE" --repo-visibility "$REDACT_VIS" --self-email "$(git config user.email 2>/dev/null)" --json)`;
43 // This sink can dispatch a model and then publish/archive the same spec.
44 // Keep its stop decision in executable shell, even when the caller runs
45 // without errexit. MEDIUM must pause for its existing user decision.
46 const scanAndGate = outsideGate ? `if ${scan}; then REDACT_CODE=0; else REDACT_CODE=$?; fi
47case "$REDACT_CODE" in
48 0) ;; # Only a successful scan may reach an outside or downstream sink.
49 2)
50 printf '%s\\n' "$REDACT_JSON"
51 printf 'REDACT_FILE: %s\\n' "$REDACT_FILE"
52 echo 'Redaction requires the MEDIUM disposition below; outside dispatch and downstream persistence are paused.' >&2
53 exit 2 ;;
54 3)
55 printf '%s\\n' "$REDACT_JSON"
56 rm -f "$REDACT_FILE"
57 echo 'HIGH redaction finding: outside dispatch and downstream persistence blocked. Redact at source and rescan; no skip.' >&2
58 exit 3 ;;
59 *)
60 rm -f "$REDACT_FILE"
61 echo "Redaction scan failed (exit $REDACT_CODE); refusing outside dispatch and downstream persistence." >&2
62 exit 1 ;;
63esac` : `${scan}\nREDACT_CODE=$?`;
65 // Brief variant: a compact pointer for repeat sinks, so the full ~40-line
66 // procedure ships once per skill, not once per enforcement point.
67 if (brief) {
68 return `#### Redaction scan — ${sinkLabel} (${sink.noun})
70Run the SAME scan-at-sink procedure shown above (resolve \`$REDACT_VIS\` once and
71reuse it; when ${sink.noun} changed since the last scan, rewrite the same \`$REDACT_FILE\`
72with your file-write tool; \`${bin} --from-file "$REDACT_FILE"
73--repo-visibility "$REDACT_VIS" --json\`), now on ${sink.noun}. Apply the same
74exit-3/2/0 handling. On exit 3, do NOT ${sink.blockVerb}; HIGH has no skip. Pass the
75same \`$REDACT_FILE\` downstream so the bytes scanned are the bytes sent.`;
76 }
78 return `#### Redaction scan — ${sinkLabel} (${sink.noun})
80Scan-at-sink on the EXACT bytes that will be sent: they live in the private file
81you wrote with your file-write tool, the scan reads that file, and the SAME file goes
82downstream. Never scan a string then re-render it, and never put the text in a shell
83command. Substitute the file's printed name for \`<redact-file-name>\`.
85\`\`\`bash
86${outsideGate ? 'command -v bun >/dev/null 2>&1 || { echo "ERROR: bun unavailable — refusing unscanned outside dispatch." >&2; exit 1; }' : 'command -v bun >/dev/null 2>&1 || echo "redaction scan skipped — bun not on PATH"'}
87# Resolve visibility once; cache + reuse. Order: local config (~/.gstack, never
88# committed) → gh → glab → unknown(=public-strict).
89REDACT_VIS=$(~/.claude/skills/gstack/bin/gstack-config get redact_repo_visibility 2>/dev/null)
90[ -z "$REDACT_VIS" ] && REDACT_VIS=$(gh repo view --json visibility -q .visibility 2>/dev/null | tr 'A-Z' 'a-z')
91[ -z "$REDACT_VIS" ] && REDACT_VIS=$(glab repo view -F json 2>/dev/null | grep -o '"visibility":"[^"]*"' | head -1 | sed 's/.*:"//;s/"//' | tr 'A-Z' 'a-z')
92REDACT_VIS="\${REDACT_VIS:-unknown}"
93REDACT_FILE=${FREE_TEXT_DIR.slice(0, -1)}/<redact-file-name>"
94[ -s "$REDACT_FILE" ] || { echo "ERROR: $REDACT_FILE is missing or empty — write ${sink.noun} into it first; refusing to send it unscanned." >&2; exit 1; }
95${scanAndGate}
96\`\`\`
98${outsideGate ? 'The shell has already stopped on HIGH, MEDIUM, or scanner failure. On MEDIUM, keep the printed REDACT_FILE pending the decision below: edit/auto-redact and rescan, cancel and remove the file, or resume only after an explicitly permitted acknowledgement. No downstream command runs in that paused shell. Clean scans retain the same scanned file for the approved sink.\n\n' : ''}Branch on \`$REDACT_CODE\`:
1001. **Exit 3 (HIGH)** — print findings; do NOT ${sink.blockVerb}; tell the user to
101 rotate + redact at source, then re-run. No skip flag for HIGH. Do not persist
102 ${sink.noun} anywhere.
1032. **Exit 2 (MEDIUM)** — AskUserQuestion per finding (cluster identical ids; PUBLIC
104 repos get sterner wording, no batch-acknowledge, no silent-proceed). PII subset
105 (\`pii.email\`/\`pii.phone.e164\`/\`pii.ssn\`/\`pii.cc\`) gets **Auto-redact** (re-run
106 with \`--auto-redact <ids>\` → use the printed sanitized body) / **Edit** / **Cancel**;
107 non-PII MEDIUM gets **Proceed (acknowledged)** / **Edit** / **Cancel** (no auto-redact).
1083. **Exit 0 (clean)** — proceed; surface \`WARN\` (tool-fence degrades) + \`LOW\` as a
109 one-line FYI (never blocks).
111${outsideGate ? 'After the approved sink consumes the file, or when the user cancels, clean up (never before dispatch reads the scanned bytes):\n\n' : ''}\`\`\`bash
112rm -f "$REDACT_FILE"
113\`\`\`
115Guardrail, not airtight enforcement — direct \`gh\`/\`git\` bypass it; it catches accidents.`;