Foundgine treats external intent as untrusted input.
The fundamental rule is:
No transport adapter should bypass the semantic/security boundary.
Foundgine’s security story follows one lifecycle rather than separate transport-specific paths:
Retrieval is never authorization. Fuzzy, full-text, BM25 and Apache AGE graph retrieval may return candidates and evidence, but every candidate is still resolved and authorized before execution.
The caller controls intent:
"read Customer.Name"
The trusted host controls authority:
identity
tenant
audience
warrant
A request must never be able to promote itself by changing ordinary intent fields.
Authorization is evaluated at the semantic boundary for:
Capability discovery is advisory. The policy is evaluated again for the actual request.
A policy can carry a provider-independent predicate such as:
resource.TenantId == context.TenantId
The predicate survives into the logical plan and is lowered by the provider.
This is important for:
A provider must not discard a predicate merely because the provider can produce a syntactically valid query without it.
Security is not only about authorization. A caller can also attack the semantic
engine with structurally expensive intent. SecurityResourceLimits is the
canonical engine-side guard and applies independently of whether the request
arrived through JSON, MCP, GraphQL, C#, or another adapter.
The default bounds include:
| Resource | Default maximum |
|---|---|
| Selection depth | 32 |
| Selection nodes | 256 |
| Operation-graph nodes | 256 |
| Operation-graph depth | 32 |
| Operation-graph fields | 512 |
| Filter depth | 32 |
| Filter nodes | 256 |
| Order terms | 64 |
| Order-path depth | 16 |
| Page size | 1,000 |
| Offset | 1,000,000 |
| Cursor length | 4,096 |
Mutation requests also have independent bounds for operations, fields, return fields, dependencies, and effects. Applications can tighten the defaults for their threat model; the important invariant is that untrusted request complexity is bounded before it can consume unbounded planner/provider resources.
Provider plan caching is an optimization boundary, not an authorization boundary. A cache entry is derived from the complete provider-independent plan, including its authorization semantics. Runtime context values are supplied at execution time rather than becoming an alternate source of authority.
The safe lifecycle is:
A cache hit must never skip semantic resolution or authorization, and provider conformance must still be satisfied before execution.
A logical traversal can hide intermediate entities:
Authorization sees the expanded path.
Therefore:
deny Contract
⇒ deny Customer.transactions
A traversal is not a security shortcut.
Capability discovery exists so dynamic callers and AI agents can construct valid intent.
A capability document is not an authorization token.
An especially important invariant is that an empty authorized field set must not mean “select everything”.
The execution/provider boundary must reject an invalid empty projection rather than widening the selection.
A provider can execute valid SQL and still violate Foundgine’s security contract.
Foundgine therefore carries required security invariants into execution and checks provider conformance.
GraphQL input is untrusted.
Do not accept identity, tenant, audience, warrant, or provider control information from GraphQL variables/arguments as trusted authority.
Use:
Foundgine.Extensions.GraphQL.HotChocolate.HotChocolate.Execution
for the secure query execution path.
For mutations use:
Foundgine.Extensions.GraphQL.HotChocolate.HotChocolate.Mutations.
MCP tool arguments are untrusted.
The MCP host should obtain security context from authenticated session/request state.
Do not allow an agent to select its own tenant or authorization role.
An LLM is an untrusted producer of intent.
Avoid:
Use:
The application remains responsible for authentication, model credentials, rate limits, quotas, and prompt/application policy.
JSON intent should be bounded with JsonReadIntentAdapterOptions.
Structural limits protect the parser/intent boundary, while semantic authorization protects the operation.
Writes have stronger security requirements than reads.
The mutation path can include:
A mutation builder is an authoring tool, not an authorization mechanism.
Caching must never cache away authorization.
The safe conceptual model is:
Foundgine.Runtime.ControlPlane is optional.
It provides authority/control-plane recovery primitives such as witness quorum, credential lifecycle, journal reconciliation, promotion/failover, and recovery evidence.
It is not required for normal semantic authorization and remains outside the core execution path.
Foundgine does not replace application security infrastructure.
The application/host remains responsible for:
Foundgine enforces the semantic execution boundary supplied by the application.
Every new transport/provider should have adversarial tests proving that it cannot:
This section records the outcome of a live adversarial run of Foundgine.RedTeam
against the running Foundgine.SupplyChain.Advanced sample, exercising both the
semantic authorization API (port 4432) and the execution/tool-calling API
(port 4422) over MCP’s Streamable HTTP transport.
After both fixes, 18 attack attempts were sent across the two surfaces. All reached the MCP endpoint (HTTP 200 at the transport layer) and were evaluated by the JSON-RPC/authorization layer underneath:
| Surface | Target | Requests | Legitimate baseline succeeded | Adversarial attempts blocked |
|---|---|---|---|---|
| Semantic authorization API | http://127.0.0.1:4432/ |
11 | 1/1 | 10/10 |
| Execution API | http://127.0.0.1:4422/ |
7 | 1/1 | 5/5 (1 unauthenticated baseline also blocked) |
Semantic authorization API
| Attack | Result |
|---|---|
| Authenticated capability discovery (baseline) | Succeeded — capability document returned, all entity/field access correctly marked Denied where policy disallows it |
| Cross-tenant policy probe | Denied |
| Named operation escalation | Denied |
| Identity claim spoofing | Rejected — server error explicitly states claims cannot assert identity/privilege directly; identity comes only from actor/token authentication |
| Entity write escalation | Denied |
| Wrong-token authentication bypass | Failed before reaching the tool (auth rejected) |
| Relationship authorization escalation | Denied |
| Sensitive field authorization probe | Denied |
| Unknown actor authentication | Failed before reaching the tool (auth rejected) |
Claim boundary manipulation (attempted scope: * widening) |
Denied — the widened claim was explicitly rejected, not silently narrowed |
Execution API
| Attack | Result |
|---|---|
| Authenticated execution baseline | Succeeded — legitimate product lookup returned data |
| Cross-customer order access | Blocked — tool invocation errored rather than returning another customer’s order |
Customer → warehouse capability escalation (update_inventory) |
Blocked |
Customer shipment write escalation (create_shipment) |
Blocked |
Customer supplier enumeration (list_suppliers) |
Blocked |
Unknown actor authorization (update_inventory) |
Blocked |
Across both surfaces, only the two intentionally-legitimate baseline calls returned data; every cross-tenant, role-spoofing, claim-widening, and unauthenticated-actor attempt was denied or errored out before reaching the underlying operation. This is consistent with the security invariants described earlier in this document — in particular that retrieval/capability discovery is advisory only, that claims cannot self-assert identity or privilege, and that authorization is re-evaluated for the actual request rather than trusted from discovery.
The execution API’s uniform "An error occurred invoking '<tool>'." message is not
an accident of this sample — it is the MCP C# SDK’s own default behavior. The SDK
sanitizes tool-invocation failures by design: any exception other than its own
McpException type is collapsed into that generic message before it reaches the
caller, precisely so a server author cannot accidentally leak exception details
(stack traces, internal identifiers, database errors) to an untrusted MCP client.
McpException.Message, by contrast, is documented as safe to propagate verbatim —
it exists for a tool to deliberately choose what to tell the caller.
The semantic API’s richer response ({"result":{"allowed":false,"kind":"Denied"}},
and the named-claim detail on spoofing attempts) is not a framework default at all.
It comes from Semantic/Api/Mcp/Program.cs’s policy_probe tool, which is a
teaching/lab tool: its explicit purpose is to let a caller see which authorization
decision path was taken (Denied vs. a conditional predicate vs. an outright
error), so the Supply Chain sample can demonstrate the security model described
earlier in this document. That is a reasonable choice for a documentation/lab
surface whose whole job is to make policy decisions legible — it is not necessarily
the right choice for the same shape on a production authorization endpoint, where a
caller probing many variants could use Denied vs. RequiresClarification vs. the
specific rejected-claim detail to map out policy boundaries faster than a uniform
error would allow.
The execution API’s Execute helper in
Semantic/Api/Mcp/Program.cs
now:
BlockedCallClassification, independent of which .NET exception type happened
to be thrown for it.ILogger, so an operator can distinguish “blocked by
authorization” from “blocked by an unrelated bug” from the server-side log alone.McpException whose message is uniform across every
classification — `“Request blocked while invoking ‘