Policy Information Providers
A policy information provider supplies facts that neither the runtime nor the request carries: whether a change request is approved, which groups a user belongs to, what a sandbox’s current risk level is. The policy gateway consults the providers registered with it in the Enrichment stage, before any rule runs, and exposes their facts to Cedar as context.<domain>. This page covers the contract, the two provider types, the registration file, and how to write policy over provider facts.
What a Provider Is
A provider attests facts. It never decides.
- A provider owns one domain, and policy reads its facts as
context.<domain>.<claim>. The registration file names the domaincontext, after the Cedar path it fills. Policies name domains, not providers, so a provider can be replaced without a policy change. - A provider returns claims. It never returns decisions, effects, obligations, or provenance. The gateway stamps provenance itself, as
context.<domain>._meta. - A provider sees only what its type allows. A
grpcprovider receives the identity and action fields of the subject. Anhttpprovider receives only the request fields its lookup keys read. Neither sees the request body, Cedar syntax, or another provider’s output. - A provider that cannot answer either fails the request closed or degrades, as its registration declares.
Registration is deployment configuration. Operators register providers in a YAML file passed to apf-policy-gateway with --pip-config. The signed bundle does not carry provider registrations.
Two Provider Types
Every registration declares a type.
A grpc provider connects in one of two ways. With https://host[:port] and a tls block, the provider runs anywhere the gateway can reach, the gateway pins the provider’s CA, and it presents its own client certificate. With unix:///path and an optional peer_uid, the provider runs on the same host and both sides verify the peer’s UID. Plain http:// is accepted only for a loopback host, localhost, 127.0.0.0/8, or ::1, for both types.
The Contract
Providers of type: grpc implement PolicyInformationService.GetAttributes, defined in contracts/proto/nvidia/agent_policy_fabric/pip/v1alpha1/pip.proto in package nvidia.agent_policy_fabric.pip.v1alpha1. The apf-proto crate generates it as pip_v1alpha1. Any language with gRPC support can implement it.
The file’s context is the contract’s domain. The registration file names the namespace after the Cedar path it fills, and the wire contract and audit records carry the same value as domain.
GetAttributesRequest
EvaluationSubject carries user_subject, tool, capability, action, args, target, runtime, and user_subject_attestation. Fields the provider does not receive are left empty.
GetAttributesResponse
Field 2, entity_attributes, is reserved for a later version of the contract.
Output Validation
The gateway validates every answer. An answer that fails any rule is recorded as invalid_output.
- Every root of
context_attributesis a requested domain, and its value is a record. - No root carries
_meta. - Claim values hold no nulls. Numbers are integers with an absolute value of at most 2^53 - 1.
- Nesting is at most 16 levels deep. Keys are 1 to 128 bytes and do not start with
__. - The encoded response is at most 64 KiB.
- A
NO_DATAanswer carries no attributes and attl_secondsof0. - A
FOUNDanswer without the domain’s root is treated asno_data.
Failures on the Wire
A provider reports a failure with a gRPC status code, which the gateway records as a status.
The contract evolves additively. The gateway rejects an AttributeStatus value it does not know, so adding one is a breaking change until providers can negotiate the contract version.
The Enrichment Stage
For each evaluation, the gateway runs these steps for every registered provider. Refer to Request Evaluation for where the stage sits in the pipeline.
- Applicability.
applies_tolists tool names or action ids. The provider runs when an entry equals the request’s tool name or its action id. A registration withoutapplies_toapplies to every evaluation. A provider that does not apply makes no lookup and records the statusnot_applicable. - Projection. The subject is projected to the fields the provider’s type allows. Only those fields leave the gateway.
- Lookup. Applicable providers run concurrently, each within its
timeout_msand the whole stage within--pip-stage-budget-ms. At most 32 lookups per provider are in flight at once. - Validation. The answer is checked against the output rules above.
- Provenance. The claims are written to
context.<domain>with a_metarecord. Refer to Provenance. - Failure mode. A provider that failed is handled as its
on_failuredeclares. Refer to Failure Modes.
Action ids differ by surface. On the OpenShell middleware path the tool name and the action id are both the bare MCP tool name, for example github_merge_pull_request. On the EvaluationService path the tool name is resource.class and the action id is <resource.class>::<verb>, for example github::merge_pull_request. One applies_to entry covers both surfaces only when callers use the same tool name on both; otherwise list both forms.
Anything a caller supplies in context_attributes is discarded before the stage runs. Only the gateway writes provider facts.
Registration File
The file is a YAML document with one providers list of 1 to 16 entries. Unknown keys are rejected anywhere in the file, and so are the keys listed under Deferred Keys. The gateway loads the file through its checked startup-file path: it must be a regular file, not a symbolic link, not group or world writable, and at most 64 KiB. The gateway parses and validates the file before any listener binds and refuses to start on any error. A validation error names the provider and the key path, for example provider 'change-management': response.must_match references key 'repo', which request.keys does not define. Once the file is accepted, the gateway logs policy information providers registered with the path, the number of providers, their contexts, and the stage budget.
Keys
Field References
response.record_path, the names in response.must_match, and the field references in response.keep name a place in the JSON answer.
- A bare name without
/is one top-level field.cmdb_ci.nameis the field namedcmdb_ci.name, not a nested path. The gateway converts it to the JSON pointer/cmdb_ci.nameand escapes a~in the name as~0. - A value that starts with
/is a JSON pointer, used as written./result/0/approvalreaches into nested records and lists.
A field whose name is not a valid claim name, such as cmdb_ci.name, can still become a claim through the map form of keep: keep: { ci: cmdb_ci.name }. A list entry that is not a valid claim name is rejected with a message that points at the map form.
Lookup Keys and Tokens
- A
{key}token is allowed in the path segments ofendpoint.urland inrequest.queryvalues. It is never allowed in the scheme, the host, or a query name. - A query value with a token holds exactly one token and nothing else.
- Every token names a key in
request.keys, and every key is used at least once in the URL or the query. - Every
response.must_matchvalue is exactly one{key}token. - A key is read from the subject path its
fromnames. A key that is absent, empty, or does not match itspatternmeans the source cannot be asked: no request is sent and the lookup fails with the statusno_data. The provider’son_failurethen applies:degradecontinues with_metaonly anddegraded: true,fail_closeddenies the request aspip_no_data:<provider>. A value is URL-encoded where its token stands; the pattern is the injection guard.
Endpoint Rules
- A URL must name a host, must not carry a user name or password, and must not carry a fragment.
- For
type: http,endpoint.tlsandendpoint.peer_uidare rejected: client certificates for http endpoints are not supported in this release. Register a system that requires one through the contract over mutual TLS. - For
type: grpc,endpoint.auth,request, andresponseare rejected. - An
https://URL on agrpcprovider requiresendpoint.tlswith all three files.endpoint.peer_uidis not allowed. - An
http://URL is accepted only for a loopback host.endpoint.tlsandendpoint.peer_uidare not allowed. - A
unix://URL is an absolute path with no trailing slash, no.or..segments, and no?,#,%, or control characters.endpoint.tlsis not allowed. The path must not be the gateway’s own evaluation socket, compared by resolved path and by inode. The socket must already exist when the gateway starts: the gateway inspects it before any listener binds and refuses to start unless it passes the socket rules under Security, so start the provider before the gateway and order the units withAfter=andRequires=on the provider’s service. Anhttps://orhttpprovider is contacted only on the first lookup and may be unreachable at startup;on_failuregoverns lookups, not startup.
Reserved Words
A context can never be one of the roots the request and the engine own: args, annotations, data, body_text, has_tool_name, target_host, target_method, target_path, or target_scheme. The evaluator also refuses an input whose namespace would shadow one of them.
A provider name or context that contains a word the audit sanitizer treats as a forbidden key is rejected, so a provider’s name can always appear in audit records. The words are token, secret, password, credential, private_key, api_key, apikey, refresh_token, access_token, cookie, authorization, body, text, raw, message, and file_content, matched as substrings without regard to case.
Deferred Keys
The following keys are reserved for a later release and are rejected today like any unknown key. The file has no legacy form: a document with a pips root is rejected.
Subject Projection
The provider’s type decides which subject fields the gateway may send. The lookup key of an http provider is visible in its registration, so a provider never receives arguments it has no use for.
- A
grpcprovider receives identity and action:user_subject,tool,capability,action, andruntime. Arguments and target fields are never sent to agrpcprovider in this release. - An
httpprovider receives the distinctfrompaths of itsrequest.keys, and nothing else. Each value is rendered into the request where its token stands. Any path in the table can be a key’sfrom;runtimerenders as the instance id.
Choose the lookup key with care. A key must be a field the tool acts on or the policy also constrains, such as a repository, a project, or a sandbox identity. It must never be a free argument the agent chooses, or the agent selects which record gates its request.
The two surfaces fill the subject differently.
The http Provider Type
A type: http registration turns an existing JSON API into a provider. For each applicable evaluation the gateway resolves the keys from the subject, sends one GET, selects the record, checks that it is the record asked for, and keeps the configured fields as the domain’s claims.
The Lookup on the Wire
For a github_merge_pull_request call whose repository argument is acme/payments, the gateway sends one request. The key value is percent-encoded for the query.
The API answers with the matching record under result.
record_path: result selects the list, which holds exactly one record. must_match confirms that its cmdb_ci.name equals the repository key. keep turns four fields into claims, and the rules see this record as context.change.
Behavior
- The request is always a
GETwithAccept: application/json, plus the credential header whenendpoint.authis set. The client follows no redirects and ignores proxy environment variables. - Resolved key values are percent-encoded for their position, a path segment or a query value, and never unescaped. A composite argument value, a list or an object, does not resolve. A boolean renders as
trueorfalse, and an integer as digits. - A key that is absent, empty, or does not match its
patternmeans the source cannot be asked. No request is sent; the lookup fails withno_dataand the provider’son_failureapplies. - Without
record_path, the body is the record, or a list that holds exactly one record. With it, the same applies to the referenced value. An empty list or a missing reference isno_data; a list of several records isinvalid_output, because the answer is ambiguous. - Every
must_matchentry must hold, or the lookup isno_data, so the API cannot answer with a different record than the one asked for. - With
keep, each named field becomes a claim with the same name, or with the name the map form gives it. A field the record does not carry, or carries asnull, is omitted. Values are kept as the API returned them, and a number must be an integer. Withoutkeep, a found record carries no claims and the domain holds only_meta. evidence_refis the keptnumberclaim, or the keptidclaim when there is nonumber, when it is a string or an integer that meets the evidence rules of the contract. A record kept under other claim names carries noevidence_ref.- The lookup must finish within
timeout_ms, which is also the HTTP client’s timeout. A response over 256 KiB isunavailable. - Credential files are read at call time, so rotation needs no restart, and read once at startup, so a bad credential fails fast instead of degrading every request.
HTTP Status Mapping
Timeouts
Keep the stage budget below the timeout OpenShell applies to middleware calls. The middleware advertises no binding timeout of its own and inherits the deadline OpenShell’s operator configured, so a stage that outlives that deadline makes OpenShell treat the call as failed. For a provider off the host, prefer degrade and a short timeout_ms.
Provenance
The gateway writes context.<domain>._meta beside the claims.
When the lookup found claims, the record holds the claims and _meta. When it did not, whether the provider did not apply, holds nothing for the subject, or degraded, the record holds only _meta.
Audit Extension
Every authorization record written after the stage ran carries an extensions.x_apf_pip object with one entry per provider.
domain is the registration’s context. Domains and provider names are values, not keys, so the audit sanitizer’s key rules cannot demote the record. An evidence_ref the sanitizer rejects is dropped from the entry, not the entry from the record. latency_ms is 0 for a provider that did not apply. Both the middleware’s apf-policy-gateway-middleware-audit.jsonl and the gateway’s apf-policy-gateway-audit.jsonl carry the extension. Refer to Audit Event Schema.
Failure Modes
Two statuses deny regardless of the failure mode. invalid_input is a failure the caller caused, so a caller cannot induce degradation: a projected subject over the 64 KiB message cap, a correlation id over 256 bytes, or a provider answering INVALID_ARGUMENT, which reports the subject it received as malformed. internal is a failure of the stage itself, such as a lookup task that panicked, and denies with pip_internal:<provider>. A provider reserves INVALID_ARGUMENT for a malformed subject; a subject it does not hold is NO_DATA.
Statuses
Writing Policy over Provider Facts
- Guard a
permitwithhas.context.change has approval && context.change.approval == "approved"grants only when the fact is present. A degraded or empty record keeps the grant off, and the tool call is denied withno grant for <action>. - A
forbidthat reads a provider fact without a guard denies the request when the fact is missing, because a forbid that fails to evaluate denies. That is the safe default for a deny rule. To deny explicitly when a provider degraded, testcontext.<domain>._meta.degraded. degraderelies on Cedar’s missing-attribute behavior. The idiom that ports to any engine is an explicit_meta.degradedtest, orfail_closed.
Refer to Capability Grants.
Security
- Sockets. Both ends of a Unix socket verify the peer. The gateway connects only to a socket owned by
peer_uidthat other users cannot access and that the gateway’s user can write: owner-only (0600) whenpeer_uidis the gateway’s own UID, otherwise group-writable (0660) with a group the gateway belongs to, since connecting to a Unix socket needs write permission on it. The gateway checks at startup that it can connect. Ancestors must be owned by root, the gateway’s user, orpeer_uidand not writable by others; a root-owned directory with the sticky bit set, such as/tmp, is also accepted. The hardened systemd unit setsPrivateUsers=yes, which maps every UID other than root and the service user tonobody, so a provider running as another user needs a unit without that setting or a shared group. A provider must accept connections only from the gateway’s UID and must reject bearer credentials on the socket. - TLS material.
ca_fileandclient_cert_filemust be regular files that are not group or world writable.client_key_filemust be readable by its owner only. - Credentials. Bearer tokens and basic credentials come from files, never from the registration file or the URL. A credential file must be a regular file readable by its owner only and at most 8 KiB. A URL that carries a user name or password is rejected.
- The registration file. It is loaded through the gateway’s checked startup path: a regular file, not a symbolic link, not group or world writable, and at most 64 KiB.
- Network egress. The hardened unit in
deploy/systemd/apf-policy-gateway.servicesetsIPAddressAllow=localhostandIPAddressDeny=any. A provider off the host needs anIPAddressAllow=entry for its address in a drop-in, and an IPv6 address also needsAF_INET6inRestrictAddressFamilies. - No forgery. A caller’s
context_attributesare discarded before the stage runs. A provider’s_metais rejected as invalid output. Only the gateway writes provenance. - Lookup keys. Key each lookup on a field the tool acts on or the policy constrains, never on a free argument the agent chooses.
Worked Example
A change-management system holds the change requests for each repository, as its configuration item. A grant lets an agent merge a pull request into a repository only while an approved, scheduled change exists for it.
Register the system as a provider. Every evaluation asks the system, so a withdrawn approval takes effect on the next request.
Start the gateway with --pip-config /etc/apf/providers.yaml, or set APF_PIP_CONFIG. Then add a capability rule to the bundle. The action is the bare MCP tool name because the OpenShell middleware evaluates the call.
The lookup is keyed on the repository the tool acts on, so the agent cannot name a different, approved change to satisfy the rule. The same merge performed as a raw HTTP request is governed by the sandbox network policy, not by this rule.
Identity Facts
The same shape turns an identity system into a provider. Key the lookup on user_subject, keep the fields a rule needs, and fail closed so a request that cannot be looked up is denied before any rule runs. The JSON API here answers a username query with a list of matching users. Leave record_path out: the gateway requires the list to hold exactly one record, where a pointer such as /0 would take the first of several.
A grant then reads the facts: context.user has clearance && context.user.clearance == "elevated". The subject is the one the caller asserts on EvaluationService.Evaluate; the OpenShell middleware path carries none, which is why the example is scoped with applies_to: unscoped, it would deny every sandbox request as pip_no_data:identity. Refer to Identity and Delegation.
The repository ships the three registration shapes as annotated files under examples/pips/.
Next Steps
- For the gateway flags, refer to CLI Commands.
- To see where the stage sits in the pipeline, refer to Request Evaluation.
- To write grants over provider facts, refer to Capability Grants.
- To read provider provenance in audit records, refer to Understand Decisions and Findings.