mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-14 17:05:28 +02:00
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>
80 lines
3.3 KiB
TypeScript
80 lines
3.3 KiB
TypeScript
/**
|
|
* Unit tests for scripts/one-way-doors.ts keyword safety net.
|
|
*
|
|
* The keyword layer is the SECONDARY safety net for ad-hoc AskUserQuestion ids
|
|
* with no registry entry. A false negative auto-approves a destructive op, so the
|
|
* credential-rotation patterns must be parallel across revoke/reset/rotate.
|
|
*/
|
|
import { describe, test, expect } from "bun:test";
|
|
import { classifyQuestion } from "../scripts/one-way-doors";
|
|
|
|
describe("one-way-door credential keyword net (#1839)", () => {
|
|
// rotate ... password was missing from the rotate alternation while revoke and
|
|
// reset both had it — the most common phrasing slipped through as two-way.
|
|
test('"rotate the database password" classifies one-way', () => {
|
|
const r = classifyQuestion({ summary: "rotate the database password" });
|
|
expect(r.oneWay).toBe(true);
|
|
expect(r.reason).toBe("keyword");
|
|
});
|
|
|
|
test("revoke/reset/rotate are all parallel for password", () => {
|
|
for (const verb of ["revoke", "reset", "rotate"]) {
|
|
const r = classifyQuestion({ summary: `${verb} the production password` });
|
|
expect(r.oneWay).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("rotate still catches the other credential nouns", () => {
|
|
for (const noun of ["api key", "token", "secret", "credential", "access key"]) {
|
|
expect(classifyQuestion({ summary: `rotate my ${noun}` }).oneWay).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("one-way-door credential keyword net (#2024)", () => {
|
|
const VERBS = ["revoke", "reset", "rotate"];
|
|
const NOUNS = ["api key", "token", "secret", "credential", "access key", "password"];
|
|
|
|
// #2024 repro rows: these leaked as two-way pre-fix because the noun
|
|
// alternations were mismatched across verbs (revoke lacked secret; reset
|
|
// lacked secret AND access key). The password-parallel test above passes on
|
|
// buggy code, so THESE rows are the fails-first proof.
|
|
test('"reset my secret" / "reset my access key" / "revoke my secret" classify one-way', () => {
|
|
for (const summary of ["reset my secret", "reset my access key", "revoke my secret"]) {
|
|
const r = classifyQuestion({ summary });
|
|
expect(r.oneWay).toBe(true);
|
|
expect(r.reason).toBe("keyword");
|
|
}
|
|
});
|
|
|
|
test("full verbs x nouns matrix classifies one-way (singular and plural)", () => {
|
|
for (const verb of VERBS) {
|
|
for (const noun of NOUNS) {
|
|
for (const form of [noun, `${noun}s`]) {
|
|
const r = classifyQuestion({ summary: `${verb} the production ${form}` });
|
|
expect(r.oneWay).toBe(true);
|
|
expect(r.reason).toBe("keyword");
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Plural forms leaked before AND after the original #2024 report: \b(...)\b
|
|
// cannot match "credentials" (no word boundary between the noun and its s).
|
|
test('plurals: "rotate the credentials" / "revoke all tokens" / "reset the passwords" classify one-way', () => {
|
|
for (const summary of ["rotate the credentials", "revoke all tokens", "reset the passwords"]) {
|
|
expect(classifyQuestion({ summary }).oneWay).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("benign summaries stay two-way (no over-match)", () => {
|
|
for (const summary of [
|
|
"reset the flaky test runner",
|
|
"rotate the log files nightly",
|
|
"revoke the meeting invite",
|
|
]) {
|
|
expect(classifyQuestion({ summary }).oneWay).toBe(false);
|
|
}
|
|
});
|
|
});
|