From 0eaff4b3767381e4d25bcb9f1d70d516ff8bb2cc Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Tue, 8 Sep 2026 17:53:57 +0000 Subject: [PATCH] feat(egress): memorable-recall row in gstack-egress grants `gstack-egress grants` promises every standing consent in force with the command that revokes it. The Memorable bridge's memorable_recall key is one, so it gets a row: off by default, granted only when `gstack-memorable enable` set it, revoked by `gstack-memorable disable`. Co-Authored-By: Claude Fable 5.1 --- bin/gstack-egress | 10 ++++++++++ test/gstack-egress-cli.test.ts | 15 +++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/bin/gstack-egress b/bin/gstack-egress index 142815b71..4ff92ad0f 100755 --- a/bin/gstack-egress +++ b/bin/gstack-egress @@ -151,6 +151,7 @@ function egressGrants(args: string[], home: string): number { const syncMode = configGet('artifacts_sync_mode') || 'off'; const repoVisibility = configGet('redact_repo_visibility') || 'unknown'; const prepushHook = configGet('redact_prepush_hook') || 'false'; + const memorableRecall = configGet('memorable_recall') || 'off'; const grants: Grant[] = [ { @@ -189,6 +190,15 @@ function egressGrants(args: string[], home: string): number { key: 'redact_prepush_hook', revoke: 'gstack-config set redact_prepush_hook false (disables the guard)', }, + { + grant: 'memorable-recall', + value: memorableRecall, + granted: memorableRecall === 'on', + detail: 'Claude Code UserPromptSubmit hook hands each prompt to the third-party memorable CLI (receipted per prompt as sink memorable-recall; the vendor\'s own capture consent is separate)', + file: configFile, + key: 'memorable_recall', + revoke: 'gstack-memorable disable (or gstack-config set memorable_recall off)', + }, ]; if (args.includes('--json')) { diff --git a/test/gstack-egress-cli.test.ts b/test/gstack-egress-cli.test.ts index 7b0398375..5c8ade64d 100644 --- a/test/gstack-egress-cli.test.ts +++ b/test/gstack-egress-cli.test.ts @@ -108,10 +108,10 @@ describe('gstack-egress verify', () => { }); describe('gstack-egress grants', () => { - test('fresh home shows the four upstream grants off, each naming file and revoke command', () => { + test('fresh home shows the five standing grants off, each naming file and revoke command', () => { const r = run(['grants']); expect(r.code).toBe(0); - for (const grant of ['telemetry', 'brain-sync', 'redact_repo_visibility', 'redact_prepush_hook']) { + for (const grant of ['telemetry', 'brain-sync', 'redact_repo_visibility', 'redact_prepush_hook', 'memorable-recall']) { expect(r.stdout).toContain(grant); } expect(r.stdout).not.toContain('[GRANTED]'); @@ -142,6 +142,17 @@ describe('gstack-egress grants', () => { expect(sync.value).toBe('full'); const hook = grants.find((g: any) => g.grant === 'redact_prepush_hook'); expect(hook.granted).toBe(false); + // the Memorable bridge consent is a standing grant too: off by default, on only via gstack-memorable enable + const memo = grants.find((g: any) => g.grant === 'memorable-recall'); + expect(memo.granted).toBe(false); + expect(memo.key).toBe('memorable_recall'); + expect(memo.revoke).toContain('gstack-memorable disable'); + spawnSync(path.join(ROOT, 'bin', 'gstack-config'), ['set', 'memorable_recall', 'on'], { + encoding: 'utf-8', env: { ...process.env, GSTACK_HOME: home }, timeout: 30_000, + }); + const after = JSON.parse(run(['grants', '--json']).stdout).find((g: any) => g.grant === 'memorable-recall'); + expect(after.granted).toBe(true); + expect(run(['grants']).stdout).toContain('[GRANTED] memorable-recall: on'); }); });