severity: security (defense-in-depth)
Observed problem
bin/gstack-learnings-search --cross-project is meant to be an allowlist: per the inline comment, cross-project learnings are "only loaded if trusted (user-stated)" to prevent prompt injection from one project's AI-generated learnings silently influencing reviews in another project.
The gate is implemented as a denylist instead:
if (isCrossProject && e.trusted === false) continue;
Any cross-project row where trusted is missing/undefined (not literally false) is admitted, because undefined === false is false. So legacy rows written before the trusted field existed, hand-edited rows, and rows produced by other tools all bypass the gate.
The trusted field was added in the security wave 3 (#988, 2026-04-13), while gstack-learnings-search shipped earlier (#622, 2026-03-29). Any learnings.jsonl written in that window — or by anything that does not set the field — has rows with no trusted key. Those rows leak across projects today.
Current behavior on upstream main (a6fb317)
A foreign project row with no trusted field and source: observed is loaded and shown as [cross-project]:
$ # foreign learnings.jsonl row, no trusted field:
$ # {"ts":"2026-05-09T00:00:00Z","type":"pattern","key":"foreign-legacy","insight":"INJECTED legacy guidance with no trusted field","source":"observed"}
$ gstack-learnings-search --cross-project --query INJECTED
LEARNINGS: 1 loaded (1 pattern)
## Patterns
- [foreign-legacy] (confidence: 5/10, observed, 2026-05-09) [cross-project]
INJECTED legacy guidance with no trusted field
Expected behavior
The gate should fail closed: a cross-project row is admitted only when trusted === true. Rows missing the field are treated as untrusted. Current-format rows are unaffected (user-stated rows carry trusted: true and still load; everything else carries trusted: false and is still excluded).
Duplicate searches performed
Candidate fix shape
Change the cross-project trust check from a denylist (e.trusted === false) to an allowlist (e.trusted !== true), and add a regression test for a foreign row with no trusted field.
severity: security (defense-in-depth)
Observed problem
bin/gstack-learnings-search --cross-projectis meant to be an allowlist: per the inline comment, cross-project learnings are "only loaded if trusted (user-stated)" to prevent prompt injection from one project's AI-generated learnings silently influencing reviews in another project.The gate is implemented as a denylist instead:
Any cross-project row where
trustedis missing/undefined(not literallyfalse) is admitted, becauseundefined === falseisfalse. So legacy rows written before thetrustedfield existed, hand-edited rows, and rows produced by other tools all bypass the gate.The
trustedfield was added in the security wave 3 (#988, 2026-04-13), whilegstack-learnings-searchshipped earlier (#622, 2026-03-29). Anylearnings.jsonlwritten in that window — or by anything that does not set the field — has rows with notrustedkey. Those rows leak across projects today.Current behavior on upstream
main(a6fb317)A foreign project row with no
trustedfield andsource: observedis loaded and shown as[cross-project]:Expected behavior
The gate should fail closed: a cross-project row is admitted only when
trusted === true. Rows missing the field are treated as untrusted. Current-format rows are unaffected (user-statedrows carrytrusted: trueand still load; everything else carriestrusted: falseand is still excluded).Duplicate searches performed
bin/gstack-learnings-search/bin/gstack-learnings-log: none target the trust-gate semantics. fix: normalize legacy learnings schema (GXG-872) #940 (stale, 2026-04-09) normalizes legacycategory/summary/detailfield names, not the trust allowlist; feat: make learnings search limits configurable via gstack-config #1514 is about configurable search limits.Candidate fix shape
Change the cross-project trust check from a denylist (
e.trusted === false) to an allowlist (e.trusted !== true), and add a regression test for a foreign row with notrustedfield.