Skip to content

gbrain context-load: list-query filter: blocks in SKILL.md manifests are silently dropped, causing unfiltered cross-repo queries at skill startup #1687

Closed

Description

@officialasishkumar

Summary

parseSkillManifest (the runtime read path in lib/gstack-memory-helpers.ts) parses every field of a gbrain.context_queries item except filter:. As a result, any kind: list query that declares a filter: block in its SKILL.md manifest runs unfiltered at skill startup, pulling pages of every type from every repo into the agent's context instead of the repo scoped set the manifest asked for.

This silently defeats the F7 design intent documented in bin/gstack-brain-context-load.ts (lines 18-20): "every default query carries an explicit repo: filter so cross-repo contamination is the non-default path."

Affected skills

Four shipped skills declare a filter: block on a kind: list query in their generated SKILL.md (the file the runtime actually reads):

  • investigate/SKILL.md (prior-investigations: type: timeline, tags_contains: "repo:{repo_slug}", content_contains: "investigate")
  • office-hours/SKILL.md
  • plan-ceo-review/SKILL.md
  • design-consultation/SKILL.md

Current behavior on upstream main

On origin/main (920a13a1):

bun -e '
import { parseSkillManifest } from "./lib/gstack-memory-helpers.ts";
const m = parseSkillManifest("investigate/SKILL.md");
const q = m.context_queries.find(q => q.id === "prior-investigations");
console.log("kind:", q.kind, "| sort:", q.sort, "| limit:", q.limit);
console.log("filter:", JSON.stringify(q.filter));
'

prints:

kind: list | sort: updated_at_desc | limit: 5
filter: undefined

The manifest declares:

    - id: prior-investigations
      kind: list
      filter:
        type: timeline
        tags_contains: "repo:{repo_slug}"
        content_contains: "investigate"
      sort: updated_at_desc
      limit: 5
      render_as: "## Prior investigations in this repo"

but filter comes back undefined. Downstream, dispatchList in bin/gstack-brain-context-load.ts only adds --filter args when q.filter is set:

if (q.filter) {
  for (const [k, v] of Object.entries(q.filter)) {
    const { resolved: rv } = substituteTemplateVars(String(v), args);
    cliArgs.push("--filter", `${k}=${rv}`);
  }
}

So the query that ships is gbrain list_pages --limit 5 --sort updated_at_desc with no filter, returning the 5 most recently updated pages of any type from any repo, then wrapping and injecting them into the agent's startup context.

Root cause

extractGbrainBlock in lib/gstack-memory-helpers.ts (around lines 388-410) matches id, kind, render_as, query, limit, glob, sort, and tail, but never filter. The GbrainManifestQuery.filter field exists in the type, defaultManifest populates it, and dispatchList consumes it. Only the manifest parser fails to read it, so a declared filter is dropped on the floor.

This is worse than a skip: the query still "succeeds", so no (unavailable) is rendered. Wrong, cross-repo data is injected with no signal that anything went wrong.

Expected behavior

parseSkillManifest should return q.filter populated from the filter: sub-block, so dispatchList emits the declared --filter k=v args and the query stays repo scoped, matching the behavior of the built in defaultManifest queries.

For the example above:

filter: {"type":"timeline","tags_contains":"repo:{repo_slug}","content_contains":"investigate"}

Notes

The fixtures in test/gstack-memory-helpers.test.ts ("parses a multi-kind manifest", "ignores incomplete query items") contain no filter: block, so filter parsing is entirely uncovered by tests today.

Activity

  1. added a commit that references this issue on May 24, 2026
    ea0f0fe
  2. xg-gh-25 commented on May 25, 2026

    @xg-gh-25

    The Bug Is Real (Reproduction + Fix Path)

    Confirmed on 920a13a1. parseSkillManifest in lib/gstack-memory-helpers.ts reads every gbrain.context_queries field except filter:, causing all kind: list queries with filter declarations to run unfiltered at skill startup. Silent cross-repo contamination with no (unavailable) signal.

    Reproduction (Copy-Paste)

    # On main branch (920a13a1)
    bun -e '
    import { parseSkillManifest } from "./lib/gstack-memory-helpers.ts";
    const m = parseSkillManifest("investigate/SKILL.md");
    const q = m.context_queries.find(q => q.id === "prior-investigations");
    console.log("Declared in SKILL.md:");
    console.log("  filter: { type: timeline, tags_contains: repo:{repo_slug}, ... }");
    console.log("\nParsed by runtime:");
    console.log("  filter:", q.filter);  // → undefined
    '

    Expected: filter: { type: "timeline", tags_contains: "repo:{repo_slug}", ... }
    Actual: filter: undefined

    Why This Is Worse Than A Skip

    dispatchList in bin/gstack-brain-context-load.ts only adds --filter args when q.filter is truthy:

    if (q.filter) {  // ← always false when parseSkillManifest drops it
      for (const [k, v] of Object.entries(q.filter)) {
        const { resolved: rv } = substituteTemplateVars(String(v), args);
        cliArgs.push("--filter", `${k}=${rv}`);
      }
    }

    Result: gbrain list_pages --limit 5 --sort updated_at_desc (no --filter repo=X) → pulls from all repos, wraps results in "## Prior investigations in this repo", injects into agent context. Agent believes it's looking at repo-scoped history when it's actually seeing random cross-repo pages.

    Affected Skills (Production Impact)

    4 shipped skills declare filter: on kind: list queries:

    • investigate/SKILL.md (prior-investigations)
    • office-hours/SKILL.md
    • plan-ceo-review/SKILL.md
    • design-consultation/SKILL.md

    All silently pull unfiltered data today.

    Fix Path

    extractGbrainBlock (lines 388-410) needs one addition:

    // Current: matches id, kind, render_as, query, limit, glob, sort, tail
    // Missing: filter
    
    if (match = line.match(/^\s+filter:\s*$/)) {
      currentQuery.filter = extractYamlObject(lines, i + 1);  // parse nested YAML
      continue;
    }

    Then the filter actually flows:

    1. SKILL.md declares filter: → parseSkillManifest reads it → dispatchList emits --filter args → gbrain query stays repo-scoped

    Memory context leakage is a memory sovereignty violation — the skill manifest is the contract, runtime must honor declared filters or fail loudly. Built with SwarmAI. Discussion: T-MEM

  3. added a commit that references this issue on Jul 13, 2026
    8966fbc
  4. time-attack commented on Jul 15, 2026

    @time-attack
    Contributor

    @16francej Please close this issue as resolved. The GBrain context loader now reads nested filter settings from skill manifests and applies them to the query, preventing the unfiltered cross-repository search reported here. This shipped in #2264.

  5. capy-ai commented on Oct 3, 2026

    @capy-ai
    Contributor

    parseSkillManifest("investigate/SKILL.md") now returns the nested filter ({"type":"timeline","tags_contains":"repo:{repo_slug}","content_contains":"investigate"}) instead of undefined, so list queries stay repo-scoped.

    Fixed by #2264 (on main since v1.60.1.0).
    If you still see this on the latest gstack, please reopen with details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions