Skip to navigation

Content Controls

View as Markdown

Content controls decide whether an already-permitted call holds up given what it carries. They are PolicyRule documents with role: control. Control rules apply to all traffic that reaches APF, MCP tool calls and plain HTTP requests alike. Their permits never grant a tool call. Their forbids deny any request they match.

A First Example

The demo bundle’s security-content-gate denies MCP tool calls that carry a secret, and replaces the body of plain HTTP requests that carry one:

apiVersion: nvidia.agent-policy-fabric.policy/v1alpha1
kind: PolicyRule
metadata:
name: security-content-gate
spec:
description: Deny secret leakage, redact PII.
language: cedar
role: control
definition: |
@id("baseline-allow")
permit (principal, action, resource);
@apf_finding("secret-detected")
@id("deny-secret")
forbid (principal, action, resource)
when { context.has_tool_name && context.annotations.secret_detected };
@apf_effect("redact-wholesale")
@id("redact-secret-rest")
permit (principal, action, resource)
when { !context.has_tool_name && context.annotations.secret_detected };
@apf_effect("redact-pii-body")
@id("redact-pii-rest")
permit (principal, action, resource)
when { !context.has_tool_name && context.annotations.pii_detected };

The rule reads two signals produced by annotators, secret_detected and pii_detected. Annotators do the inspection, Cedar decides, and effects carry out the rewrite.

How a Control Rule Works

StatementFires whenResult
forbid with @apf_findingThe condition matches.The request is denied with reason security-content-gate/deny-secret, and the finding is recorded. Evaluation stops.
permit with @apf_effectThe condition matches.The request continues, and the effect is applied if the request is finally allowed.
permit without annotationsThe condition matches.Nothing. A control permit does not grant a tool call.

Building Blocks

Detection Signals

Read annotations with context.annotations.<name>. Bool annotations are always present. Guard string and integer annotations with has:

forbid (principal, action, resource)
when {
context.annotations has secret_kind &&
context.annotations.secret_kind == "aws_access_key"
};

Refer to Annotators.

Traffic Type

context.has_tool_name is true for MCP tool calls and false for plain HTTP requests. Use it to treat the two differently in one rule, as the demo does.

Request Target

context.target_host, context.target_method, context.target_path, and context.target_scheme describe the destination. They are present when the request has a target, which is always the case on the OpenShell middleware path.

@id("block-uploads")
forbid (principal, action, resource)
when {
context has target_method &&
context.target_method == "PUT" &&
!context.has_tool_name
};

Tool Arguments

context.args carries MCP tool arguments, with _len and _num fields derived for string arguments. Refer to Capability Grants.

Choosing a Response

GoalPattern
Block the requestforbid, optionally with @apf_finding to record why.
Let the request through in a safer formpermit with @apf_effect referencing a body replacement or patch.
Let the request through and flag itpermit with @apf_effect referencing an effect whose verdict is warn or escalate, or with @apf_finding alone.
Record an observation onlypermit with @apf_finding. The decision is unchanged and the finding is recorded.

Fail-Closed Rewrites

A rewrite that cannot be applied denies the request, because forwarding the original would drop a mandated change.

  • A body patch requires a JSON body. A patch on a non-JSON body denies the request.
  • A replace or remove patch on a path the body does not contain denies the request. The demo’s redact-pii-body effect replaces /text, so a plain HTTP request that contains PII but has no top-level text field is denied.
  • An effect that reads an annotation with source denies the request if that annotation was not produced.

Prefer a whole-body replacement when you cannot predict the body’s shape.

Scope

Content controls see the request only. APF binds the supervisor middleware’s request operation, so it does not inspect responses or WebSocket messages, and it never sees credentials, which OpenShell injects after APF decides. Refer to Enforcement Coverage and Limitations.

Next Steps