fix(one-way-doors): unify credential noun net + wire it into the runtime (#2024)

Library fix: revoke/reset/rotate now share ONE noun alternation (api key,
token, secret, credential, access key, password) with optional plural s?.
Pre-fix leaks: "reset my secret", "reset my access key", "revoke my secret"
(mismatched per-verb lists) and every plural form ("rotate the credentials",
"revoke all tokens" — \b(...)\b cannot match a trailing s).

Runtime wiring — the regexes could never fire in production before:
- gstack-question-preference --check gains --summary-stdin: the question
  text pipes via stdin (never argv — summaries carry quotes/newlines/shell
  metacharacters) and feeds isOneWayDoor alongside the id, so an ad-hoc
  destructive question with a stored never-ask preference now forces
  ASK_NORMALLY. Empty/absent stdin keeps exact id-only semantics.
- question-preference-hook falls back to classifyQuestion(question text)
  when the registry lookup misses, so unregistered destructive questions
  pass through to a human instead of auto-deciding.
- question-tuning resolver prose shows the piped form (SKILL.md regen lands
  in the wave's release commit).

Tripwires (verified fail-first): full verbs x nouns x singular/plural matrix
with the #2024 repro rows, benign-summary no-over-match rows, stdin
transport survival (quotes/newlines), empty-stdin fail-safe, and hook
fallback both directions (destructive -> pass-through, benign -> deny).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-07-09 19:13:54 -07:00
co-authored by Claude Fable 5
parent 3d97863b14
commit e0662ea7b5
7 changed files with 199 additions and 11 deletions
+9 -4
View File
@@ -62,10 +62,15 @@ const DESTRUCTIVE_PATTERNS: RegExp[] = [
/\bterraform\s+destroy\b/i,
/\brollback\b/i,
// Credentials / auth — allow filler words ("the", "my") between verb and noun
/\brevoke\s+[\w\s]*\b(api key|token|credential|access key|password)\b/i,
/\breset\s+[\w\s]*\b(api key|token|password|credential)\b/i,
/\brotate\s+[\w\s]*\b(api key|token|secret|credential|access key|password)\b/i,
// Credentials / auth — allow filler words ("the", "my") between verb and noun.
// All three verbs share ONE noun list (#2024: mismatched alternations let
// "reset my secret" / "reset my access key" / "revoke my secret" leak as
// two-way), with optional plural (`s?` — \b(...)\b alone cannot match
// "credentials"). Keep these parallel: a verb-specific noun list is how
// this class of false negative happens.
/\brevoke\s+[\w\s]*\b(api key|token|secret|credential|access key|password)s?\b/i,
/\breset\s+[\w\s]*\b(api key|token|secret|credential|access key|password)s?\b/i,
/\brotate\s+[\w\s]*\b(api key|token|secret|credential|access key|password)s?\b/i,
// Scope / architecture forks (reversible with effort — still deserve confirmation)
/\barchitectur(e|al)\s+(change|fork|shift|decision)\b/i,
+6 -2
View File
@@ -23,7 +23,11 @@ export function generateQuestionTuning(ctx: TemplateContext): string {
const bin = binDir(ctx);
return `## Question Tuning (skip entirely if \`QUESTION_TUNING: false\`)
Before each AskUserQuestion, choose \`question_id\` from \`scripts/question-registry.ts\` or \`{skill}-{slug}\`, then run \`${bin}/gstack-question-preference --check "<id>"\`. \`AUTO_DECIDE\` means choose the recommended option and say "Auto-decided [summary] → [option] (your preference). Change with /plan-tune." \`ASK_NORMALLY\` means ask.
Before each AskUserQuestion, choose \`question_id\` from \`scripts/question-registry.ts\` or \`{skill}-{slug}\`, then run the check with the question summary piped on stdin (the summary lets the one-way keyword net catch ad-hoc destructive questions whose id has no registry entry — #2024; stdin, never argv, so quotes/newlines survive):
\`\`\`bash
printf '%s' "<question summary>" | ${bin}/gstack-question-preference --check "<id>" --summary-stdin
\`\`\`
\`AUTO_DECIDE\` means choose the recommended option and say "Auto-decided [summary] → [option] (your preference). Change with /plan-tune." \`ASK_NORMALLY\` means ask.
**Embed the question_id as a marker in the question text** so hooks can identify it deterministically (plan-tune cathedral T14 / D18 progressive markers). Append \`<gstack-qid:{question_id}>\` somewhere in the rendered question (the leading line or trailing line is fine; the marker doesn't render visibly to the user when wrapped in HTML-style angle brackets, but the hook strips it). Without the marker the PreToolUse enforcement hook treats the AUQ as observed-only and never auto-decides — so always include it when the question matches a registered \`question_id\`.
@@ -51,7 +55,7 @@ export function generateQuestionPreferenceCheck(ctx: TemplateContext): string {
const bin = binDir(ctx);
return `## Question Preference Check (skip if \`QUESTION_TUNING: false\`)
Before each AskUserQuestion, run: \`${bin}/gstack-question-preference --check "<id>"\`.
Before each AskUserQuestion, run: \`printf '%s' "<question summary>" | ${bin}/gstack-question-preference --check "<id>" --summary-stdin\`.
\`AUTO_DECIDE\` → auto-choose recommended with inline annotation. \`ASK_NORMALLY\` → ask.`;
}