Foundgine

Adversarial & Security Boundary Testing

Files: Tests/GraphSecurityBoundaryTests.cs, Tests/OpenIntentMutationSecurityTests.cs, Tests/OpenIntentSupplyChainTests.cs, Tests/CapabilityBoundaryTests.cs.

The other docs in this set cover authorization policy (01), read-side traversal correctness (02), refusing to guess (03), and where candidates come from (04). This one covers a different axis: proof that the mechanisms those policies rely on — bounded traversal, graph-level pruning, mutation authoring, and field-level result shaping — actually hold under a caller that is open-ended or actively adversarial, not just under the happy path the starter sample exercises.

Why “open intent” needs its own security tests

The starter sample’s MCP tools are closed: get_my_orders, place_order, and so on are fixed shapes with fixed arguments, so there’s no room for a caller to ask for something the API doesn’t already anticipate. This sample exposes an open intent surface instead — ReadIntent and SemanticMutationIntentBuilder let a caller (or an agent) describe an arbitrary read or write shape at runtime: any entity, any relationship traversal, any depth. That flexibility is exactly what makes agentic use useful, and exactly what makes it a security surface: nothing stops a caller from asking for a traversal four levels deep, or a mutation graph with a forward-referencing dependency, unless something inside the compiler and planner refuses it.

Graph-level security: two boundaries, tested against the real domain

GraphSecurityBoundaryTests.cs exercises two boundaries from Foundgine’s semantic-security layer, deliberately against this sample’s own generated domain (the recursive Product.components -> componentProduct bill-of- materials edge, and the Supplier.incidents relationship) rather than a synthetic model — so a regression here would be caught in a shape that looks like real usage, not just in the core library’s own unit tests.

Traversal depth. A ReadIntent that walks Product -> components -> componentProduct -> components is 4 levels deep. ReadIntentCompiler.CompileOperationGraph takes a SecurityResourceLimits and throws before planning or execution if the intent’s depth exceeds MaxOperationGraphDepth — the test pins this two ways: a limit of 3 rejects the intent (with a message that names depth as the cause), and the identical intent compiled with a limit of 4 succeeds and produces exactly 4 graph nodes. Testing both sides matters: it proves the limit itself caused the rejection, not some incidental property of the traversal shape.

Graph-level authorization. Depth limits stop a request from being too expensive; they say nothing about whether the caller is allowed to see every node in it. Supplier.incidents is denied to every role except Analyst and SupplyChainManager (see 01-Claims-And-Authorization.md). The test compiles one ReadIntent covering Supplier.Name plus incidents.Severity, then runs SemanticAuthorizer.AuthorizeGraphWithEvidence twice against the same compiled graph — once as WarehouseOperator, once as Analyst. For the operator, the incidents subtree (the ComplianceIncident node) is pruned entirely and only the Supplier node survives; for the analyst, both nodes remain. Authorization here removes a whole relationship subtree from the graph the planner will ever see — it isn’t a filter applied to rows after the fact.

Open-intent mutations: the authoring surface has its own fail-closed rules

OpenIntentMutationSecurityTests.cs and OpenIntentSupplyChainTests.cs test SemanticMutationIntentBuilder itself — the thing a caller uses to describe a write. Because the surface is open (arbitrary entities, fields, and cross-operation dependencies), the builder has to reject malformed authoring at build time rather than let a bad shape reach the planner:

OpenIntentSupplyChainTests.cs’s fan-out case (PurchaseOrder -> PurchaseOrderLine + Shipment, all sharing one generated identity) is the positive-path companion to these: it proves the legitimate version of a multi-step, dependency-ordered mutation graph plans correctly, so the rejections above are shown to be about the specific malformed shapes, not about open-intent mutations being restricted in general.

CapabilityBoundaryTests: the field-leak boundary, pinned against this domain’s own shape

The other three test files are about what a caller is allowed to ask for. CapabilityBoundaryTests.cs is about a narrower and easy-to-miss failure: even a fully authorized, correctly-scoped query must never let a field the caller didn’t select — or a join key that only exists to make a traversal possible — leak into the result row.

This sample’s own generated domain doesn’t happen to have a backing-only column (every CLR property is a selectable semantic field), so these tests hand-build a small MetadataRegistry shaped the way a real ERP integration often is: a PurchaseOrder.SupplierId foreign key that’s present on every backing row and required to resolve the Supplier -> PurchaseOrders relationship, but that has no FieldMetadata entry and is never exposed as a selectable field.

Two cases, both asserting the same invariant from different angles:

The point of hand-authoring the metadata rather than relying on the sample’s generated domain: the invariant is only meaningfully tested when a backing-only, join-only field actually exists to try to leak. A domain where every column is already a public field can’t exercise this path at all — which is exactly why the test builds the one shape that can.

How this doc set fits together

Read in this order, the five docs describe one continuous chain of guarantees for an agent operating against an open, natural-language-ish intent surface instead of a fixed API:

  1. 01 — who is the caller, and what can their role touch at all.
  2. 02 — do the read-side business scenarios stay correct and bounded even against adversarial data (cycles, unbounded depth).
  3. 03 — when a phrase is ambiguous or unrecognized, refuse instead of guessing.
  4. 04 — where the candidate interpretations that feed grounding come from, and how each retrieval strategy degrades when its backing capability isn’t available.
  5. 05 (this doc) — even once a request is authorized, bounded, and grounded, the compiler, authorizer, and execution boundary each still enforce their own limits: traversal depth, subtree-level denial, fail-closed mutation authoring, and never leaking an unselected or join-only field.

Every layer fails closed on its own terms. None of them assumes an earlier layer already caught the problem.