Skip to content

feat(reach): export reach_analysis in the model JSON and agent-reach results in the pentest SARIF - #54

Merged
Animesh-Sri-bugb merged 2 commits into
mainfrom
fm/gl-reach-export
Oct 2, 2026
Merged

Animesh-Sri-bugb merged 2 commits into
mainfrom
fm/gl-reach-export

Conversation

@Animesh-Sri-bugb

Copy link
Copy Markdown
Contributor

What

PR #53 added @agents, @reaches, @effects and @gates to the parsed model, and listed SARIF and consumer work as follow-ups. This PR is that follow-up. It puts what the verbs declare into the outputs other tools read: the model JSON, guardlink_lookup and the SARIF pentest profile. All three key each claim by the same claim key, so a consumer can join them.

1. reach_analysis in the model JSON (new SPEC §5.5)

guardlink parse, report --format json, .guardlink/model.json, MCP guardlink_parse and the guardlink://model resource now carry a derived block:

"reach_analysis": {
  "version": 1,
  "summary": { "reaches": 7, "agent_reaches": 6, "unentitled_reaches": 5, "effects": 9, "mutating_effects": 7, "ungated_effects": 5, "gates": 2 },
  "unentitled_reaches": [ { "index", "claim_key", "file", "line", "verb", "actor", "agent", "capability", "canonical_capability", "asset", "identity", "description", "near_misses": [ … "blocker" ], "colocated_effects": [ … ] } ],
  "mutating_effects":   [ { "index", "claim_key", "file", "line", "effect", "asset", "identity", "description", "gated", "gates": [ … ], "gate_near_misses": [ … "blocker" ], "colocated_reaches": [ … ] } ]
}
  • Each row points at its claim in two ways: index into the array next to it, and claim_key.
  • The block is written only when the model declares a reach, effect or gate. A model without these verbs exports the same bytes as before, and the annotation hash does not change in either case.
  • version is bumped when a key is renamed or removed, or when a rule changes what a row means. Adding a key does not bump it.

2. Gated and ungated effects (SPEC §3.2.1)

A gate covers an effect on its asset when either of these holds:

  • the gate names no capability; or
  • every reach bound to the effect's code is the capability the gate names. "Bound to the same code" means the same file and the same structure-layer anchor, which is what writing the claims in one doc-block gives you.

When no reach is bound to the effect's code, nothing in the model says which capability reaches the effect, so a capability-scoped gate does not cover it. Its near miss reports capability-unknown. The other blocker is other-capability. Both rules err toward reporting the effect, the same way the can-minus-may join does.

Lookup gains a new form, ungated effects [for <asset>], and unentitled reaches rows now carry claim_key.

3. guardlink/agent-reach in the pentest SARIF profile (SPEC §6.8)

The profile now has one more rule, appended after guardlink/boundary-claim. It adds one kind: "review", level: "none" result for each @agents/@reaches claim, then one for each @effects that is not read, appended after the boundary claims. properties carries:

  • subject (reach or effect)
  • the actor, the agent flag, the capability, the asset and the identity
  • the effect, gated, gates and gate_near_misses
  • the claims bound to the same code

relatedLocations points at the gates and at the bound claims.

Please review this choice: the list is complete, not only unentitled reaches and ungated effects. Two existing rules force that:

  • §6.1 says nothing in SARIF may be derived from @entitles, and tests/actor-entitlement.test.ts pins this for the pentest profile too. If only unentitled reaches got results, adding an @entitles would remove a result, and that is the mechanism §6.1 rules out.
  • §3.2.1 says a gate suppresses nothing.

So an entitled reach gets a result just like an unentitled one, and nothing on that result says whether it is entitled. A gated effect also stays in the list, with its gates, because the gate is what a probe should try to get past. A consumer that wants the unentitled or ungated subset joins on claimKey to reach_analysis. A new test checks that the pentest export is byte-identical with and without entitlements on the reach fixture.

The github profile is unchanged, byte for byte. The three existing pentest fixtures change only by the added rule.

Also

  • .guardlink/model.json now writes reaches, effects and gates in canonical order, like every other relation. feat(reach): declare what an embedded agent can reach with @agents, @reaches, @effects and @gates #53 left them in scan order.
  • The guardlink sarif --profile pentest summary line reports the agent-reach count when it is non-zero.
  • SPEC §3.2.1, §5.1, §5.5 (new), §6.1 and §6.8, docs/GUARDLINK_REFERENCE.md (new section Reach in the exports), the lookup tool description, the instruction template's lookup list and CHANGELOG are updated.

Tests

  • New fixture tests/fixtures/support-desk/, with its pentest export pinned in tests/fixtures/sarif-pentest/support-desk.sarif. It has five unentitled reaches, covering each near-miss reason, and seven mutating effects: one gated by a scoped gate, one gated by an unscoped gate, and five ungated, one of them with a capability-unknown near miss. FIXTURE.md tabulates them.
  • tests/reach-export.test.ts (new, 13 tests) covers:
    • the block's version, summary and row shapes;
    • indexes and claim keys resolving to their claims;
    • the gating rules, including other-capability;
    • byte-identical output and an unchanged annotation hash for models without the verbs;
    • stale-block replacement;
    • each surface: CLI parse, report --format json, model.json indexed into the ordered arrays, MCP guardlink_parse, and lookup.
  • tests/sarif-pentest.test.ts gains an agent-reach block covering:
    • result order, and that the github prefix is intact;
    • reach and effect properties;
    • claim-key joins to reach_analysis;
    • entitlement invariance;
    • removing every gate changes gated but never the result count;
    • the github profile is unchanged by every reach verb.
  • The existing github-profile byte-identity tests still pass.

One note for reviewers: the parser reads files in fast-glob's traversal order. That order is not stable across directories, so a multi-directory project's array order, and with it its SARIF result order, can differ between runs. I put the new fixture's sources in one directory and did not change the parser, because changing it would move result order everywhere.

Verification

  • npm run build && npm test: 119 files, 2191 passed, 1 skipped
  • npm run lint: clean
  • node dist/cli/index.js validate . --artifacts: artifacts current and drawable, validation passed with the same 16 unmitigated exposures as before
  • guardlink diff HEAD~2: one added flow (ThreatModel → #sarif via agentReachResults), risk unchanged

The second commit regenerates the synced instruction files and graph artifacts.

…results in the pentest SARIF

The reach verbs (@agentS, @reaches, @effects, @gates) were in the parsed
model, but the two joins a consumer needs from them, can-minus-may and
whether an effect is gated, were only reachable one lookup at a time, and
SARIF carried none of it.

Model JSON (SPEC §5.5). `guardlink parse`, `report --format json`,
`.guardlink/model.json`, MCP `guardlink_parse` and `guardlink://model` now
carry a derived `reach_analysis` block: `version: 1`, a `summary`,
`unentitled_reaches` with each near miss and its reason, and every
mutating effect with `gated`, the gates that cover it and the gates on
its asset that do not. Every row names its claim by index into the arrays
beside it and by claim key. The block is written only when the model
declares a reach, effect or gate, so other models export the same bytes;
the annotation hash never moves.

Gating (SPEC §3.2.1). A gate covers an effect on its asset when it names
no capability, or when every reach bound to the effect's code (same file
and structure-layer anchor, i.e. one doc-block) is the capability it
names. With no reach bound there, a scoped gate does not count
(`capability-unknown`). New lookup form `ungated effects [for <asset>]`;
`unentitled reaches` rows gain `claim_key`.

SARIF (SPEC §6.8). The pentest profile appends one `guardlink/agent-reach`
review result per reach and per non-read effect, after the boundary
claims, with the actor, agent flag, capability, asset, effect, `gated`
and gates in `properties`. The list is complete on purpose: §6.1 forbids
any result derived from @entitles, and a gate suppresses nothing, so the
unentitled and ungated subsets come from `reach_analysis`, joined on the
claim key. The github profile is unchanged byte for byte; the three
existing pentest fixtures gain the rule only.

Also: `reaches`, `effects` and `gates` are canonically ordered in
`.guardlink/model.json` like every other relation. The new support-desk
fixture keeps its sources in one directory because the parser reads
files in traversal order, which is not stable across directories.

Verified: npm run build && npm test (119 files, 2191 passed, 1 skipped),
npm run lint, guardlink validate . --artifacts.
@Animesh-Sri-bugb
Animesh-Sri-bugb merged commit b8043a8 into main Oct 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant