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
+23 -4
View File
@@ -5,7 +5,9 @@
# Schema: { "<question_id>": "always-ask" | "never-ask" | "ask-only-for-one-way" }
#
# Subcommands:
# --check <id> → emit ASK_NORMALLY | AUTO_DECIDE | ASK_ONLY_ONE_WAY
# --check <id> [--summary-stdin] → emit ASK_NORMALLY | AUTO_DECIDE | ASK_ONLY_ONE_WAY
# (--summary-stdin pipes the question text so the
# keyword net can catch ad-hoc destructive ids, #2024)
# --write '{...}' → set a preference (user-origin gate enforced)
# --read → dump preferences JSON
# --clear [<id>] → clear one or all preferences
@@ -44,22 +46,39 @@ ensure_file() {
# --check <question_id>
# -----------------------------------------------------------------------
do_check() {
local QID="${1:-}"
local QID="" SUMMARY_STDIN=false
while [ $# -gt 0 ]; do
case "$1" in
--summary-stdin) SUMMARY_STDIN=true; shift ;;
*) [ -z "$QID" ] && QID="$1"; shift ;;
esac
done
if [ -z "$QID" ]; then
echo "ASK_NORMALLY"
return 0
fi
# #2024: the question text feeds the keyword classifier for unregistered
# ids. Transport is stdin (not argv) — summaries carry quotes, newlines,
# and shell metacharacters an argv tail would mangle. Handed to bun via
# env so no shell re-quoting happens. Empty summary = id-only behavior.
local QSUMMARY=""
if [ "$SUMMARY_STDIN" = true ]; then
QSUMMARY=$(cat 2>/dev/null || true)
fi
ensure_file
cd "$ROOT_DIR"
PREF_FILE_PATH="$PREF_FILE" QID="$QID" bun -e "
PREF_FILE_PATH="$PREF_FILE" QID="$QID" QSUMMARY="$QSUMMARY" bun -e "
import('./scripts/one-way-doors.ts').then((oneway) => {
const fs = require('fs');
const qid = process.env.QID;
const summary = process.env.QSUMMARY || undefined;
const prefs = JSON.parse(fs.readFileSync(process.env.PREF_FILE_PATH, 'utf-8'));
const pref = prefs[qid];
// Always check one-way status first — safety overrides preferences.
const oneWay = oneway.isOneWayDoor({ question_id: qid });
// summary (when piped) lets the keyword net catch ad-hoc destructive
// questions whose id has no registry entry (#2024).
const oneWay = oneway.isOneWayDoor({ question_id: qid, summary });
if (oneWay) {
console.log('ASK_NORMALLY');