Repository navigation
feat(reach): show agent reach in the report and on the dashboard - #55
Merged
Merged
Conversation
`@agents`, `@reaches`, `@effects` and `@gates` reach `lookup`, `diff`, the model JSON (`reach_analysis`) and the pentest SARIF, but nothing a person reads drew them: the report and the dashboard ignored all four. This adds one derived view, `src/reach`, and draws it on both surfaces so they cannot disagree. It adds no join of its own: unentitled reaches come from `findUnentitledReaches` and gating from `classifyEffects`, the same answers `reach_analysis` and the SARIF export carry. What the view holds, for every actor that reaches something: - the reach map: actor x asset, with the capabilities exposed on the asset and the effects bound to the same code as the actor's reach. An effect with no reach bound to its code goes in a "not tied to a reach" row rather than a row the model never drew; - unentitled reaches, each near miss with the reason it does not cover; - ungated mutations, each gate near miss with its reason; - gates, egress from a reached surface (a `@flows` that leaves the model or crosses a `@boundary`), and injection-to-tool routes; - the OWASP Top 10 for LLM Applications rows, keyed on `@agents`: LLM06 (unentitled reach, a broader execution identity with no gate, an ungated mutation), LLM01 (outside input that flows into an agent with an ungated mutation) and LLM05 (a mutating effect bound to the agent tool's code). Surfaces: - `guardlink report` gains an "Agents and LLM Reach" section when any reach annotation exists, plus three summary rows; a model without them reports byte-for-byte as before. - `guardlink report --agents` (and `guardlink_report` with `agents: true`) writes the same section as its own document, threat-model-agents.md. - The dashboard gains an Agents & Reach page with an empty state, an Agent Reach diagram tab, an Actors table on Data & Boundaries, a Reach block in the asset drawer, an AI-agent mark on agent tiles, and "What to do next" items for unentitled reaches and ungated mutations. - `guardlink threat-report` hands the LLM the reach claims and the derived lists as `agent_reach`, and the prompt describes the four verbs. Tests: tests/agent-reach-surfaces.test.ts reads tests/fixtures/support-desk, the fixture the reach export and pentest SARIF are pinned on, and pins the derived view, the report section, the agent-only report and the reach diagram as golden files beside it. It checks that the view agrees with `reach_analysis`, covers injection, egress and a broader identity on a small inline app, and covers the empty states, escaping on the dashboard and the `--agents` CLI path. SARIF and the JSON export are unchanged.
…ach diagram Rendered from tests/fixtures/support-desk with `guardlink dashboard`.
Animesh-Sri-bugb
force-pushed
the
fm/gl-reach-surfaces
branch
from
October 2, 2026 08:17
37024d6 to
b8750ed
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
@agents,@reaches,@effectsand@gatesalready reachlookup,diff, the model JSON (reach_analysis) and the pentest SARIF. The report and the dashboard ignored them. This draws them where people read the model, from one derived view (src/reach/summary.ts), so the two surfaces cannot disagree. It adds no join of its own: unentitled reaches come fromfindUnentitledReachesand gating fromclassifyEffects, the same answersreach_analysisand the SARIF export carry. A test checks the view's counts and ungated list againstbuildReachAnalysis.Dashboard: Agents & Reach page
@entitles) and the effects the actor's tools have on it (gated / no gate / read). Agents and other principals are marked. Effects that no reach is tied to get their own row.uncited,no-asset,other-asset).@effectsother thanreadwith no@gatesin front of it.@flowsout of a reached surface that leaves the model or crosses a@boundary), and OWASP Top 10 for LLM Applications rows: LLM06, LLM01 and LLM05.Report
guardlink reportgains an Agents and LLM Reach section, plus three Executive Summary rows, whenever any reach annotation exists. The section has, per actor, each capability, whether it is entitled and what it leads to; the reach map as a table; the lists above; open exposures on reached assets; and the OWASP mapping. A model with no reach annotations reports exactly as before.guardlink report --agents(andguardlink_reportwithagents: true) writes the same section as its own document,threat-model-agents.md, so a team can publish an LLM threat model alone or as part of the full one. Example:golden/threat-model-agents.md.guardlink threat-report(both the CLI and MCP serialisers) passes the reach claims and the derived lists to the LLM asagent_reach. The prompt describes the four verbs and asks for an agents section mapped to the OWASP rows.How effects are tied to an actor
An effect lands in an actor's row only when a reach of that actor is bound to the same code (
boundCode: one doc-block on one declaration). Whether it is gated isclassifyEffects. An effect with no reach bound to its code goes in a "not tied to a reach" row. In the fixture, that's the nightly refund sweep (ungated: the issue-refund gate is capability-scoped and nothing says which capability reaches the sweep) and the reviewed email (gated by an unscoped gate).OWASP rows (these key on
@agentsonly, and each is a target for review or test, not a finding):@flowssource that is neither a declared asset nor an actor, and has nothing flowing into it, reaches an agent that has an ungated mutation.SPEC §3.2.1 gains a Where they are read paragraph that says this.
Not changed
SARIF and the JSON export are unchanged.
.guardlink/model.jsonmoved only by content (new files, line numbers, the annotation hash), not by shape.Tests
tests/agent-reach-surfaces.test.ts(21 tests) readstests/fixtures/support-desk, the fixture the reach export and the pentest SARIF are already pinned on, unchanged apart from a FIXTURE.md paragraph. It pins four golden files ingolden/beside it: the derived view (reach-summary.json), the report section, the agent-only report and the reach diagram.reach_analysis, the per-actor placement and the "not tied to a reach" row, and the entitlement and gate near-miss reasons. It checks that LLM rows stay off the CI principal and that the main report is unchanged without reach annotations. It also covers the feature-slice suffix, the threat-report serialisation, the dashboard page, diagram tab, actor table, asset drawer data, actions and empty state, HTML escaping, andreport --agentsthrough the real CLI.Verified:
npm run build && npm teston the rebased branch: 120 files, 2212 passed, 1 skipped.npm run lint: clean.guardlink validate . --artifacts: artifacts current and drawable. Artifacts were regenerated in their own commit.The last commit only hosts the two screenshots above (
docs/images/). Drop it before merging if they should not live in the repository.