Skip to content

feat: route access on the code-graph worklist, boundary suggestions and checks, boundary claims in the hypothesis ledger - #50

Merged
Animesh-Sri-bugb merged 5 commits into
mainfrom
fm/fx-guardlink-worklist-boundaries
Oct 1, 2026
Merged

Animesh-Sri-bugb merged 5 commits into
mainfrom
fm/fx-guardlink-worklist-boundaries

Conversation

@Animesh-Sri-bugb

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

Copy link
Copy Markdown
Contributor

Rebased onto main after #49 (SARIF enrichment) merged, so the diff below is only this PR's five commits. The two PRs touched some of the same files; the combined tree is what was tested.

What

A @boundary claims that trust changes between two sides, and its description says what holds the line. Until now nothing checked one. This PR has the optional code graph inform boundaries and check them, and lets the hypothesis ledger record the result of testing one.

  1. Route access levels on the worklist. loadWorklist asks the code graph's codegraph_auth_boundary in the same process as the attack surface and the routes. The query is an optional call: a server that does not publish it still yields the worklist. Each entry gains access[] (level public | authenticated | elevated | unknown, basis, unknown_reason, a summary of the evidence, the guard text with its file:line, and caveats). The worklist gains auth_verdict, access_withheld_reason and resolver_density. Levels are carried only under the classified verdict. Any other verdict withholds them and the prompt says why, and unknown is never rendered as public. guardlink_worklist returns these fields too.

  2. Boundary suggestions in the annotate prompt. For the rows it shows, the worklist block adds Boundaries the graph suggests:

    • a caller boundary per classified route, naming the guard the graph cites as what enforces it;
    • a data, process, egress or filesystem boundary per sink class the handler reaches (data_layer, exec, network, filesystem), anchored at the example sink.
  3. Boundary checks. guardlink validate --code-graph, and guardlink_validate with code_graph: true, emit four warnings:

    • boundary-access-contradicted: a boundary, or an @assumes on its inner asset, says "authenticated" or "admin", and a route on the inner side is classified below that;
    • boundary-access-unknown: the same claim, on a route the graph could not classify;
    • boundary-missing: a route handler reaches a sink, and no asset its file names has a boundary to the outside;
    • boundary-unused: no declared flow crosses the boundary, and no recognised entry point reaches either side.

    The outer side is inferred the way guardlink paths infers entries: an undeclared endpoint, or an External.* asset. A boundary between two inside assets has no known direction, so the access checks skip it.

  4. Boundary claims in the hypothesis ledger.

    • guardlink hypothesis contradict|support <key | #id | file:line> --evidence … records an outcome. A contradiction is held to the same evidence bar as a confirmation.
    • guardlink hypothesis boundaries [--json] lists every boundary's state (guardlink.boundary-claims/v1): unverified, supported, contradicted, or retest. Outcomes expire with the code, the way exposure outcomes do.
    • confirm --from-scan records a finding whose claim key names a @boundary as that boundary's outcome. It also reads boundary_claim_key/boundaryClaimKey placed beside an exposure's key, and then records both.
    • A boundary key on its own never falls through to the coarse exposure joins.

Compatibility

  • No graph: the annotate prompt is byte-identical, as the existing test asserts for every no-graph status and playbook. validate without the flag and guardlink_validate without code_graph are unchanged.
  • A graph without codegraph_auth_boundary: the rendered worklist is byte-identical. A test asserts this, for MCP and bravos stand-ins alike.
  • SARIF: untouched. None of the boundary checks become results.
  • Ledger:
    • the schema stays guardlink.hypotheses/v1, and boundary and exposure outcomes share entries, told apart by their outcome;
    • the reader refuses an entry whose history mixes the two kinds;
    • an older GuardLink reads a ledger that holds a boundary outcome as corrupt, so it refuses to write rather than dropping the entry.
  • Behaviour change: in --from-scan, a finding stamped with a boundary's claim key used to be reported stale. It is now recorded as that boundary's outcome.

Review notes

  • statedAccess reads the claimed level from the description's words (login/authenticated/API key → authenticated; admin/role/permission → elevated). It skips text that says the line is open ("unauthenticated", "anonymous", "no login"). It is a heuristic, so every warning quotes the word it matched.
  • A route belongs to an asset when its handler's file names that asset. Flows are not used for this, because a flow line names both of its ends. An outer-facing boundary attributes only its inner side.

Verification (local; CI did not run)

  • On main (which now includes feat(sarif): carry each finding's declared context, additively; rewrite SPEC §6 #49) plus this branch: npm test reports 114 files, 2043 passed, 1 skipped. The skipped test is the live-graph test, which needs real graph tooling. npm run lint and tsc --noEmit are clean.
  • New tests:
    • tests/boundary-check.test.ts (12): every check, the verdict gates, the no-graph path, and validate --code-graph through the real CLI against a stand-in codegraph-mcp;
    • tests/hypothesis-boundary.test.ts (13): resolving targets, evidence rules, expiry, reader validation, every scan-import route, and the CLI round trip;
    • tests/codegraph.test.ts: access levels, withheld verdicts, the unchanged worklist when the query is unsupported, suggestions, both transports, and the MCP surfaces.
  • guardlink validate . --artifacts: current and drawable. guardlink diff origin/main: +4 flows. Two exposures moved down a few lines because the doc-block grew; the risk is unchanged.
  • tests/query-views.test.ts was re-measured in Chrome, not bumped. I rendered both diagrams with mermaid@11 under the dashboard's options at 1440×900: filtered {34 nodes, 89 links, 8 clusters}, whole model {48 nodes, 180 links}. The counter agrees.
  • Not verified against a real graph build. The graph tooling installed locally predates the route and attack-surface queries. The transports run against stand-in executables, and the fixtures follow the shape of codegraph_auth_boundary's answer (verdict, verdict_note, resolver_density, and routes[].access with level, basis, unknown_reason, summary, evidence[] and caveats[]).

…t boundaries from them

The worklist now asks codegraph_auth_boundary in the same graph process as
the attack surface and the routes, as an optional call: a graph that does not
publish it still yields the worklist, unchanged. Each entry carries the level
of every route the graph classified (public, authenticated, elevated, or
unknown with its reason), with the basis and the guard text it rests on.
Levels are carried only under the classified verdict; any other verdict
withholds them and carries the graph's note, and unknown always renders as
unknown, never as public.

The annotate prompt's worklist block shows the level beside each route and
adds a 'Boundaries the graph suggests' section for the rows it shows: a
caller boundary per classified route, naming the cited guard as what
enforces it, and a data, process, egress or filesystem boundary per sink
class the handler reaches, anchored at the example sink. Without a graph
the prompt is byte-identical, as before.
'guardlink validate --code-graph' and guardlink_validate with code_graph
compare each @boundary with what the graph measured, as warnings:

- boundary-access-contradicted: the description (or an @assumes on the
  inner asset) states a login or an admin check, and a route on the inner
  side is classified below it;
- boundary-access-unknown: the same claim where the graph could not decide
  a route's level, reported unknown with its reason, never as public;
- boundary-missing: a route handler reaches a classified sink and no asset
  its file names declares a boundary to the outside;
- boundary-unused: no declared flow crosses the boundary and no entry point
  the graph recognises reaches either side.

The outer side is inferred as 'guardlink paths' infers entries: an
undeclared endpoint, or a declared External.* asset. A boundary between two
inside assets has no known direction, so the access checks skip it. A
route belongs to an asset when its handler's file names the asset. The
access checks need the graph's classified verdict; the others need only a
usable worklist. Without the flag nothing is consulted and the output is
unchanged. None of this reaches the SARIF export.
…cted

A @boundary is a claim a probe can test from its outer side. The hypothesis
ledger now records that outcome against the boundary's claim key:

- 'guardlink hypothesis contradict|support <target>' records it, with
  evidence; a contradiction is held to the confirmation's bar. A target is
  the claim key, the #id, or the file:line of the @boundary.
- 'guardlink hypothesis boundaries [--json]' lists every boundary as
  unverified, supported, contradicted or retest
  (guardlink.boundary-claims/v1).
- Outcomes expire with the code as exposure outcomes do: supported lapses
  to unverified, contradicted asks for a retest.
- 'confirm --from-scan' records a finding whose claim key names a boundary
  as that boundary's outcome (it used to report it stale), and reads
  boundary_claim_key / boundaryClaimKey beside an exposure's key, recording
  both. A boundary key alone never falls to the coarse exposure joins.
  'contradicted' unless the finding says boundary_outcome: supported.

Boundary and exposure outcomes share the entries list; the outcome says
which kind an entry is, and the reader refuses an entry that mixes them. A
boundary outcome never moves an exposure's state and nothing is written to
source for one. A ledger holding one reads as corrupt to an older
GuardLink, which refuses to write it rather than dropping the entry.

Also documents the worklist access levels, boundary suggestions and
'validate --code-graph' in the README, the reference and the changelog.
…e after the boundary-check flow

The boundary checks add one flow, #codegraph -> #cli, which draws one more
edge. Rendered both diagrams with mermaid@11 under the dashboard's initialize
options at a confirmed 1440x900 viewport and counted the SVG: filtered
{nodes:34, links:89, clusters:8}, whole model {nodes:48, links:180}. Chrome
and measureLegibility agree on both pairs.
@Animesh-Sri-bugb
Animesh-Sri-bugb force-pushed the fm/fx-guardlink-worklist-boundaries branch from 5f23550 to 949319f Compare October 1, 2026 19:29
@Animesh-Sri-bugb
Animesh-Sri-bugb merged commit fb0c56d into main Oct 1, 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