Skip to content

feat(reach): show agent reach in the report and on the dashboard - #55

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

Animesh-Sri-bugb merged 4 commits into
mainfrom
fm/gl-reach-surfaces

Conversation

@Animesh-Sri-bugb

@Animesh-Sri-bugb Animesh-Sri-bugb commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

What

@agents, @reaches, @effects and @gates already reach lookup, 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 from findUnentitledReaches and gating from classifyEffects, the same answers reach_analysis and the SARIF export carry. A test checks the view's counts and ungated list against buildReachAnalysis.

Dashboard: Agents & Reach page

Agents & Reach page, support-desk fixture

  • Reach map: actor × asset. Each cell holds the capabilities exposed on that asset (✓ entitled / ✗ no cited @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.
  • Unentitled reaches, with each near miss's reason (uncited, no-asset, other-asset).
  • Ungated mutations: an @effects other than read with no @gates in front of it.
  • Gates, egress from a reach (a @flows out of a reached surface that leaves the model or crosses a @boundary), and OWASP Top 10 for LLM Applications rows: LLM06, LLM01 and LLM05.
  • An empty state when the model has no reach annotations.
  • Elsewhere on the dashboard: an Agent Reach diagram tab, an Actors table on Data & Boundaries (AI agent / principal / approver), a Reach block in the asset drawer, an AI-agent mark on agent tiles in the heatmap, and "What to do next" items for unentitled reaches and ungated mutations.

Agent Reach diagram tab

Report

  • guardlink report gains 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 (and guardlink_report with agents: 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 as agent_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 is classifyEffects. 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 @agents only, and each is a target for review or test, not a finding):

  • LLM06 Excessive Agency covers three things: an unentitled reach (functionality); an effect that runs as a different identity than the agent presents, with no gate in between (permissions); and an ungated mutation the agent reaches (autonomy).
  • LLM01 Prompt Injection: a @flows source that is neither a declared asset nor an actor, and has nothing flowing into it, reaches an agent that has an ungated mutation.
  • LLM05 Improper Output Handling: a write, delete, execute or notify bound to the same code as the agent's tool.

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.json moved only by content (new files, line numbers, the annotation hash), not by shape.

Tests

  • tests/agent-reach-surfaces.test.ts (21 tests) reads tests/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 in golden/ beside it: the derived view (reach-summary.json), the report section, the agent-only report and the reach diagram.
  • It checks that the view agrees with 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, and report --agents through the real CLI.
  • That fixture declares no untrusted input, egress or broader identity, so LLM01, egress and the LLM06 permissions row are covered by a small inline app in the same file.

Verified:

  • npm run build && npm test on 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.
  • Rendered in Chrome in dark and light themes. No console errors. At 420px wide, the reach map scrolls inside its table wrapper.

The last commit only hosts the two screenshots above (docs/images/). Drop it before merging if they should not live in the repository.

`@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
Animesh-Sri-bugb merged commit 1c2da5d 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