Skip to content

feat(boundary): add a directed @boundary from <outer> to <inner> - #52

Merged
Animesh-Sri-bugb merged 2 commits into
mainfrom
fm/fx-boundary-direction-guardlink
Oct 1, 2026
Merged

Animesh-Sri-bugb merged 2 commits into
mainfrom
fm/fx-boundary-direction-guardlink

Conversation

@Animesh-Sri-bugb

Copy link
Copy Markdown
Contributor

What

@boundary gains an optional directed form, @boundary from <outer> to <inner>. The between, and and | forms are unchanged and stay undirected.

Why

A boundary was an undirected pair. A consumer could find its untrusted side only by inferring it from an endpoint that no @asset declares. That fails when both sides are declared assets, which is the usual case for an application and its database. In tests/fixtures/expense-api, the #api/#db data boundary in front of the SQL injection exports as basis: "unknown", and so does #data in sarif-shop. The directed form lets the author state the side.

Changes

  • Grammar and parser (src/parser/parse-line.ts). The directed form is read inline and from .gal sidecars. A line whose arguments start with from and do not parse is reported as malformed (@boundary from #a and #b, between #a to #b, #a to #b, from #a -> #b, and so on). from and to elsewhere in a line are not evidence, because they are common English words: @boundary is used to mark a trust change stays a prose warning.

  • Model. On a directed boundary, boundaries[] records directed: true, with asset_a as the outer side and asset_b as the inner side. Undirected boundaries get no new field. Their annotation hash and claim key are unchanged, so no hash version bump is needed and no existing ledger entry is re-keyed. Declaring, dropping or reversing a direction changes both, so a recorded test outcome does not carry over to a boundary whose direction changed.

  • SARIF. A declared side is basis: "declared". It is kept separate from the inferred undeclared-endpoint, which is unchanged for undirected boundaries, and from unknown. It appears in two places:

    • run.graphs[0]: in each boundary edge's guardlink/side. A directed edge is guardlink/directed: true, with sourceNodeId the outer side and targetNodeId the inner side.
    • The pentest profile's guardlink/boundary-claim results. Their inner_routes now also list routes into a declared inner side.
  • Validation (CLI, MCP and TUI validate). A directed boundary whose outer or inner side resolves to nothing is an error, unresolved-boundary-side. A side resolves if it is a #id that some @asset defines, or a name that is a declared asset path or a @flows endpoint. A repository-qualified #repo.id is left to the workspace merge. Undirected boundaries are not checked.

  • Other readers of boundaries.

    • guardlink diff reports a change of direction.
    • validate --code-graph checks a directed boundary against its declared inner side, including one between two declared assets, which it used to skip.
    • guardlink_lookup "boundary for X" and hypothesis boundaries --json include the direction.
  • Conformance corpus. The new conformance/boundaries.json covers:

    • directed, undirected, sidecar, malformed and prose lines;
    • the side the SARIF export states for each basis;
    • which directed boundaries validation rejects.

    tests/conformance-boundaries.test.ts runs it against both the parser and the exporter. It ships in the npm package next to flows.json, and conformance/README.md documents its format.

  • Agent material. The annotate prompt, the map playbook, the code-graph boundary suggestions and the generated agent instructions now tell agents to write the directed form when the code shows which side is less trusted, and between otherwise.

  • Docs. SPEC §3.2 @boundary (direction, malformed lines, validation, records), §2.12, §5.3, §6.6, §6.8, the quick reference and the EBNF; docs/GUARDLINK_REFERENCE.md; CHANGELOG; CONTRIBUTING.

Compatibility

  • github profile. Exports are byte-identical for any model without a directed boundary. tests/sarif-pentest.test.ts still matches both tests/fixtures/sarif-baseline/*.github.sarif baselines byte for byte. The graph description text changes only when a directed boundary exists.
  • pentest profile. The only change for existing models is the guardlink/boundary-claim rule's help.markdown, which now describes the three bases. expense-api.sarif and sarif-shop.sarif are regenerated for that one line. The new tests/fixtures/sarif-pentest/boundary-direction.sarif (from the new tests/fixtures/boundary-direction) contains all three bases.
  • sarif_profile_version stays 1. No member is added; "declared" is a new value of the existing basis, and it appears only for the new syntax. SPEC §6.6 tells older readers to treat any basis other than unknown as naming both sides.
  • This repository's own boundaries are left undirected. All of them have exactly one undeclared side, so inference already answers them, and converting them would move the pinned graph counts.

Verification

  • npm run build && npm test: 117 files, 2139 tests pass (1 skipped).
  • npm run lint and tsc --noEmit are clean.
  • guardlink validate . --artifacts, status . and ci . pass. guardlink sync and artifacts . were re-run, and the clean-tree check holds after them.
  • New tests:
    • tests/boundary-direction.test.ts: grammar, prose tiering, sidecars, hash and claim key, validation, lookup, diff, agent material.
    • tests/conformance-boundaries.test.ts.
    • A directed-boundary block in tests/sarif-pentest.test.ts, including schema validation.
    • A directed case in tests/boundary-check.test.ts.

A @boundary was an undirected pair, so the only way to tell its untrusted
side was to infer it from an endpoint no @asset declares. That says nothing
when both sides are declared assets, which is the usual case for an
application and its database: the SARIF export could only report the side
as unknown.

The new form '@boundary from <outer> to <inner>' states it. The between,
and and | forms are unchanged and stay undirected.

- Parser: the directed form is read inline and from .gal sidecars. A line
  whose arguments open with 'from' and do not parse is malformed; 'from' and
  'to' elsewhere in a sentence are not evidence, so prose stays a warning.
- Model: boundaries[] records directed: true, with asset_a the outer side and
  asset_b the inner side. Undirected boundaries carry no new field, and their
  annotation hash and claim key do not move; a declared, dropped or reversed
  direction changes both.
- SARIF: a declared side is basis 'declared', distinct from the inferred
  'undeclared-endpoint' and from 'unknown', in run.graphs[0] guardlink/side
  (a directed edge runs outer to inner, guardlink/directed: true) and in the
  pentest profile's boundary-claim results, whose inner_routes now also
  cover a declared inner side. Exports of models with no directed boundary
  are unchanged; the github baselines still match byte for byte. The
  boundary-claim rule's help text describes the three bases, so the two
  pentest consumer fixtures are regenerated, and a third,
  boundary-direction.sarif, holds all three bases.
- validate (CLI, MCP, TUI): a directed boundary whose outer or inner side is
  no declared asset and no @flows endpoint is an error,
  unresolved-boundary-side. diff reports a change of direction, and
  validate --code-graph uses a declared inner side.
- conformance/boundaries.json pins the grammar (directed, undirected,
  malformed), the side the export states, and the validation result, and
  runs in tests/conformance-boundaries.test.ts.
- SPEC 3.2, 2.12, 5.3, 6.6 and 6.8, the annotate prompt, the map playbook,
  the code-graph boundary suggestions and the agent instruction templates
  describe the directed form and ask agents to use it when the trust
  direction is known.
@Animesh-Sri-bugb
Animesh-Sri-bugb merged commit dbc0a01 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