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>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-24 20:35:11 +00:00
1 parent 4732b9dd7a
commit dd1f942c16
3 files changed
+11 -7

No files matched your search

+1 -1
View File
@@ -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 <remote-url> <allow|off>`** 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 [<remote-url>]` prints `<tier>\t<allow|off>`, 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 <remote-url> <allow|off>`** 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 [<remote-url>]` prints `<tier>\t<allow|off>`, 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 `<tier>\t<allow|off>` reads as `unreadable`.
### Changed
+6 -5
View File
@@ -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
}
+4 -1
View File
@@ -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');
});
});