From dd1f942c163085ef1bf7fcec6d7f3af57ef2e27a Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 24 Oct 2026 20:35:11 +0000 Subject: [PATCH] fix(repo-policy): reserve only the two metadata keys, so the _unattributed sentinel stays a valid policy key Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- CHANGELOG.md | 2 +- bin/gstack-gbrain-repo-policy | 11 ++++++----- test/gbrain-repo-policy.test.ts | 5 ++++- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c87cc8d36..7d83c6c75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ **A read-only repo can now opt in to Memorable recall, one remote at a time, and a corrupt policy store no longer turns the recall veto off.** #3359 (exGeni) asked for a way to keep a repo out of the brain while still letting its prompts reach Memorable: today the recall hook skips every `deny` and `read-only` repo, and `read-only` is also the tier people pick just to stop code ingest. Garry approved a per-remote flag on 2026-10-24: off by default, and a `deny` always wins. While building the negative controls we found that the hook's policy lookup went through `get`, which quarantines a corrupt store and answers "unset", so a truncated `gbrain-repo-policy.json` let recall run in every repo, read-only and deny included, and reset the store on the way. ### Added -- **`bin/gstack-gbrain-repo-policy memorable set `** stores the opt-in under a reserved `_memorable_recall` map in the policy store. It is refused unless the remote is read-only (deny vetoes recall; read-write never needed it). `memorable get []` prints `\t`, and `list` adds a `memorable=allow` column. Changing a remote's tier clears its opt-in, and keys starting with `_` are refused as remotes. +- **`bin/gstack-gbrain-repo-policy memorable set `** stores the opt-in under a reserved `_memorable_recall` map in the policy store. It is refused unless the remote is read-only (deny vetoes recall; read-write never needed it). `memorable get []` prints `\t`, and `list` adds a `memorable=allow` column. Changing a remote's tier clears its opt-in, and the two metadata keys (`_schema_version`, `_memorable_recall`) are refused as remotes. - **`lib/gbrain-repo-policy-client.ts`**: `memorableRecallPolicy(url)` returns the tier and the opt-in in one spawn. Anything but exactly `\t` reads as `unreadable`. ### Changed diff --git a/bin/gstack-gbrain-repo-policy b/bin/gstack-gbrain-repo-policy index 8df5fa1a0..e1b04378b 100755 --- a/bin/gstack-gbrain-repo-policy +++ b/bin/gstack-gbrain-repo-policy @@ -48,7 +48,8 @@ # "github.com/baz/qux": "deny", # "_memorable_recall": { "github.com/foo/bar": "allow" } # } -# Keys starting with "_" are reserved and never name a remote. +# The keys _schema_version and _memorable_recall are reserved; other keys +# name remotes (including the _unattributed sentinel memory-ingest uses). # # Tier semantics: # read-write — agent may search AND write new pages from this repo. @@ -216,11 +217,11 @@ memorable_optin() { # jq filter that removes $key's opt-in (and an emptied map). DROP_MEMORABLE_OPTIN='if (._memorable_recall | type) == "object" then (._memorable_recall |= del(.[$key])) | (if ._memorable_recall == {} then del(._memorable_recall) else . end) else . end' -# Keys starting with "_" hold store metadata (_schema_version, -# _memorable_recall); a remote that normalizes to one cannot be stored. +# _schema_version and _memorable_recall hold store metadata; a remote that +# normalizes to one of them cannot be stored. reject_reserved_key() { case "$1" in - _*) die "'$1' is a reserved key in the policy store, not a remote" ;; + _schema_version|_memorable_recall) die "'$1' is a reserved key in the policy store, not a remote" ;; esac } @@ -456,7 +457,7 @@ cmd_list() { fi ensure_file jq -r '(._memorable_recall | if type == "object" then . else {} end) as $m - | to_entries[] | select(.key | startswith("_") | not) + | to_entries[] | select(.key != "_schema_version" and .key != "_memorable_recall") | "\(.key)\t\(.value)" + (if $m[.key] == "allow" then "\tmemorable=allow" else "" end)' "$POLICY_FILE" | sort } diff --git a/test/gbrain-repo-policy.test.ts b/test/gbrain-repo-policy.test.ts index 9c0c9fc17..d8cb1b632 100644 --- a/test/gbrain-repo-policy.test.ts +++ b/test/gbrain-repo-policy.test.ts @@ -526,9 +526,12 @@ describe('memorable recall opt-in (#3359)', () => { expect(r.stderr).toContain('not a readable file'); }); - test('reserved keys are never stored as remotes', () => { + test('the two metadata keys are never stored as remotes; the _unattributed sentinel still is', () => { const r = run(['set', '_memorable_recall', 'read-write']); expect(r.status).toBe(2); expect(r.stderr).toContain('reserved key'); + expect(run(['set', '_schema_version', 'deny']).status).toBe(2); + expect(run(['set', '_unattributed', 'deny']).status).toBe(0); + expect(run(['list']).stdout).toBe('_unattributed\tdeny'); }); });