From 85ab953cc9174e85dda10fb8d59e5b0bf315052b Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 14 Aug 2026 19:28:49 -0700 Subject: [PATCH] fix(safety): unknown question-preference source exits the documented 2, not 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The --write user-origin gate documents exit 2 as "rejected, do not retry" (profile poisoning defense), but a source outside both the allowed and the explicitly-rejected lists fell through to exit 1 — the generic validation code callers treat as retryable. Unknown sources now exit 2 with the same do-not-retry rejection message as the known non-user-originated ones. Closes #2390. Contributed by @gregario (PR #2429). Co-Authored-By: Claude Fable 5 --- bin/gstack-question-preference | 4 ++-- test/gstack-question-preference.test.ts | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/bin/gstack-question-preference b/bin/gstack-question-preference index 34271aeef..67be97e40 100755 --- a/bin/gstack-question-preference +++ b/bin/gstack-question-preference @@ -174,8 +174,8 @@ do_write() { process.exit(2); } if (!ALLOWED_SOURCES.includes(j.source)) { - process.stderr.write('gstack-question-preference: invalid source \"' + j.source + '\"; allowed: ' + ALLOWED_SOURCES.join(', ') + '\n'); - process.exit(1); + process.stderr.write('gstack-question-preference: rejected — source \"' + j.source + '\" is not user-originated (profile poisoning defense)\n'); + process.exit(2); } // Optional free_text — sanitize (no injection patterns, no newlines, <=300 chars) diff --git a/test/gstack-question-preference.test.ts b/test/gstack-question-preference.test.ts index 8b6fcbe04..c37813b1a 100644 --- a/test/gstack-question-preference.test.ts +++ b/test/gstack-question-preference.test.ts @@ -338,8 +338,16 @@ describe('--write user-origin gate (profile-poisoning defense)', () => { '--write', JSON.stringify({ question_id: 'q1', preference: 'never-ask', source: 'anonymous' }), ); - expect(r.status).not.toBe(0); - expect(r.stderr).toContain('invalid source'); + expect(r.status).toBe(2); + expect(r.stderr).toContain('profile poisoning defense'); + }); + + test('unknown source exits 2 (user-origin rejection), not 1 (validation error)', () => { + const r = run( + '--write', + JSON.stringify({ question_id: 'q1', preference: 'never-ask', source: 'tool-output' }), + ); + expect(r.status).toBe(2); }); });