From 896d66cdf491f168fc97dbce2318ce2136f7ff06 Mon Sep 17 00:00:00 2001 From: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com> Date: Sat, 26 Sep 2026 21:24:41 +0200 Subject: [PATCH 1/8] feat(server): define guarded provider contracts Signed-off-by: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com> --- .../server/experimental/contracts/README.md | 195 ++++ .../contracts/guard-contract.schema.json | 520 ++++++++++ .../contracts/minimal.guard.example.yaml | 38 + .../contracts/minimal.openapi.example.yaml | 39 + .../experimental/contracts/reference.md | 896 ++++++++++++++++++ .../experimental/test_guard_contract.py | 84 ++ 6 files changed, 1772 insertions(+) create mode 100644 nemoguardrails/server/experimental/contracts/README.md create mode 100644 nemoguardrails/server/experimental/contracts/guard-contract.schema.json create mode 100644 nemoguardrails/server/experimental/contracts/minimal.guard.example.yaml create mode 100644 nemoguardrails/server/experimental/contracts/minimal.openapi.example.yaml create mode 100644 nemoguardrails/server/experimental/contracts/reference.md create mode 100644 tests/server/experimental/test_guard_contract.py diff --git a/nemoguardrails/server/experimental/contracts/README.md b/nemoguardrails/server/experimental/contracts/README.md new file mode 100644 index 0000000000..674740b2e0 --- /dev/null +++ b/nemoguardrails/server/experimental/contracts/README.md @@ -0,0 +1,195 @@ +# Guarded provider contracts + +A guard contract makes the guardrail boundary readable: which fields contain +guarded text, which values are constrained, and which data stays opaque under +provider authority. You do not need to read Python to understand that policy. +The deployment separately decides which rails inspect the selected text. + +Status: experimental, `1.0.0-alpha.1`. The provider OpenAPI remains authoritative +for HTTP shapes. This is a custom contract format using JSON Schema vocabulary, +not an OpenAPI document or Overlay. Overlay 1.1 is used for normalization and +portable export. + +## A complete small contract + +```yaml +version: '1.0.0-alpha.1' +operationId: createText +profile: single_text.v1 +request: + required: [prompt] + properties: + prompt: + type: string + minLength: 1 + x-nemo-guardrails: + classification: guarded + subject: + kind: text + role: user + replaceable: true + x-nemo-guardrails: + opaque_fields: [model] +response: + required: [text] + properties: + text: + type: string + minLength: 1 + x-nemo-guardrails: + classification: guarded + subject: + kind: text + role: assistant + replaceable: true + status: + const: complete + x-nemo-guardrails: + classification: constrained + reason: projection_policy.complete_text +integration: + endpoint: + unsupported_request_code: unsupported_text_request + unsupported_response_code: unsupported_text_response +``` + +This [executable example](minimal.guard.example.yaml) is paired with a +[small provider description](minimal.openapi.example.yaml). It guards the +request's `prompt` and response's `text`, delegates `model` to the provider, and +constrains response `status` to `complete`. `replaceable: true` permits replacing +the selected text while preserving unrelated data. + +`operationId` selects one operation in the normalized provider OpenAPI. +`request` and `response` are required; add `stream` for streaming and +`excluded_stream_events` for deliberately unsupported event schemas. +`integration` holds runtime bindings, separate from policy. The supported +profiles and binding restrictions are listed in the +[reference](reference.md#validation-and-compatibility-limits). + +## Reading the policy + +| Declaration | Meaning | +| --- | --- | +| `classification: guarded` | Contains guarded text directly or through explicitly described children. | +| `classification: constrained` | Locally restricts structure or values without making that field a text subject. | +| `classification: opaque` | Delegates the entire value to the provider; Guardrails does not inspect its nested shape. | +| `opaque_fields: [id, model]` | Shorthand for explicit opaque properties on this object only. | +| `subject` | Identifies text, its user/assistant role, and replacement policy. | +| `reason` | Records why a restriction exists, using the existing namespaced reason vocabulary. | + +A guarded field can also have constraints. These are not three separate maps +or mutually exclusive sets of schema keywords. `opaque` is not a safety claim: +authors must decide whether a value can affect guarded content before delegating +it. Nested provider changes inside an opaque value do not trigger local review. + +Every selected object must account for every known provider property exactly +once. Name opaque fields explicitly; wildcards, duplicates, and overlap with +`properties` are errors. Unknown runtime keys follow `additionalProperties` +and, where declared, `unknown_fields: configurable`. Allowing unknown keys does +not waive review of newly documented provider fields at a selected boundary. + +The [reference](reference.md) puts each annotation beside its semantics, +examples, and edge cases, including requiredness, nulls, defaults, replacement, +and streaming. + +## Version and stability + +The only accepted contract version is `"1.0.0-alpha.1"`. It identifies this +experimental authoring envelope and its documented semantics. Unsupported +versions and unknown keys are errors; there are no version aliases or alternate +layouts. + +A bare `version: 1` would identify a format, not inherently promise stability. +The explicit prerelease label makes the status clear: authoring and semantics +may change before a stable `1.0.0`. Increment the alpha revision when publishing +an incompatible contract revision; never silently reinterpret a published +revision. This follows [Semantic Versioning](https://semver.org/#spec-item-9). + +Contract versions are independent of `overlay: 1.1.0`, provider manifest +`version: 1`, generated annotation versions, and capability profile identifiers. +Those identify different formats or semantic boundaries. + +List guard contracts directly in `provider.yaml`: + +```yaml +operations: + - chat-completions.guard.yaml +``` + +Each operation has one maintained contract. OpenAPI Overlays are used for +evidence-backed normalization and portable export, not as operation-authoring +inputs. + +## Editor support and validation + +[guard-contract.schema.json](guard-contract.schema.json) is a self-contained +JSON Schema 2020-12 schema. Associate it with `*.guard.yaml` in your editor. For +the [YAML language server](https://github.com/redhat-developer/yaml-language-server#associating-schemas), +the equivalent workspace setting is: + +```json +{ + "yaml.schemas": { + "./nemoguardrails/server/experimental/contracts/guard-contract.schema.json": "**/*.guard.yaml" + } +} +``` + +The schema offers key/value completion and catches misspellings, wrong types, +unsupported schema keywords, blank exclusion reasons, and malformed integration +metadata. It is not proof of provider compatibility. Provider coverage, source +reachability, text-target cardinality, hook compatibility, and runtime capability +require semantic validation against the provider source. Provider-aware +validation and generation are introduced with the compiler. + +## Authoring workflow + +Start from the [minimal contract](minimal.guard.example.yaml) and review it +against the selected provider operation. Classify every selected provider field, +declare accepted or explicitly excluded stream events when streaming is present, +and put runtime bindings under `integration`. List the `*.guard.yaml` filename +in the provider manifest. + +Use schema validation while authoring. When changing a constraint, edit that +field's schema and reason, then review it against the pinned provider +description. Do not mark a newly documented content-bearing field opaque merely +to silence a review error. + +## Author review checklist + +Before submitting a new or changed operation, verify: + +- The provider source is immutable, pinned, and digest-verified. +- Normalization actions cite evidence and contain provider facts, not Guardrails + policy. +- The guard contract uses standard Schema Objects for structure. +- Every provider field at every guarded object boundary is classified once. +- Every opaque field has been reviewed as independent of guarded content. +- Every constrained field is necessary to make the current capability safe and + unambiguous. +- Every disabled feature has a structured reason. +- Guarded text has the correct role and explicit replacement policy. +- Arrays and unions identify exactly the supported text target. +- Stream event variants are disjoint and cover every accepted provider branch. +- Stateful stream behavior remains in handwritten hooks rather than the + contract. +- Endpoint symbols live inside the declared runtime package. +- Schema validation and the relevant runtime tests succeed. + +## Standards boundary + +This document format is custom, using familiar JSON Schema vocabulary and +explicit `x-nemo-guardrails` semantics. It is neither an OpenAPI document nor an +OpenAPI Overlay. Its exported transformation follows +[Overlay 1.1](https://spec.openapis.org/overlay/v1.1.0.html); ordinary Overlay +tools apply the transformation, not guardrail behavior. + +See the [semantic limits](reference.md#validation-and-compatibility-limits) +for the current implementation boundary and known schema/runtime differences. + +## Provider contracts + +Provider-specific contracts are introduced with their provider capabilities. +Each operation contract covers its request, buffered response, and optional +streaming boundary as one policy document. Use the [reference](reference.md) +for precise syntax and meaning. diff --git a/nemoguardrails/server/experimental/contracts/guard-contract.schema.json b/nemoguardrails/server/experimental/contracts/guard-contract.schema.json new file mode 100644 index 0000000000..5a55ba3b7b --- /dev/null +++ b/nemoguardrails/server/experimental/contracts/guard-contract.schema.json @@ -0,0 +1,520 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:nemo-guardrails:guard-contract:1.0.0-alpha.1", + "title": "NeMo Guard Contract 1.0.0-alpha.1 (experimental)", + "description": "Policy-first authoring format. Schema validation checks syntax, not provider coverage or runtime capability.", + "type": "object", + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string", + "minLength": 1 + }, + "version": { + "const": "1.0.0-alpha.1" + }, + "operationId": { + "type": "string", + "pattern": "\\S" + }, + "profile": { + "const": "single_text.v1" + }, + "request": { + "$ref": "#/$defs/schema" + }, + "response": { + "$ref": "#/$defs/schema" + }, + "stream": { + "allOf": [ + { + "$ref": "#/$defs/schema" + }, + { + "type": "object", + "required": [ + "oneOf", + "x-nemo-guardrails" + ], + "properties": { + "x-nemo-guardrails": { + "type": "object", + "required": [ + "transport" + ] + }, + "oneOf": { + "type": "array", + "items": { + "type": "object", + "required": [ + "x-nemo-guardrails" + ], + "properties": { + "x-nemo-guardrails": { + "type": "object", + "required": [ + "event" + ] + } + } + } + } + } + } + ] + }, + "excluded_stream_events": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "reason": { + "type": "string", + "pattern": "\\S" + }, + "sources": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1, + "uniqueItems": true + } + }, + "required": [ + "reason", + "sources" + ] + } + }, + "integration": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$" + }, + "endpoint": { + "$ref": "#/$defs/endpoint" + } + }, + "required": [ + "endpoint" + ] + } + }, + "required": [ + "version", + "operationId", + "profile", + "request", + "response", + "integration" + ], + "dependentRequired": { + "excluded_stream_events": [ + "stream" + ] + }, + "$defs": { + "symbol": { + "type": "object", + "additionalProperties": false, + "properties": { + "module": { + "type": "string", + "pattern": "^[a-zA-Z_]\\w*(\\.[a-zA-Z_]\\w*)*$" + }, + "name": { + "type": "string", + "pattern": "^[a-zA-Z_]\\w*$" + } + }, + "required": [ + "module", + "name" + ] + }, + "endpoint": { + "type": "object", + "additionalProperties": false, + "properties": { + "route_path": { + "type": "string", + "pattern": "^/[^/].*[^/]$|^/[^/]$" + }, + "operation_label": { + "type": "string", + "pattern": "\\S" + }, + "unsupported_request_code": { + "type": "string", + "pattern": "\\S" + }, + "unsupported_response_code": { + "type": "string", + "pattern": "\\S" + }, + "stream_hooks": { + "$ref": "#/$defs/symbol" + }, + "api_revision": { + "$ref": "#/$defs/symbol" + } + }, + "required": [ + "unsupported_request_code", + "unsupported_response_code" + ] + }, + "subject": { + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "const": "text" + }, + "role": { + "enum": [ + "user", + "assistant" + ] + }, + "replaceable": { + "type": "boolean" + }, + "replacement_blocked_by": { + "type": "string", + "minLength": 1 + }, + "replacement_reason": { + "type": "string", + "pattern": "^(core_capability|provider_integrity|projection_policy)\\.[a-z][a-z0-9_]*$" + } + }, + "required": [ + "kind", + "role" + ] + }, + "selection": { + "type": "object", + "additionalProperties": { + "type": [ + "string", + "integer", + "boolean" + ] + } + }, + "variant": { + "type": "object", + "additionalProperties": false, + "properties": { + "match": { + "$ref": "#/$defs/selection" + }, + "cardinality": { + "enum": [ + "any", + "at_most_one", + "exactly_one" + ] + } + } + }, + "eventVariant": { + "type": "object", + "additionalProperties": false, + "properties": { + "source_schema": { + "type": "string", + "minLength": 1 + }, + "shape": { + "type": "string", + "minLength": 1 + }, + "match": { + "$ref": "#/$defs/selection" + }, + "required_fields": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "external_reason": { + "type": "string", + "pattern": "^(core_capability|provider_integrity|projection_policy)\\.[a-z][a-z0-9_]*$" + } + }, + "required": [ + "source_schema", + "shape" + ] + }, + "event": { + "type": "object", + "additionalProperties": false, + "properties": { + "classification": { + "enum": [ + "guarded_delta", + "snapshot", + "opaque", + "provider_error" + ] + }, + "variants": { + "type": "array", + "items": { + "$ref": "#/$defs/eventVariant" + }, + "minItems": 1 + }, + "missing_text": { + "enum": [ + "reject", + "opaque", + "empty" + ] + }, + "missing_text_shape": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "classification", + "variants" + ] + }, + "transport": { + "type": "object", + "additionalProperties": false, + "properties": { + "data_discriminator": { + "type": "string", + "minLength": 1 + }, + "require_sse_event": { + "type": "boolean" + }, + "non_data_shape": { + "type": "string", + "minLength": 1 + }, + "sentinels": { + "type": "object", + "additionalProperties": { + "type": "string", + "minLength": 1 + } + } + } + }, + "metadata": { + "type": "object", + "additionalProperties": false, + "properties": { + "classification": { + "enum": [ + "guarded", + "constrained", + "opaque" + ] + }, + "subject": { + "$ref": "#/$defs/subject" + }, + "reason": { + "type": "string", + "pattern": "^(core_capability|provider_integrity|projection_policy)\\.[a-z][a-z0-9_]*$" + }, + "gate": { + "const": "disabled" + }, + "extension": { + "type": "boolean" + }, + "model": { + "type": "string", + "minLength": 1 + }, + "source": { + "type": "string", + "pattern": "^#/components/schemas/[A-Za-z_][A-Za-z0-9._-]*$" + }, + "unknown_fields": { + "enum": [ + "configurable" + ] + }, + "stream_selector_field": { + "type": "string", + "minLength": 1 + }, + "opaque_fields": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "not": { + "const": "*" + } + }, + "minItems": 1, + "uniqueItems": true + }, + "variant": { + "$ref": "#/$defs/variant" + }, + "event": { + "$ref": "#/$defs/event" + }, + "event_schema_binding": { + "enum": [ + "operation_response", + "component" + ] + }, + "event_schema_reason": { + "type": "string", + "pattern": "^(core_capability|provider_integrity|projection_policy)\\.[a-z][a-z0-9_]*$" + }, + "transport": { + "$ref": "#/$defs/transport" + } + } + }, + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "title": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string" + }, + "$comment": { + "type": "string" + }, + "examples": { + "type": "array", + "items": {} + }, + "deprecated": { + "type": "boolean" + }, + "readOnly": { + "type": "boolean" + }, + "writeOnly": { + "type": "boolean" + }, + "type": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "const": {}, + "default": {}, + "enum": { + "type": "array", + "items": {}, + "minItems": 1, + "uniqueItems": true + }, + "minItems": { + "type": "integer", + "minimum": 0 + }, + "maxItems": { + "type": "integer", + "minimum": 0 + }, + "minLength": { + "type": "integer", + "minimum": 0 + }, + "maxLength": { + "type": "integer", + "minimum": 0 + }, + "pattern": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/field" + } + }, + "required": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "additionalProperties": { + "type": "boolean" + }, + "items": { + "$ref": "#/$defs/schema" + }, + "oneOf": { + "type": "array", + "items": { + "$ref": "#/$defs/schema" + }, + "minItems": 1 + }, + "x-nemo-guardrails": { + "$ref": "#/$defs/metadata" + } + }, + "patternProperties": { + "^x-(?!nemo-guardrails$)": {} + } + }, + "field": { + "allOf": [ + { + "$ref": "#/$defs/schema" + }, + { + "type": "object", + "required": [ + "x-nemo-guardrails" + ], + "properties": { + "x-nemo-guardrails": { + "type": "object", + "required": [ + "classification" + ] + } + } + } + ] + } + } +} diff --git a/nemoguardrails/server/experimental/contracts/minimal.guard.example.yaml b/nemoguardrails/server/experimental/contracts/minimal.guard.example.yaml new file mode 100644 index 0000000000..1cb321e26e --- /dev/null +++ b/nemoguardrails/server/experimental/contracts/minimal.guard.example.yaml @@ -0,0 +1,38 @@ +version: '1.0.0-alpha.1' +operationId: createText +profile: single_text.v1 +request: + required: [prompt] + properties: + prompt: + type: string + minLength: 1 + x-nemo-guardrails: + classification: guarded + subject: + kind: text + role: user + replaceable: true + x-nemo-guardrails: + opaque_fields: [model] +response: + required: [text] + properties: + text: + type: string + minLength: 1 + x-nemo-guardrails: + classification: guarded + subject: + kind: text + role: assistant + replaceable: true + status: + const: complete + x-nemo-guardrails: + classification: constrained + reason: projection_policy.complete_text +integration: + endpoint: + unsupported_request_code: unsupported_text_request + unsupported_response_code: unsupported_text_response diff --git a/nemoguardrails/server/experimental/contracts/minimal.openapi.example.yaml b/nemoguardrails/server/experimental/contracts/minimal.openapi.example.yaml new file mode 100644 index 0000000000..831c98315a --- /dev/null +++ b/nemoguardrails/server/experimental/contracts/minimal.openapi.example.yaml @@ -0,0 +1,39 @@ +openapi: 3.1.0 +info: + title: Synthetic buffered text example + version: 1.0.0 +paths: + /text: + post: + operationId: createText + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TextRequest' + responses: + '200': + description: Generated text + content: + application/json: + schema: + $ref: '#/components/schemas/TextResponse' +components: + schemas: + TextRequest: + type: object + required: [prompt, model] + properties: + prompt: + type: string + model: + type: string + TextResponse: + type: object + required: [text] + properties: + text: + type: string + status: + type: string + enum: [complete, partial] diff --git a/nemoguardrails/server/experimental/contracts/reference.md b/nemoguardrails/server/experimental/contracts/reference.md new file mode 100644 index 0000000000..aeccd9e791 --- /dev/null +++ b/nemoguardrails/server/experimental/contracts/reference.md @@ -0,0 +1,896 @@ +# Guard contract reference + +This is the syntax and semantic specification for `1.0.0-alpha.1` +(experimental). Start with the [README](README.md) for a complete small example +and the editing workflow. This reference defines what an implementation must +preserve, not just how the compiler currently generates Python. + +The [authoring schema](guard-contract.schema.json) defines accepted syntax: +a limited subset of JSON Schema with local `x-nemo-guardrails` annotations. +Generic JSON Schema tools do not enforce Guardrails-specific semantics. + +## Authority and scope + +The pinned provider description owns HTTP paths, source schemas, and provider +facts. Evidence-backed normalization corrects that description before guard +policy is interpreted. A guard contract selects a supported boundary; it does +not redefine the whole provider API or claim to configure all detectors, +prompts, input/output rails, or deployment settings. + +Request and response policy describes structural acceptance and text targets. +The deployment separately selects which rails inspect that text and whether a +rail result permits, rejects, or replaces it. A marked subject is a guardable +surface, not a guarantee that every deployment runs a particular safety check. + +## Examples + +Begin with the [minimal contract](minimal.guard.example.yaml), paired with +its [provider OpenAPI](minimal.openapi.example.yaml). Complete examples are +introduced with their provider capabilities. + +For authoring and validation steps, use the [guard contract guide](README.md). + +Root component names, projection inventories, operation bindings, and source +bindings are derived. They are not a second set of authoring decisions. + +## Payload Projections + +A Payload Projection is a normal OpenAPI Schema Object with local Guardrails +annotations. It describes either a request or a buffered JSON response. + +### Minimal request example + +```yaml +request: + type: object + additionalProperties: true + title: example.chat.request.text.v1 + properties: + messages: + type: array + minItems: 1 + maxItems: 1 + items: + type: object + additionalProperties: true + title: ExampleUserMessageProjection + properties: + role: + const: user + x-nemo-guardrails: + classification: constrained + content: + type: string + minLength: 1 + x-nemo-guardrails: + classification: guarded + subject: + kind: text + role: user + replaceable: true + required: [role, content] + x-nemo-guardrails: + model: ExampleUserMessageProjection + source: '#/components/schemas/ProviderUserMessage' + unknown_fields: configurable + x-nemo-guardrails: + classification: guarded + stream: + type: boolean + default: false + x-nemo-guardrails: + classification: constrained + model: + x-nemo-guardrails: + classification: opaque + required: [messages] + x-nemo-guardrails: + model: ExampleChatGuardedRequestProjection + stream_selector_field: stream +``` + +The schema describes the local request policy. The frontend derives the root +provider source, direction, and capability metadata from the contract and +selected operation. Optional `title` and `x-nemo-guardrails.model` give stable +projection and class-name overrides. `stream_selector_field` identifies the +boolean request field that selects streaming. + +### Complete field accounting + +Every field declared by a bound provider object must be classified exactly +once: + +| Classification | Meaning | +| --- | --- | +| `guarded` | Contains or leads to content inspected by Guardrails. | +| `constrained` | Narrows the provider shape so guarded extraction, replacement, or response handling is safe and unambiguous. | +| `opaque` | Remains provider-owned and is forwarded without Guardrails interpretation. | +| local extension | Does not exist in the provider source; it is an explicitly capability-gated compatibility field. | + +For example: + +```yaml +properties: + input: + type: string + minLength: 1 + x-nemo-guardrails: + classification: guarded + subject: + kind: text + role: user + replaceable: true + + response_format: + type: 'null' + default: null + x-nemo-guardrails: + classification: constrained + gate: disabled + reason: projection_policy.plain_text_output + + temperature: + x-nemo-guardrails: + classification: opaque +``` + +Compilation fails if a field from the provider source reaches this object and +is not reviewed. This is the provider-drift boundary: a new provider field +cannot silently acquire guard semantics. + +`opaque` does not mean “unknown” or “ignored.” It records a deliberate decision +that the field does not affect the guarded capability and can remain under +provider authority. Opaque does not mean trusted or safe: the entire value is +delegated to the provider and must be preserved without local assertions. + +`opaque_fields` is shorthand for individually annotated opaque properties on +this object. Names must be explicit and unique, cannot overlap `properties`, +and cannot use wildcards. It does not review nested fields inside an opaque +value. New known fields at a selected boundary require review even when +runtime unknown fields are allowed. + +### Guarded text subjects + +The `single_text.v1` profile identifies one user or assistant text subject. +Selected native representations do not permit multiple independent subjects. +Replacement may affect only the selected text; unrelated metadata and declared +replacement restrictions must be preserved. + +A direct guarded string declares its provider-neutral meaning: + +```yaml +content: + type: string + minLength: 1 + x-nemo-guardrails: + classification: guarded + subject: + kind: text + role: assistant + replaceable: true +``` + +Subject keys are: + +| Key | Meaning | +| --- | --- | +| `role` | `user` for input or `assistant` for generated output. | +| `replaceable` | Whether a buffered Guardrails decision may replace this provider field. Required by `single_text.v1`. | +| `replacement_blocked_by` | Related provider field that may make replacement unsafe. | +| `replacement_reason` | Structured reason for a restriction on replacement. | + +For example, replacing cited text may invalidate its citations: + +```yaml +subject: + kind: text + role: assistant + replaceable: true + replacement_blocked_by: citations + replacement_reason: provider_integrity.cited_text +``` + +The runtime can replace the text only when the related field is absent. If +`replaceable` is false, or replacement is conditional, a +`replacement_reason` is required. + +### Constraints and capability gates + +Standard JSON Schema keywords describe the narrowed provider value. The +semantic compiler currently recognizes these selection and validation +keywords: + +```text +type, const, default, enum, +minItems, maxItems, minLength, maxLength, pattern +``` + +The compiler rejects unsupported schema keywords and unknown Guardrails keys +with the component and nested property location. It accepts descriptive +annotations such as `description` and `examples` without treating them as +constraints. Opaque properties cannot carry validation assertions: classify a +property as constrained when the guard projection should validate it. A disabled +gate must agree with its null-only schema. + +Examples include selecting one role: + +```yaml +role: + const: user + x-nemo-guardrails: + classification: constrained +``` + +and restricting generation to one output: + +```yaml +n: + const: 1 + default: 1 + x-nemo-guardrails: + classification: constrained + reason: core_capability.single_text_target +``` + +A disabled capability uses a null-only projection: + +```yaml +tools: + type: 'null' + default: null + x-nemo-guardrails: + classification: constrained + gate: disabled + reason: core_capability.tool_content +``` + +`gate: disabled` means the guarded operation does not support that capability; +it does not mean the provider lacks it. A gated field must be constrained and +must provide a reason. A disabled field may be absent or null but must not +enable the unsupported feature. Normalization must not disguise this local +restriction as a provider fact. + +Reason identifiers use one of these namespaces: + +| Prefix | Use | +| --- | --- | +| `core_capability.*` | The shared runtime capability does not yet support the provider feature. | +| `provider_integrity.*` | Accepting or changing the value could invalidate related provider data or protocol guarantees. | +| `projection_policy.*` | The chosen guarded projection deliberately narrows otherwise supported provider behavior. | + +Use a stable lower-case identifier after the prefix, for example +`core_capability.tool_content`. Reasons are reviewable policy identifiers, not +free-form explanations. + +### Defaults + +`default` is an annotation, not permission to insert an absent property into +the forwarded provider payload. Generated models may use defaults internally; +that does not authorize rewriting provider input. See the +[JSON Schema annotation guidance](https://json-schema.org/understanding-json-schema/reference/annotations). + +### Requiredness + +The standard object-level `required` array determines which properties must be +present in the guarded projection. Properties omitted from this array are +optional, even when the provider requires them. Keep a policy `reason` on a +property when deliberately changing provider requiredness. + +```yaml +required: [messages] +``` + +Field-level `x-nemo-guardrails.required` is not accepted. Optionality does not +imply nullability in JSON Schema: the value schema must explicitly permit null. +Provider-required opaque values remain under provider authority rather than +being promoted to local validation. + +### Nested objects and arrays + +Attach object metadata to every nested object that the compiler traverses: + +```yaml +message: + type: object + properties: + content: {} + x-nemo-guardrails: + classification: guarded + model: ExampleMessageProjection + source: '#/components/schemas/ProviderMessage' + unknown_fields: configurable +``` + +For an array, put the nested object metadata on `items` and classify the array +field itself: + +```yaml +messages: + type: array + minItems: 1 + maxItems: 1 + items: + type: object + title: ExampleMessageProjection + properties: {} + x-nemo-guardrails: + model: ExampleMessageProjection + source: '#/components/schemas/ProviderMessage' + unknown_fields: configurable + x-nemo-guardrails: + classification: guarded +``` + +Author a direct guarded array with `minItems: 1` and `maxItems: 1` when the +guarded target is its first item. For union variants, the current +`single_text.v1` profile requires an `exactly_one` matching selection. +Cardinality is therefore part of safety, not merely validation convenience. + +### Union variants + +Arrays and alternatives must identify the supported text unambiguously. +Explicit match selectors must agree with declared constant fields. Authored +alternative order must not supply identity. The `single_text.v1` profile also +requires nonempty text and an unambiguous traversal. + +Use variants when an array contains a provider union and only selected object +variants participate in the guarded capability: + +```yaml +content: + type: array + minItems: 1 + maxItems: 1 + items: + oneOf: + - type: object + title: ExampleTextBlockProjection + properties: + type: + const: text + x-nemo-guardrails: + classification: constrained + text: + type: string + minLength: 1 + x-nemo-guardrails: + classification: guarded + subject: + kind: text + role: assistant + replaceable: true + required: [type, text] + x-nemo-guardrails: + model: ExampleTextBlockProjection + source: '#/components/schemas/ProviderTextBlock' + unknown_fields: configurable + variant: + match: + type: text + cardinality: exactly_one + x-nemo-guardrails: + classification: guarded +``` + +`variant.match` can be omitted when required properties with `const` values +identify the variant. An explicit match must correspond to those selected +constants. `cardinality` is one of `any`, `at_most_one`, or `exactly_one`, although +the current payload profile requires `exactly_one` for the guarded target. + +### Alternative text representations + +Use a field-level `oneOf` when the same semantic text may use different native +encodings, such as a string or a one-element text-block array: + +```yaml +content: + oneOf: + - type: string + minLength: 1 + x-nemo-guardrails: + subject: + kind: text + role: user + replaceable: true + - type: array + minItems: 1 + maxItems: 1 + items: + type: object + properties: + text: + type: string + minLength: 1 + x-nemo-guardrails: + classification: guarded + subject: + kind: text + role: user + replaceable: true + x-nemo-guardrails: + model: ExampleInputTextBlockProjection + source: '#/components/schemas/ProviderInputTextBlock' + unknown_fields: configurable + x-nemo-guardrails: {} + x-nemo-guardrails: + classification: guarded +``` + +Each representation must lead to exactly one text subject with the same +provider-neutral meaning. The compiler derives multiple native bindings for +one semantic subject. + +### Unknown-field policies + +Use standard `additionalProperties` for object closure. Add +`x-nemo-guardrails.unknown_fields: configurable` only for a content boundary +whose closure the runtime may override: + +| Value | Generated behavior | +| --- | --- | +| `additionalProperties: true` or omitted | Accept additional provider fields. | +| `additionalProperties: false` | Reject additional fields, while retaining declared opaque properties. | +| `unknown_fields: configurable` with `additionalProperties: true` | Accept additional fields by default and allow the runtime's closed-content policy to reject unreviewed extras. | + +Use `additionalProperties` for allow/forbid behavior rather than duplicating it +in the extension. + +These policies do not replace field accounting against the pinned provider +contract. Known provider fields at a guarded boundary must still be marked +guarded, constrained, or opaque during compilation. + +### Local extension fields + +Occasionally a compatibility layer adds a field that is not claimed by the +provider source. Mark it explicitly: + +```yaml +reasoning_content: + type: 'null' + default: null + x-nemo-guardrails: + classification: constrained + extension: true + gate: disabled + reason: core_capability.reasoning_content +``` + +A local extension must be a disabled constrained capability. Do not use +`extension: true` to avoid proving that a real provider field exists. + +## Stream Projections + +Stream policy describes decoded provider data events and declared SSE framing. +It does not implicitly upgrade the source to OpenAPI 3.2 transport `itemSchema`. + +A Stream Projection describes complete SSE event payloads. The compiler uses it +to generate typed event models and a stateless per-event classifier. + +The projection does not describe cross-event ordering. Stateful sequence +validation and provider-native error framing remain handwritten stream hooks. + +### Stream root + +```yaml +stream: + title: example.chat.stream.text.v1 + oneOf: + - {} + - {} + x-nemo-guardrails: + event_schema_binding: operation_response + transport: + require_sse_event: false + non_data_shape: '[DONE]' + sentinels: + '[DONE]': '[DONE]' +``` + +Root stream keys are: + +| Key | Meaning | +| --- | --- | +| `event_schema_binding` | `operation_response` when the SSE response reaches the event schema; `component` only when justified by external evidence. | +| `event_schema_reason` | Required reason for a component-only binding and forbidden for an operation-bound one. | +| `transport` | SSE discriminator, event-field, non-data, and sentinel behavior. | + +Transport keys are: + +| Key | Meaning | +| --- | --- | +| `data_discriminator` | JSON property used by the provider stream union discriminator, when present. | +| `require_sse_event` | Require the SSE `event:` field to match the JSON discriminator. | +| `non_data_shape` | Shape name for SSE events without a `data:` field. | +| `sentinels` | Map exact non-JSON `data:` values to stable shape names. | + +When `require_sse_event` is true, `data_discriminator` is required. + +### Stream event review + +Every schema in the selected provider stream union must be accepted or named +in `excluded_stream_events` with a nonblank author reason. Unknown, duplicate, +stale, or simultaneously accepted/excluded entries are errors. A new event +schema must require a new author decision; implicit rejection is insufficient +as source-update review. External provider-error variants retain the existing +evidence-rationale requirement. Nested provider union coverage remains governed +by the existing selection checks; the exclusion list covers top-level event +schemas, not every future nested union alternative. + +### Event families + +Each root `oneOf` entry is an event family: one projection model and one +Guardrails role shared by one or more native provider variants. + +```yaml +- type: object + title: ExampleStreamPayloadProjection + properties: + object: + const: chat.chunk + x-nemo-guardrails: + classification: constrained + choices: + type: array + maxItems: 1 + items: + type: object + properties: + delta: + type: object + properties: + content: + type: string + x-nemo-guardrails: + classification: guarded + subject: + kind: text + role: assistant + x-nemo-guardrails: + classification: guarded + model: ExampleStreamDeltaProjection + unknown_fields: configurable + source: '#/components/schemas/ProviderStreamDelta' + x-nemo-guardrails: + model: ExampleStreamChoiceProjection + unknown_fields: configurable + source: '#/components/schemas/ProviderStreamChoice' + x-nemo-guardrails: + classification: guarded + required: [object, choices] + x-nemo-guardrails: + model: ExampleStreamPayloadProjection + source: '#/components/schemas/ProviderChatStreamEvent' + event: + classification: guarded_delta + variants: + - source_schema: ProviderChatStreamEvent + shape: chat.chunk:content + match: + object: chat.chunk + required_fields: [] + missing_text: opaque + missing_text_shape: chat.chunk:metadata +``` + +Event classifications are: + +| Classification | Runtime role | Meaning | +| --- | --- | --- | +| `guarded_delta` | `GUARDED_TEXT` | Carries a new text delta inspected by Guardrails. | +| `snapshot` | `TEXT_SNAPSHOT` | Carries accumulated text checked against previously observed deltas. | +| `opaque` | `OPAQUE_METADATA` | Contains no guarded text. | +| `provider_error` | `PROVIDER_ERROR` | Provider-native in-stream failure. | + +Each event variant has: + +| Key | Meaning | +| --- | --- | +| `source_schema` | Provider component represented by this variant. | +| `shape` | Stable Guardrails name returned after classification. | +| `match` | Dotted JSON selectors and scalar values that identify the event. | +| `required_fields` | Top-level fields that must be required to distinguish the event. | +| `external_reason` | Evidence reason for a provider-error schema accepted outside the primary stream union. Other external variants are not supported. | + +A variant must have at least one `match` entry or required field. The compiler +proves selectors against the provider schema and rejects duplicate selectors. +Rules must also be semantically disjoint: the runtime rejects an event if more +than one rule matches it. + +### Text paths are derived + +Authors do not write a `text_path`. They mark the nested text property with a +guarded subject. From the preceding example, the compiler derives something +equivalent to: + +```python +StreamEventRule( + shape="chat.chunk:content", + role=StreamEventRole.GUARDED_TEXT, + match=(("object", "chat.chunk"),), + text_path=("choices", 0, "delta", "content"), +) +``` + +The generated path means +`payload["choices"][0]["delta"]["content"]`. Keeping the path derived prevents +the extraction logic from drifting away from the schema that validates it. + +### Events without text + +An opaque event family is not an opaque property: the family still has a +selected schema and constrained fields. Those constraints remain enforced even +though the family has no guarded text subject. + +A structurally valid event in a guarded family may omit its text field. Choose +that behavior explicitly: + +| `missing_text` | Behavior | +| --- | --- | +| `reject` | Reject the event. This is the default. | +| `opaque` | Reclassify it as metadata; `missing_text_shape` may give it a distinct shape, otherwise the original shape is reused. | +| `empty` | Treat it as an empty text snapshot. | + +Only `guarded_delta` families may use `opaque`; only `snapshot` families may +use `empty`. + +For example, Chat Completions chunks may carry role, usage, or finish metadata +without a content delta: + +```yaml +event: + classification: guarded_delta + variants: [...] + missing_text: opaque + missing_text_shape: chat.completion.chunk:metadata +``` + +### Classifier versus hooks + +The generated classifier answers questions about one event in isolation: + +- Is its JSON shape valid? +- Which declared variant matched? +- Does it carry guarded text, a snapshot, metadata, or an error? +- What text should the shared runner inspect? + +Handwritten hooks answer questions that require history or executable provider +behavior: + +- Was a block started before receiving its delta? +- Is this event legal after the preceding event? +- Did the stream end with unfinished provider state? +- How should a Guardrails failure be framed as provider-native SSE? + +An operation with streaming declares a hooks class under its endpoint metadata. +The class must be constructible without arguments and implement +`observe_event`, `validate_end_of_stream`, and `encode_error`. A fresh hooks +instance is created for every upstream stream; the stateless generated +classifier may be shared. + +## Integration and stable identities + +`integration.endpoint` holds error codes and optional route, label, stream-hook, +and API-revision bindings. When `route_path` or `operation_label` is omitted, +the route defaults to the selected provider path and the label is derived from +the operation name. Stateful stream behavior is +still implemented by the declared hook; changing a hook is a behavior change. + +`integration.name` optionally preserves an existing artifact identity. Otherwise +the name is derived from `operationId`, for example `createText` becomes +`create_text`. Root component names, projection references, and operation binding +actions are derived. Existing `title` and `x-nemo-guardrails.model` overrides +remain supported to preserve names; renaming them may change generated imports. +Unnamed object alternatives need an explicit source or one event variant so +their derived identity does not depend on list position. + +Types may be inferred only where unambiguous. A scalar whose provider type is +nullable, a union, or absent needs an explicit type. Selecting an object union +may require `x-nemo-guardrails.source`. No classification or subject is inferred. + +## Endpoint binding + +Runtime metadata belongs in the contract's integration section: + +```yaml +integration: + name: chat + endpoint: + route_path: /v1/chat + operation_label: Example Chat + unsupported_request_code: unsupported_example_chat_shape + unsupported_response_code: unsupported_example_chat_response_shape + stream_hooks: + module: example_guardrails_provider.chat.stream_hooks + name: ExampleChatStreamHooks + api_revision: + module: example_guardrails_provider.chat.api_revision + name: EXAMPLE_API_REVISION +``` + +The compiler derives projection references and the operation declaration. +`integration.name` is optional and otherwise derived from `operationId`. + +Endpoint keys are: + +| Key | Meaning | +| --- | --- | +| `route_path` | Runtime route; it must match the provider operation, optionally with a concrete leading API-version segment. | +| `operation_label` | Human-readable name used in diagnostics. | +| `unsupported_request_code` | Provider-facing error code for unsupported request shapes. | +| `unsupported_response_code` | Provider-facing error code for unsupported response or stream shapes. | +| `stream_hooks` | Optional provider-package Python symbol for stateful streaming behavior. | +| `api_revision` | Optional provider-package `ProviderApiRevisionBinding` symbol. | + +Python symbols must live inside the runtime package declared by +`provider.yaml`. Stream hooks are required when a stream projection is bound +and forbidden when no stream projection exists. + +## `x-nemo-guardrails` quick reference + +The meaning of a key depends on where the extension is attached. The same +extension name is intentionally local rather than one large embedded document. + +### Projection object metadata + +These keys appear on a root projection or nested object Schema Object: + +| Key | Applies to | Meaning | +| --- | --- | --- | +| `model` | payload and stream objects | Optional Python model-name override. An identifier-valued `title` supplies the default. | +| `source` | payload and stream objects | Provider component reference. Root bindings are derived; use it on nested objects that select a named provider schema and omit it for inline objects. | +| `unknown_fields` | payload and stream objects | Optional runtime configurability; ordinary closure comes from `additionalProperties`. | +| `stream_selector_field` | request root | Field whose boolean value selects a streaming response. | +| `event_schema_binding` | stream root | `operation_response` or `component`. | +| `event_schema_reason` | stream root | Required structured reason for a component-only stream binding. | +| `transport` | stream root | Stream framing and discriminator metadata. | + +### Field metadata + +These keys appear on a property Schema Object: + +| Key | Meaning | +| --- | --- | +| `classification` | `guarded`, `constrained`, or `opaque`. | +| `subject` | Direct guarded-text meaning and replacement policy. | +| `reason` | Structured capability, integrity, or projection-policy rationale. | +| `gate` | Currently only `disabled`; requires a constrained field and reason. | +| `extension` | Marks a local compatibility field absent from the provider source. Only valid for a disabled constrained field. | +| `model` | Optional generated name for a nested object, overriding its `title`. | +| `source` | Binds that nested object to a provider component. | +| `unknown_fields` | Sets that nested object's unknown-field policy. | + +Array item objects carry `title`, optional `model`, `source`, and `unknown_fields` on the +`items` Schema Object. A field-level representation branch carries either a +direct `subject` or an annotated `items` object, not an ordinary field +classification. + +### Variant, event, and endpoint metadata + +| Location | Key | Meaning | +| --- | --- | --- | +| object union branch | `variant.match` | Optional selector override; defaults to required properties with `const` values. | +| object union branch | `variant.cardinality` | `any`, `at_most_one`, or `exactly_one`. | +| stream event family | `event.classification` | `guarded_delta`, `snapshot`, `opaque`, or `provider_error`. | +| stream event family | `event.variants` | Provider schemas and selectors represented by the family. | +| stream event family | `event.missing_text` | `reject`, `opaque`, or `empty`. | +| stream event family | `event.missing_text_shape` | Optional alternate shape used when guarded text is absent. | + +## Evidence-backed normalization + +A guard contract narrows a correct provider contract. It should not silently +repair an incomplete one. + +When the pinned provider OpenAPI omits or misstates a fact required for +compilation, record authoritative evidence in `evidence.yaml` and apply a +separate `normalization.overlay.yaml` before guard contracts. + +```yaml +version: 1 +provider: example +primary: + id: example_openapi + kind: openapi + origin: https://example.com/openapi.yaml + revision: provider-revision + sha256: +supplemental: + - id: example_streaming_docs + kind: reviewed_documentation + origin: https://example.com/docs/streaming + revision: reviewed-2026-09-22 + claims: + - POST /chat returns an SSE stream when stream is true + claims_sha256: +``` + +Each normalization action cites one declared evidence source: + +```yaml +overlay: 1.1.0 +info: + title: Complete the example streaming contract + version: 1.0.0 +actions: + - target: $.paths['/chat'].post.responses + description: Add the documented SSE response. + x-guard-evidence: example_streaming_docs + update: + '200': + content: + text/event-stream: + schema: + $ref: '#/components/schemas/ProviderChatStreamEvent' + x-guard-source-evidence: reviewed-2026-09-22 +``` + +Evidence kinds are `openapi`, `sdk_types`, and `reviewed_documentation`. +Machine-readable evidence pins the downloaded artifact with `sha256`. +Reviewed documentation pins a canonical list of claims with `claims_sha256` +because a mutable web page cannot be treated as an immutable artifact. +The digest is SHA-256 over the compact, ASCII JSON representation of the claims +array. For example: + +```bash +uv run --locked python -c 'import hashlib,json; claims=["POST /chat returns SSE"]; print(hashlib.sha256(json.dumps(claims,separators=(",",":"),ensure_ascii=True).encode()).hexdigest())' +``` + +Normalization and guard policy have different responsibilities and formats: + +```text +normalization Overlay: make the provider contract factually complete +guard contract: define the supported guarded subset +``` + +## Reading compiler failures + +Semantic failures report three pieces of information: + +```text +compile + + +``` + +Treat the semantic path as the authoring location to inspect. Common failures +mean: + +| Failure | Likely cause | +| --- | --- | +| `provider contract has unreviewed fields` | A bound provider object contains fields not classified by the projection. | +| `field is absent from provider contract` | The projection claims a field that the pinned provider schema does not expose; use normalization only with evidence. | +| `does not identify one provider schema branch` | A `type`, `const`, or representation selection is ambiguous or unsupported by the provider union. | +| `requires exactly one matching guarded array variant` | The current capability profile cannot identify one text target. Narrow array cardinality or variant selection. | +| `stream event selection is duplicated` | Two stream variants declare the same selector. Give each native shape a unique selector. | +| runtime reports an ambiguous stream event | Different selectors can still match the same payload. Make the authored rules semantically disjoint. | +| `stream text paths require explicit array cardinality` | A generated stream extraction path crosses an unconstrained array. | +| `must bind its declared stream projection` | Endpoint metadata, stream projection, and handwritten hooks are inconsistent. | + +Do not respond to a compiler proof failure by weakening coverage or marking an +unknown content-bearing field opaque without review. The failure is the desired +signal that the guarded boundary changed. + +## Validation and compatibility limits + +There are three different checks: authoring syntax, provider-aware semantic +validation, and runtime enforcement. A generic JSON Schema validator knows only +standard assertions; it does not enforce subjects, configurable policies, +variant cardinality, review decisions, or hook behavior. + +The current frontend requires component-reference operation roots, JSON +requests, HTTP 200 JSON responses, and optional HTTP 200 SSE responses. Only +the documented single-text profiles are supported. Tools, images, audio, +multiple independent guarded messages, structured output, and other semantic +content kinds require explicit shared runtime and compiler capability designs. + +Public JSON Schema and runtime acceptance are not identical in every case. +Selected-variant cardinality, optional/null/default handling, provider-owned +requiredness, and disabled gates need separate conformance work. Characterization +tests record concrete cases; artifact reproducibility is not proof of complete +schema/runtime equivalence. + +If implementation and this semantic specification disagree, record the mismatch +and add a conformance case; do not silently weaken either definition. + +Human authoring trials are still needed to assess readability and editing effort. +The alpha label makes no stable compatibility promise; see the +[version policy](README.md#version-and-stability). diff --git a/tests/server/experimental/test_guard_contract.py b/tests/server/experimental/test_guard_contract.py new file mode 100644 index 0000000000..273b319889 --- /dev/null +++ b/tests/server/experimental/test_guard_contract.py @@ -0,0 +1,84 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import re +from pathlib import Path + +import pytest +import yaml +from jsonschema import Draft202012Validator +from jsonschema.exceptions import ValidationError + +REPOSITORY_ROOT = Path(__file__).parents[3] +CONTRACTS_ROOT = REPOSITORY_ROOT / "nemoguardrails/server/experimental/contracts" +SCHEMA_PATH = CONTRACTS_ROOT / "guard-contract.schema.json" +MINIMAL_CONTRACT_PATH = CONTRACTS_ROOT / "minimal.guard.example.yaml" +MARKDOWN_LINK = re.compile(r"\[[^]]+\]\(([^)]+)\)") + + +@pytest.fixture(scope="module") +def guard_contract_schema() -> dict: + """Load the guard contract schema shared by the validation tests.""" + return json.loads(SCHEMA_PATH.read_text()) + + +@pytest.fixture(scope="module") +def minimal_guard_contract() -> dict: + """Load the minimal guard contract example.""" + return yaml.safe_load(MINIMAL_CONTRACT_PATH.read_text()) + + +def test_guard_contract_schema_is_valid(guard_contract_schema: dict) -> None: + """The guard contract schema is a valid JSON Schema 2020-12 document.""" + Draft202012Validator.check_schema(guard_contract_schema) + + +def test_minimal_guard_contract_matches_schema(guard_contract_schema: dict, minimal_guard_contract: dict) -> None: + """The minimal guard contract demonstrates a schema-valid document.""" + Draft202012Validator(guard_contract_schema).validate(minimal_guard_contract) + + +def test_guard_contract_rejects_unknown_version(guard_contract_schema: dict, minimal_guard_contract: dict) -> None: + """The schema rejects contracts authored for an unsupported version.""" + contract = minimal_guard_contract | {"version": "1.0.0"} + + with pytest.raises(ValidationError): + Draft202012Validator(guard_contract_schema).validate(contract) + + +def test_guard_contract_rejects_unknown_top_level_key( + guard_contract_schema: dict, minimal_guard_contract: dict +) -> None: + """The schema rejects misspelled or unsupported top-level keys.""" + contract = minimal_guard_contract | {"requests": {}} + + with pytest.raises(ValidationError): + Draft202012Validator(guard_contract_schema).validate(contract) + + +def test_contract_documentation_has_no_broken_local_links() -> None: + """Relative links in the contract documentation resolve locally.""" + broken_links: list[str] = [] + + for document in CONTRACTS_ROOT.glob("*.md"): + for link in MARKDOWN_LINK.findall(document.read_text()): + if "://" in link or link.startswith("#"): + continue + target, _, _fragment = link.partition("#") + if target and not (document.parent / target).exists(): + broken_links.append(f"{document.name}: {link}") + + assert broken_links == [] From 4b78e315289fc46a98d3f76d1b7d04101a7e173b Mon Sep 17 00:00:00 2001 From: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com> Date: Sat, 26 Sep 2026 22:49:57 +0200 Subject: [PATCH 2/8] fix(server): tighten guard contract validation Signed-off-by: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com> --- .../contracts/guard-contract.schema.json | 2 +- .../experimental/contracts/reference.md | 118 +++++++++--------- .../experimental/test_guard_contract.py | 43 ++++++- 3 files changed, 99 insertions(+), 64 deletions(-) diff --git a/nemoguardrails/server/experimental/contracts/guard-contract.schema.json b/nemoguardrails/server/experimental/contracts/guard-contract.schema.json index 5a55ba3b7b..b6e3c80230 100644 --- a/nemoguardrails/server/experimental/contracts/guard-contract.schema.json +++ b/nemoguardrails/server/experimental/contracts/guard-contract.schema.json @@ -492,7 +492,7 @@ } }, "patternProperties": { - "^x-(?!nemo-guardrails$)": {} + "^x-(?!nemo-guardrails)": {} } }, "field": { diff --git a/nemoguardrails/server/experimental/contracts/reference.md b/nemoguardrails/server/experimental/contracts/reference.md index aeccd9e791..1cf5d81e80 100644 --- a/nemoguardrails/server/experimental/contracts/reference.md +++ b/nemoguardrails/server/experimental/contracts/reference.md @@ -206,11 +206,13 @@ minItems, maxItems, minLength, maxLength, pattern ``` The compiler rejects unsupported schema keywords and unknown Guardrails keys -with the component and nested property location. It accepts descriptive -annotations such as `description` and `examples` without treating them as -constraints. Opaque properties cannot carry validation assertions: classify a -property as constrained when the guard projection should validate it. A disabled -gate must agree with its null-only schema. +with the component and nested property location. Other `x-*` schema annotations +are available to non-Guardrails tooling and ignored by Guardrails. Keys beginning +with `x-nemo-guardrails` are reserved; only the exact `x-nemo-guardrails` key is +accepted. Descriptive annotations such as `description` and `examples` are not +treated as constraints. Opaque properties cannot carry validation assertions: +classify a property as constrained when the guard projection should validate it. +A disabled gate must agree with its null-only schema. Examples include selecting one role: @@ -475,8 +477,54 @@ validation and provider-native error framing remain handwritten stream hooks. stream: title: example.chat.stream.text.v1 oneOf: - - {} - - {} + - type: object + title: ExampleStreamPayloadProjection + properties: + object: + const: chat.chunk + x-nemo-guardrails: + classification: constrained + choices: + type: array + maxItems: 1 + items: + type: object + properties: + delta: + type: object + properties: + content: + type: string + x-nemo-guardrails: + classification: guarded + subject: + kind: text + role: assistant + x-nemo-guardrails: + classification: guarded + model: ExampleStreamDeltaProjection + unknown_fields: configurable + source: '#/components/schemas/ProviderStreamDelta' + x-nemo-guardrails: + model: ExampleStreamChoiceProjection + unknown_fields: configurable + source: '#/components/schemas/ProviderStreamChoice' + x-nemo-guardrails: + classification: guarded + required: [object, choices] + x-nemo-guardrails: + model: ExampleStreamPayloadProjection + source: '#/components/schemas/ProviderChatStreamEvent' + event: + classification: guarded_delta + variants: + - source_schema: ProviderChatStreamEvent + shape: chat.chunk:content + match: + object: chat.chunk + required_fields: [] + missing_text: opaque + missing_text_shape: chat.chunk:metadata x-nemo-guardrails: event_schema_binding: operation_response transport: @@ -518,59 +566,9 @@ schemas, not every future nested union alternative. ### Event families -Each root `oneOf` entry is an event family: one projection model and one -Guardrails role shared by one or more native provider variants. - -```yaml -- type: object - title: ExampleStreamPayloadProjection - properties: - object: - const: chat.chunk - x-nemo-guardrails: - classification: constrained - choices: - type: array - maxItems: 1 - items: - type: object - properties: - delta: - type: object - properties: - content: - type: string - x-nemo-guardrails: - classification: guarded - subject: - kind: text - role: assistant - x-nemo-guardrails: - classification: guarded - model: ExampleStreamDeltaProjection - unknown_fields: configurable - source: '#/components/schemas/ProviderStreamDelta' - x-nemo-guardrails: - model: ExampleStreamChoiceProjection - unknown_fields: configurable - source: '#/components/schemas/ProviderStreamChoice' - x-nemo-guardrails: - classification: guarded - required: [object, choices] - x-nemo-guardrails: - model: ExampleStreamPayloadProjection - source: '#/components/schemas/ProviderChatStreamEvent' - event: - classification: guarded_delta - variants: - - source_schema: ProviderChatStreamEvent - shape: chat.chunk:content - match: - object: chat.chunk - required_fields: [] - missing_text: opaque - missing_text_shape: chat.chunk:metadata -``` +Each root `oneOf` entry, such as `ExampleStreamPayloadProjection` in the +preceding example, is an event family: one projection model and one Guardrails +role shared by one or more native provider variants. Event classifications are: diff --git a/tests/server/experimental/test_guard_contract.py b/tests/server/experimental/test_guard_contract.py index 273b319889..20f2c988ff 100644 --- a/tests/server/experimental/test_guard_contract.py +++ b/tests/server/experimental/test_guard_contract.py @@ -15,6 +15,7 @@ import json import re +from copy import deepcopy from pathlib import Path import pytest @@ -26,19 +27,21 @@ CONTRACTS_ROOT = REPOSITORY_ROOT / "nemoguardrails/server/experimental/contracts" SCHEMA_PATH = CONTRACTS_ROOT / "guard-contract.schema.json" MINIMAL_CONTRACT_PATH = CONTRACTS_ROOT / "minimal.guard.example.yaml" +REFERENCE_PATH = CONTRACTS_ROOT / "reference.md" MARKDOWN_LINK = re.compile(r"\[[^]]+\]\(([^)]+)\)") +STREAM_ROOT_EXAMPLE = re.compile(r"### Stream root\n\n```yaml\n(?P.*?)\n```", re.DOTALL) @pytest.fixture(scope="module") def guard_contract_schema() -> dict: """Load the guard contract schema shared by the validation tests.""" - return json.loads(SCHEMA_PATH.read_text()) + return json.loads(SCHEMA_PATH.read_text(encoding="utf-8")) @pytest.fixture(scope="module") def minimal_guard_contract() -> dict: """Load the minimal guard contract example.""" - return yaml.safe_load(MINIMAL_CONTRACT_PATH.read_text()) + return yaml.safe_load(MINIMAL_CONTRACT_PATH.read_text(encoding="utf-8")) def test_guard_contract_schema_is_valid(guard_contract_schema: dict) -> None: @@ -69,12 +72,46 @@ def test_guard_contract_rejects_unknown_top_level_key( Draft202012Validator(guard_contract_schema).validate(contract) +def test_guard_contract_allows_non_guardrails_schema_extensions( + guard_contract_schema: dict, minimal_guard_contract: dict +) -> None: + """Non-Guardrails schema extensions remain available to other tooling.""" + contract = deepcopy(minimal_guard_contract) + contract["request"]["x-provider-annotation"] = {"value": "example"} + + Draft202012Validator(guard_contract_schema).validate(contract) + + +def test_guard_contract_rejects_guardrails_extension_lookalikes( + guard_contract_schema: dict, minimal_guard_contract: dict +) -> None: + """The Guardrails extension namespace rejects misspelled sibling keys.""" + contract = deepcopy(minimal_guard_contract) + contract["request"]["x-nemo-guardrails-extra"] = {} + + with pytest.raises(ValidationError): + Draft202012Validator(guard_contract_schema).validate(contract) + + +def test_stream_root_documentation_example_matches_schema( + guard_contract_schema: dict, minimal_guard_contract: dict +) -> None: + """The documented stream root forms a schema-valid contract section.""" + reference = REFERENCE_PATH.read_text(encoding="utf-8") + match = STREAM_ROOT_EXAMPLE.search(reference) + assert match is not None + contract = deepcopy(minimal_guard_contract) + contract.update(yaml.safe_load(match.group("yaml"))) + + Draft202012Validator(guard_contract_schema).validate(contract) + + def test_contract_documentation_has_no_broken_local_links() -> None: """Relative links in the contract documentation resolve locally.""" broken_links: list[str] = [] for document in CONTRACTS_ROOT.glob("*.md"): - for link in MARKDOWN_LINK.findall(document.read_text()): + for link in MARKDOWN_LINK.findall(document.read_text(encoding="utf-8")): if "://" in link or link.startswith("#"): continue target, _, _fragment = link.partition("#") From 4a685875f93700c9399db262e100ca7ce9b59681 Mon Sep 17 00:00:00 2001 From: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:43:45 +0200 Subject: [PATCH 3/8] docs(server): scope guard contract docs to the format Signed-off-by: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com> --- .../server/experimental/contracts/README.md | 138 +++---- .../experimental/contracts/reference.md | 369 ++++++------------ 2 files changed, 168 insertions(+), 339 deletions(-) diff --git a/nemoguardrails/server/experimental/contracts/README.md b/nemoguardrails/server/experimental/contracts/README.md index 674740b2e0..c179da2a18 100644 --- a/nemoguardrails/server/experimental/contracts/README.md +++ b/nemoguardrails/server/experimental/contracts/README.md @@ -1,14 +1,17 @@ # Guarded provider contracts -A guard contract makes the guardrail boundary readable: which fields contain -guarded text, which values are constrained, and which data stays opaque under -provider authority. You do not need to read Python to understand that policy. -The deployment separately decides which rails inspect the selected text. +A guard contract makes the guardrail boundary of a provider operation readable: +which fields contain guarded content, which values are constrained, and which +data stays opaque under provider authority. You do not need to read Python to +understand that policy. Your deployment separately decides which rails inspect +the selected content. Status: experimental, `1.0.0-alpha.1`. The provider OpenAPI remains authoritative -for HTTP shapes. This is a custom contract format using JSON Schema vocabulary, -not an OpenAPI document or Overlay. Overlay 1.1 is used for normalization and -portable export. +for HTTP shapes. A guard contract is a custom format for the NVIDIA NeMo +Guardrails library that uses JSON Schema vocabulary with `x-nemo-guardrails` +annotations. It is neither an OpenAPI document nor an +[OpenAPI Overlay](https://spec.openapis.org/overlay/v1.1.0.html), and generic +OpenAPI or JSON Schema tools do not apply its guardrail semantics. ## A complete small contract @@ -56,69 +59,46 @@ integration: This [executable example](minimal.guard.example.yaml) is paired with a [small provider description](minimal.openapi.example.yaml). It guards the request's `prompt` and response's `text`, delegates `model` to the provider, and -constrains response `status` to `complete`. `replaceable: true` permits replacing -the selected text while preserving unrelated data. +constrains response `status` to `complete`. `replaceable: true` declares that +the selected text may be replaced while preserving unrelated data. It records +policy only: the guarded proxy does not execute replacement yet and rejects every +replacement decision. -`operationId` selects one operation in the normalized provider OpenAPI. +`operationId` selects one operation in the provider OpenAPI. `request` and `response` are required; add `stream` for streaming and `excluded_stream_events` for deliberately unsupported event schemas. -`integration` holds runtime bindings, separate from policy. The supported -profiles and binding restrictions are listed in the -[reference](reference.md#validation-and-compatibility-limits). +`integration` holds runtime bindings, separate from policy. ## Reading the policy | Declaration | Meaning | | --- | --- | -| `classification: guarded` | Contains guarded text directly or through explicitly described children. | -| `classification: constrained` | Locally restricts structure or values without making that field a text subject. | -| `classification: opaque` | Delegates the entire value to the provider; Guardrails does not inspect its nested shape. | +| `classification: guarded` | Contains guarded content directly or through explicitly described children. | +| `classification: constrained` | Locally restricts structure or values without making that field a subject. | +| `classification: opaque` | Delegates the entire value to the provider; the library does not inspect its nested shape. | | `opaque_fields: [id, model]` | Shorthand for explicit opaque properties on this object only. | -| `subject` | Identifies text, its user/assistant role, and replacement policy. | -| `reason` | Records why a restriction exists, using the existing namespaced reason vocabulary. | +| `subject` | Identifies guarded content: its kind, its user/assistant role, and its replacement policy. | +| `reason` | Records why a restriction exists as a namespaced identifier, such as `core_capability.tool_content`. | -A guarded field can also have constraints. These are not three separate maps -or mutually exclusive sets of schema keywords. `opaque` is not a safety claim: -authors must decide whether a value can affect guarded content before delegating -it. Nested provider changes inside an opaque value do not trigger local review. - -Every selected object must account for every known provider property exactly -once. Name opaque fields explicitly; wildcards, duplicates, and overlap with -`properties` are errors. Unknown runtime keys follow `additionalProperties` -and, where declared, `unknown_fields: configurable`. Allowing unknown keys does -not waive review of newly documented provider fields at a selected boundary. - -The [reference](reference.md) puts each annotation beside its semantics, -examples, and edge cases, including requiredness, nulls, defaults, replacement, -and streaming. +Each selected object accounts for every known provider property exactly once. +This version supports one subject kind, `text`, because rails currently inspect +text only. +The [reference](reference.md) defines each annotation with its semantics, +examples, and edge cases, including opaque values, unknown fields, requiredness, +nulls, defaults, replacement, and streaming. ## Version and stability -The only accepted contract version is `"1.0.0-alpha.1"`. It identifies this -experimental authoring envelope and its documented semantics. Unsupported -versions and unknown keys are errors; there are no version aliases or alternate -layouts. - -A bare `version: 1` would identify a format, not inherently promise stability. -The explicit prerelease label makes the status clear: authoring and semantics -may change before a stable `1.0.0`. Increment the alpha revision when publishing -an incompatible contract revision; never silently reinterpret a published -revision. This follows [Semantic Versioning](https://semver.org/#spec-item-9). - -Contract versions are independent of `overlay: 1.1.0`, provider manifest -`version: 1`, generated annotation versions, and capability profile identifiers. -Those identify different formats or semantic boundaries. +The only accepted contract version is `"1.0.0-alpha.1"`. Unsupported versions and +unknown keys are errors; there are no version aliases or alternate layouts. +Authoring and semantics may change before a stable `1.0.0`. Increment the alpha +revision when you publish an incompatible contract revision, and never +reinterpret a published revision. This follows +[Semantic Versioning](https://semver.org/#spec-item-9). -List guard contracts directly in `provider.yaml`: - -```yaml -operations: - - chat-completions.guard.yaml -``` - -Each operation has one maintained contract. OpenAPI Overlays are used for -evidence-backed normalization and portable export, not as operation-authoring -inputs. +Contract versions are independent of capability profile identifiers such as +`single_text.v1`, which identify semantic boundaries rather than the document +format. Each operation has one maintained contract. ## Editor support and validation @@ -137,56 +117,34 @@ the equivalent workspace setting is: The schema offers key/value completion and catches misspellings, wrong types, unsupported schema keywords, blank exclusion reasons, and malformed integration -metadata. It is not proof of provider compatibility. Provider coverage, source -reachability, text-target cardinality, hook compatibility, and runtime capability -require semantic validation against the provider source. Provider-aware -validation and generation are introduced with the compiler. - -## Authoring workflow - -Start from the [minimal contract](minimal.guard.example.yaml) and review it -against the selected provider operation. Classify every selected provider field, -declare accepted or explicitly excluded stream events when streaming is present, -and put runtime bindings under `integration`. List the `*.guard.yaml` filename -in the provider manifest. +metadata. It does not prove provider compatibility; see the +[validation limits](reference.md#validation-and-compatibility-limits). -Use schema validation while authoring. When changing a constraint, edit that -field's schema and reason, then review it against the pinned provider -description. Do not mark a newly documented content-bearing field opaque merely -to silence a review error. +## Authoring and review -## Author review checklist +Start from the [minimal contract](minimal.guard.example.yaml), review it against +the selected provider operation, and validate it with the schema while you edit. +When you change a constraint, edit that field's schema and reason together. Do +not mark a newly documented content-bearing field opaque to silence a review +error. -Before submitting a new or changed operation, verify: +Before you submit a new or changed operation, verify: - The provider source is immutable, pinned, and digest-verified. -- Normalization actions cite evidence and contain provider facts, not Guardrails - policy. - The guard contract uses standard Schema Objects for structure. - Every provider field at every guarded object boundary is classified once. - Every opaque field has been reviewed as independent of guarded content. - Every constrained field is necessary to make the current capability safe and unambiguous. - Every disabled feature has a structured reason. -- Guarded text has the correct role and explicit replacement policy. -- Arrays and unions identify exactly the supported text target. +- Every subject has the correct kind, role, and explicit replacement policy. +- Arrays and unions identify exactly the supported subject. - Stream event variants are disjoint and cover every accepted provider branch. - Stateful stream behavior remains in handwritten hooks rather than the contract. -- Endpoint symbols live inside the declared runtime package. +- Endpoint symbols live inside the provider's runtime package. - Schema validation and the relevant runtime tests succeed. -## Standards boundary - -This document format is custom, using familiar JSON Schema vocabulary and -explicit `x-nemo-guardrails` semantics. It is neither an OpenAPI document nor an -OpenAPI Overlay. Its exported transformation follows -[Overlay 1.1](https://spec.openapis.org/overlay/v1.1.0.html); ordinary Overlay -tools apply the transformation, not guardrail behavior. - -See the [semantic limits](reference.md#validation-and-compatibility-limits) -for the current implementation boundary and known schema/runtime differences. - ## Provider contracts Provider-specific contracts are introduced with their provider capabilities. diff --git a/nemoguardrails/server/experimental/contracts/reference.md b/nemoguardrails/server/experimental/contracts/reference.md index 1cf5d81e80..672575c1b1 100644 --- a/nemoguardrails/server/experimental/contracts/reference.md +++ b/nemoguardrails/server/experimental/contracts/reference.md @@ -1,24 +1,22 @@ # Guard contract reference -This is the syntax and semantic specification for `1.0.0-alpha.1` -(experimental). Start with the [README](README.md) for a complete small example -and the editing workflow. This reference defines what an implementation must -preserve, not just how the compiler currently generates Python. +This is the syntax and semantic specification for guard contract version +`1.0.0-alpha.1` (experimental) in the NVIDIA NeMo Guardrails library. Start with +the [README](README.md) for a complete small example and the editing workflow. +This reference defines what an implementation must preserve. The [authoring schema](guard-contract.schema.json) defines accepted syntax: a limited subset of JSON Schema with local `x-nemo-guardrails` annotations. -Generic JSON Schema tools do not enforce Guardrails-specific semantics. ## Authority and scope The pinned provider description owns HTTP paths, source schemas, and provider -facts. Evidence-backed normalization corrects that description before guard -policy is interpreted. A guard contract selects a supported boundary; it does -not redefine the whole provider API or claim to configure all detectors, -prompts, input/output rails, or deployment settings. +facts. A guard contract selects a supported boundary; it does not redefine the +whole provider API or claim to configure all detectors, prompts, input/output +rails, or deployment settings. -Request and response policy describes structural acceptance and text targets. -The deployment separately selects which rails inspect that text and whether a +Request and response policy describes structural acceptance and guarded subjects. +The deployment separately selects which rails inspect that content and whether a rail result permits, rejects, or replaces it. A marked subject is a guardable surface, not a guarantee that every deployment runs a particular safety check. @@ -30,12 +28,9 @@ introduced with their provider capabilities. For authoring and validation steps, use the [guard contract guide](README.md). -Root component names, projection inventories, operation bindings, and source -bindings are derived. They are not a second set of authoring decisions. - ## Payload Projections -A Payload Projection is a normal OpenAPI Schema Object with local Guardrails +A Payload Projection is a normal OpenAPI Schema Object with local guardrail annotations. It describes either a request or a buffered JSON response. ### Minimal request example @@ -89,11 +84,12 @@ request: stream_selector_field: stream ``` -The schema describes the local request policy. The frontend derives the root -provider source, direction, and capability metadata from the contract and -selected operation. Optional `title` and `x-nemo-guardrails.model` give stable -projection and class-name overrides. `stream_selector_field` identifies the -boolean request field that selects streaming. +The schema describes the local request policy. Do not declare the source +schema, direction, or capability profile on the projection root: they come from +`operationId`, the `request` or `response` key, and the top-level `profile`. +Optional `title` and `x-nemo-guardrails.model` give stable projection and +class-name overrides. `stream_selector_field` identifies the boolean request +field that selects streaming. ### Complete field accounting @@ -102,9 +98,9 @@ once: | Classification | Meaning | | --- | --- | -| `guarded` | Contains or leads to content inspected by Guardrails. | +| `guarded` | Contains or leads to content that rails inspect. | | `constrained` | Narrows the provider shape so guarded extraction, replacement, or response handling is safe and unambiguous. | -| `opaque` | Remains provider-owned and is forwarded without Guardrails interpretation. | +| `opaque` | Remains provider-owned and is forwarded without interpretation by the library. | | local extension | Does not exist in the provider source; it is an explicitly capability-gated compatibility field. | For example: @@ -134,14 +130,15 @@ properties: classification: opaque ``` -Compilation fails if a field from the provider source reaches this object and -is not reviewed. This is the provider-drift boundary: a new provider field -cannot silently acquire guard semantics. +This is the provider-drift boundary: a field that the provider adds later must +not reach the guarded boundary without an explicit review decision. +Provider-aware validation, which compares the contract with the pinned provider +source, enforces this; it is not part of this format definition. -`opaque` does not mean “unknown” or “ignored.” It records a deliberate decision +`opaque` does not mean "unknown" or "ignored." It records a deliberate decision that the field does not affect the guarded capability and can remain under provider authority. Opaque does not mean trusted or safe: the entire value is -delegated to the provider and must be preserved without local assertions. +delegated to the provider. `opaque_fields` is shorthand for individually annotated opaque properties on this object. Names must be explicit and unique, cannot overlap `properties`, @@ -149,7 +146,12 @@ and cannot use wildcards. It does not review nested fields inside an opaque value. New known fields at a selected boundary require review even when runtime unknown fields are allowed. -### Guarded text subjects +### Guarded subjects + +A subject marks the content that rails inspect and declares its meaning: its +kind, its role, and its replacement policy. This version supports one kind, +`text`, because rails currently inspect text only. Other kinds require new +capability profiles. The `single_text.v1` profile identifies one user or assistant text subject. Selected native representations do not permit multiple independent subjects. @@ -174,8 +176,9 @@ Subject keys are: | Key | Meaning | | --- | --- | -| `role` | `user` for input or `assistant` for generated output. | -| `replaceable` | Whether a buffered Guardrails decision may replace this provider field. Required by `single_text.v1`. | +| `kind` | Required content kind. This version accepts only `text`. | +| `role` | Required. `user` for input or `assistant` for generated output. | +| `replaceable` | Whether a buffered rail decision may replace this provider field. The `single_text.v1` profile requires it; the authoring schema does not enforce that requirement. | | `replacement_blocked_by` | Related provider field that may make replacement unsafe. | | `replacement_reason` | Structured reason for a restriction on replacement. | @@ -190,26 +193,28 @@ subject: replacement_reason: provider_integrity.cited_text ``` -The runtime can replace the text only when the related field is absent. If -`replaceable` is false, or replacement is conditional, a -`replacement_reason` is required. +Replacement is permitted only when the related field is absent. If +`replaceable` is false, or replacement is conditional, a `replacement_reason` +is required. + +Replacement policy is declared, not yet executed: the guarded proxy rejects every +replacement decision until safe replacement is implemented. Declaring it now +keeps each contract's replacement review next to the field it protects. ### Constraints and capability gates -Standard JSON Schema keywords describe the narrowed provider value. The -semantic compiler currently recognizes these selection and validation -keywords: +Standard JSON Schema keywords describe the narrowed provider value. The format +accepts these selection and validation keywords: ```text type, const, default, enum, minItems, maxItems, minLength, maxLength, pattern ``` -The compiler rejects unsupported schema keywords and unknown Guardrails keys -with the component and nested property location. Other `x-*` schema annotations -are available to non-Guardrails tooling and ignored by Guardrails. Keys beginning -with `x-nemo-guardrails` are reserved; only the exact `x-nemo-guardrails` key is -accepted. Descriptive annotations such as `description` and `examples` are not +The authoring schema rejects other schema keywords and unknown +`x-nemo-guardrails` keys. Other `x-*` schema annotations are available to other +tooling and ignored by the library. Keys beginning with `x-nemo-guardrails` are +reserved; only the exact `x-nemo-guardrails` key is accepted. Descriptive annotations such as `description` and `examples` are not treated as constraints. Opaque properties cannot carry validation assertions: classify a property as constrained when the guard projection should validate it. A disabled gate must agree with its null-only schema. @@ -249,8 +254,8 @@ tools: `gate: disabled` means the guarded operation does not support that capability; it does not mean the provider lacks it. A gated field must be constrained and must provide a reason. A disabled field may be absent or null but must not -enable the unsupported feature. Normalization must not disguise this local -restriction as a provider fact. +enable the unsupported feature. Do not disguise this local restriction as a +provider fact. Reason identifiers use one of these namespaces: @@ -267,7 +272,7 @@ free-form explanations. ### Defaults `default` is an annotation, not permission to insert an absent property into -the forwarded provider payload. Generated models may use defaults internally; +the forwarded provider payload. An implementation may use defaults internally; that does not authorize rewriting provider input. See the [JSON Schema annotation guidance](https://json-schema.org/understanding-json-schema/reference/annotations). @@ -289,7 +294,7 @@ being promoted to local validation. ### Nested objects and arrays -Attach object metadata to every nested object that the compiler traverses: +Attach object metadata to every nested object that the projection traverses: ```yaml message: @@ -418,9 +423,8 @@ content: classification: guarded ``` -Each representation must lead to exactly one text subject with the same -provider-neutral meaning. The compiler derives multiple native bindings for -one semantic subject. +Each representation must lead to exactly one subject with the same +provider-neutral meaning. ### Unknown-field policies @@ -428,7 +432,7 @@ Use standard `additionalProperties` for object closure. Add `x-nemo-guardrails.unknown_fields: configurable` only for a content boundary whose closure the runtime may override: -| Value | Generated behavior | +| Value | Behavior | | --- | --- | | `additionalProperties: true` or omitted | Accept additional provider fields. | | `additionalProperties: false` | Reject additional fields, while retaining declared opaque properties. | @@ -439,7 +443,7 @@ in the extension. These policies do not replace field accounting against the pinned provider contract. Known provider fields at a guarded boundary must still be marked -guarded, constrained, or opaque during compilation. +guarded, constrained, or opaque. ### Local extension fields @@ -465,11 +469,10 @@ A local extension must be a disabled constrained capability. Do not use Stream policy describes decoded provider data events and declared SSE framing. It does not implicitly upgrade the source to OpenAPI 3.2 transport `itemSchema`. -A Stream Projection describes complete SSE event payloads. The compiler uses it -to generate typed event models and a stateless per-event classifier. - -The projection does not describe cross-event ordering. Stateful sequence -validation and provider-native error framing remain handwritten stream hooks. +A Stream Projection describes complete SSE event payloads, one event at a +time. It does not describe cross-event ordering. Stateful sequence validation +and provider-native error framing belong to the stream hooks bound under +`integration.endpoint.stream_hooks`. ### Stream root @@ -556,68 +559,63 @@ When `require_sse_event` is true, `data_discriminator` is required. ### Stream event review Every schema in the selected provider stream union must be accepted or named -in `excluded_stream_events` with a nonblank author reason. Unknown, duplicate, -stale, or simultaneously accepted/excluded entries are errors. A new event -schema must require a new author decision; implicit rejection is insufficient -as source-update review. External provider-error variants retain the existing -evidence-rationale requirement. Nested provider union coverage remains governed -by the existing selection checks; the exclusion list covers top-level event -schemas, not every future nested union alternative. +in `excluded_stream_events` with a nonblank author reason: + +```yaml +excluded_stream_events: + - reason: outside_single_text_stream_profile + sources: [ProviderAudioDeltaEvent, ProviderAudioDoneEvent] +``` + +Unknown, duplicate, stale, or simultaneously accepted/excluded entries are +errors. A new event schema must require a new author decision; implicit +rejection is insufficient as source-update review. External provider-error +variants still need an `external_reason`. Nested provider unions are covered by +the ordinary selection rules; the exclusion list covers top-level event schemas, +not every future nested union alternative. ### Event families Each root `oneOf` entry, such as `ExampleStreamPayloadProjection` in the -preceding example, is an event family: one projection model and one Guardrails +preceding example, is an event family: one projection model and one guardrail role shared by one or more native provider variants. Event classifications are: -| Classification | Runtime role | Meaning | -| --- | --- | --- | -| `guarded_delta` | `GUARDED_TEXT` | Carries a new text delta inspected by Guardrails. | -| `snapshot` | `TEXT_SNAPSHOT` | Carries accumulated text checked against previously observed deltas. | -| `opaque` | `OPAQUE_METADATA` | Contains no guarded text. | -| `provider_error` | `PROVIDER_ERROR` | Provider-native in-stream failure. | +| Classification | Meaning | +| --- | --- | +| `guarded_delta` | Carries a new text delta that rails inspect. | +| `snapshot` | Carries accumulated text checked against previously observed deltas. | +| `opaque` | Contains no guarded content. | +| `provider_error` | Provider-native in-stream failure. | Each event variant has: | Key | Meaning | | --- | --- | | `source_schema` | Provider component represented by this variant. | -| `shape` | Stable Guardrails name returned after classification. | +| `shape` | Stable shape name returned after classification. | | `match` | Dotted JSON selectors and scalar values that identify the event. | | `required_fields` | Top-level fields that must be required to distinguish the event. | | `external_reason` | Evidence reason for a provider-error schema accepted outside the primary stream union. Other external variants are not supported. | -A variant must have at least one `match` entry or required field. The compiler -proves selectors against the provider schema and rejects duplicate selectors. -Rules must also be semantically disjoint: the runtime rejects an event if more -than one rule matches it. +A variant must have at least one `match` entry or required field. Selectors +must be provable against the provider schema and must not be duplicated. Rules +must also be semantically disjoint: an event that matches more than one rule is +rejected. ### Text paths are derived -Authors do not write a `text_path`. They mark the nested text property with a -guarded subject. From the preceding example, the compiler derives something -equivalent to: - -```python -StreamEventRule( - shape="chat.chunk:content", - role=StreamEventRole.GUARDED_TEXT, - match=(("object", "chat.chunk"),), - text_path=("choices", 0, "delta", "content"), -) -``` - -The generated path means -`payload["choices"][0]["delta"]["content"]`. Keeping the path derived prevents -the extraction logic from drifting away from the schema that validates it. +Authors do not write a text path. They mark the nested text property with a +guarded subject, and the path follows from that marking. In the preceding +example it is `choices[0].delta.content`. Keeping the path derived prevents +extraction from drifting away from the schema that validates it. ### Events without text An opaque event family is not an opaque property: the family still has a selected schema and constrained fields. Those constraints remain enforced even -though the family has no guarded text subject. +though the family has no subject. A structurally valid event in a guarded family may omit its text field. Choose that behavior explicitly: @@ -631,63 +629,24 @@ that behavior explicitly: Only `guarded_delta` families may use `opaque`; only `snapshot` families may use `empty`. -For example, Chat Completions chunks may carry role, usage, or finish metadata -without a content delta: +For example, the chat chunks in the stream root example may carry role, usage, +or finish metadata without a content delta: ```yaml event: classification: guarded_delta variants: [...] missing_text: opaque - missing_text_shape: chat.completion.chunk:metadata + missing_text_shape: chat.chunk:metadata ``` -### Classifier versus hooks - -The generated classifier answers questions about one event in isolation: - -- Is its JSON shape valid? -- Which declared variant matched? -- Does it carry guarded text, a snapshot, metadata, or an error? -- What text should the shared runner inspect? - -Handwritten hooks answer questions that require history or executable provider -behavior: - -- Was a block started before receiving its delta? -- Is this event legal after the preceding event? -- Did the stream end with unfinished provider state? -- How should a Guardrails failure be framed as provider-native SSE? - -An operation with streaming declares a hooks class under its endpoint metadata. -The class must be constructible without arguments and implement -`observe_event`, `validate_end_of_stream`, and `encode_error`. A fresh hooks -instance is created for every upstream stream; the stateless generated -classifier may be shared. - ## Integration and stable identities `integration.endpoint` holds error codes and optional route, label, stream-hook, and API-revision bindings. When `route_path` or `operation_label` is omitted, the route defaults to the selected provider path and the label is derived from -the operation name. Stateful stream behavior is -still implemented by the declared hook; changing a hook is a behavior change. - -`integration.name` optionally preserves an existing artifact identity. Otherwise -the name is derived from `operationId`, for example `createText` becomes -`create_text`. Root component names, projection references, and operation binding -actions are derived. Existing `title` and `x-nemo-guardrails.model` overrides -remain supported to preserve names; renaming them may change generated imports. -Unnamed object alternatives need an explicit source or one event variant so -their derived identity does not depend on list position. - -Types may be inferred only where unambiguous. A scalar whose provider type is -nullable, a union, or absent needs an explicit type. Selecting an object union -may require `x-nemo-guardrails.source`. No classification or subject is inferred. - -## Endpoint binding - -Runtime metadata belongs in the contract's integration section: +the operation name. Stateful stream behavior is implemented by the declared +hook; changing a hook is a behavior change. ```yaml integration: @@ -705,9 +664,6 @@ integration: name: EXAMPLE_API_REVISION ``` -The compiler derives projection references and the operation declaration. -`integration.name` is optional and otherwise derived from `operationId`. - Endpoint keys are: | Key | Meaning | @@ -717,11 +673,21 @@ Endpoint keys are: | `unsupported_request_code` | Provider-facing error code for unsupported request shapes. | | `unsupported_response_code` | Provider-facing error code for unsupported response or stream shapes. | | `stream_hooks` | Optional provider-package Python symbol for stateful streaming behavior. | -| `api_revision` | Optional provider-package `ProviderApiRevisionBinding` symbol. | +| `api_revision` | Optional provider-package symbol that binds the provider API revision. | + +Python symbols must live inside the provider's runtime package. Stream hooks are +required when a stream projection is bound and forbidden when no stream +projection exists. -Python symbols must live inside the runtime package declared by -`provider.yaml`. Stream hooks are required when a stream projection is bound -and forbidden when no stream projection exists. +`integration.name` names the integration. When omitted, it is derived from +`operationId`, for example `createText` becomes `create_text`. `title` and +`x-nemo-guardrails.model` give projections and models stable names; renaming +them is a visible change. Unnamed object alternatives need an explicit source or +one event variant so their derived identity does not depend on list position. + +Types may be inferred only where unambiguous. A scalar whose provider type is +nullable, a union, or absent needs an explicit type. Selecting an object union +may require `x-nemo-guardrails.source`. No classification or subject is inferred. ## `x-nemo-guardrails` quick reference @@ -753,7 +719,7 @@ These keys appear on a property Schema Object: | `reason` | Structured capability, integrity, or projection-policy rationale. | | `gate` | Currently only `disabled`; requires a constrained field and reason. | | `extension` | Marks a local compatibility field absent from the provider source. Only valid for a disabled constrained field. | -| `model` | Optional generated name for a nested object, overriding its `title`. | +| `model` | Optional model name for a nested object, overriding its `title`. | | `source` | Binds that nested object to a provider component. | | `unknown_fields` | Sets that nested object's unknown-field policy. | @@ -773,122 +739,27 @@ classification. | stream event family | `event.missing_text` | `reject`, `opaque`, or `empty`. | | stream event family | `event.missing_text_shape` | Optional alternate shape used when guarded text is absent. | -## Evidence-backed normalization - -A guard contract narrows a correct provider contract. It should not silently -repair an incomplete one. - -When the pinned provider OpenAPI omits or misstates a fact required for -compilation, record authoritative evidence in `evidence.yaml` and apply a -separate `normalization.overlay.yaml` before guard contracts. - -```yaml -version: 1 -provider: example -primary: - id: example_openapi - kind: openapi - origin: https://example.com/openapi.yaml - revision: provider-revision - sha256: -supplemental: - - id: example_streaming_docs - kind: reviewed_documentation - origin: https://example.com/docs/streaming - revision: reviewed-2026-09-22 - claims: - - POST /chat returns an SSE stream when stream is true - claims_sha256: -``` - -Each normalization action cites one declared evidence source: - -```yaml -overlay: 1.1.0 -info: - title: Complete the example streaming contract - version: 1.0.0 -actions: - - target: $.paths['/chat'].post.responses - description: Add the documented SSE response. - x-guard-evidence: example_streaming_docs - update: - '200': - content: - text/event-stream: - schema: - $ref: '#/components/schemas/ProviderChatStreamEvent' - x-guard-source-evidence: reviewed-2026-09-22 -``` - -Evidence kinds are `openapi`, `sdk_types`, and `reviewed_documentation`. -Machine-readable evidence pins the downloaded artifact with `sha256`. -Reviewed documentation pins a canonical list of claims with `claims_sha256` -because a mutable web page cannot be treated as an immutable artifact. -The digest is SHA-256 over the compact, ASCII JSON representation of the claims -array. For example: - -```bash -uv run --locked python -c 'import hashlib,json; claims=["POST /chat returns SSE"]; print(hashlib.sha256(json.dumps(claims,separators=(",",":"),ensure_ascii=True).encode()).hexdigest())' -``` - -Normalization and guard policy have different responsibilities and formats: - -```text -normalization Overlay: make the provider contract factually complete -guard contract: define the supported guarded subset -``` - -## Reading compiler failures - -Semantic failures report three pieces of information: - -```text -compile - - -``` - -Treat the semantic path as the authoring location to inspect. Common failures -mean: - -| Failure | Likely cause | -| --- | --- | -| `provider contract has unreviewed fields` | A bound provider object contains fields not classified by the projection. | -| `field is absent from provider contract` | The projection claims a field that the pinned provider schema does not expose; use normalization only with evidence. | -| `does not identify one provider schema branch` | A `type`, `const`, or representation selection is ambiguous or unsupported by the provider union. | -| `requires exactly one matching guarded array variant` | The current capability profile cannot identify one text target. Narrow array cardinality or variant selection. | -| `stream event selection is duplicated` | Two stream variants declare the same selector. Give each native shape a unique selector. | -| runtime reports an ambiguous stream event | Different selectors can still match the same payload. Make the authored rules semantically disjoint. | -| `stream text paths require explicit array cardinality` | A generated stream extraction path crosses an unconstrained array. | -| `must bind its declared stream projection` | Endpoint metadata, stream projection, and handwritten hooks are inconsistent. | - -Do not respond to a compiler proof failure by weakening coverage or marking an -unknown content-bearing field opaque without review. The failure is the desired -signal that the guarded boundary changed. - ## Validation and compatibility limits There are three different checks: authoring syntax, provider-aware semantic validation, and runtime enforcement. A generic JSON Schema validator knows only standard assertions; it does not enforce subjects, configurable policies, -variant cardinality, review decisions, or hook behavior. +variant cardinality, review decisions, or hook behavior. Provider coverage, +source reachability, text-target cardinality, and hook compatibility require +validation against the provider source. -The current frontend requires component-reference operation roots, JSON -requests, HTTP 200 JSON responses, and optional HTTP 200 SSE responses. Only -the documented single-text profiles are supported. Tools, images, audio, -multiple independent guarded messages, structured output, and other semantic -content kinds require explicit shared runtime and compiler capability designs. +This version supports component-reference operation roots, JSON requests, HTTP +200 JSON responses, and optional HTTP 200 SSE responses. Only the documented +single-text profiles are supported. Tools, images, audio, multiple independent +guarded messages, structured output, and other semantic content kinds require +explicit shared runtime capability designs. Public JSON Schema and runtime acceptance are not identical in every case. Selected-variant cardinality, optional/null/default handling, provider-owned -requiredness, and disabled gates need separate conformance work. Characterization -tests record concrete cases; artifact reproducibility is not proof of complete -schema/runtime equivalence. +requiredness, and disabled gates are known areas where the two can differ. -If implementation and this semantic specification disagree, record the mismatch -and add a conformance case; do not silently weaken either definition. +If an implementation and this specification disagree, treat it as a defect and +resolve it explicitly; do not silently weaken either definition. -Human authoring trials are still needed to assess readability and editing effort. The alpha label makes no stable compatibility promise; see the [version policy](README.md#version-and-stability). From 7e9e84b475e61ec2ce1a34e23114249ba2f28cec Mon Sep 17 00:00:00 2001 From: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com> Date: Tue, 6 Oct 2026 12:05:06 +0200 Subject: [PATCH 4/8] docs(server): explain guarded subject roles Signed-off-by: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com> --- nemoguardrails/server/experimental/contracts/reference.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nemoguardrails/server/experimental/contracts/reference.md b/nemoguardrails/server/experimental/contracts/reference.md index 672575c1b1..b924746b4a 100644 --- a/nemoguardrails/server/experimental/contracts/reference.md +++ b/nemoguardrails/server/experimental/contracts/reference.md @@ -155,6 +155,10 @@ capability profiles. The `single_text.v1` profile identifies one user or assistant text subject. Selected native representations do not permit multiple independent subjects. +Because each direction has exactly one subject, its role currently matches the +direction: request subjects are `user` and response subjects are `assistant`. +`role` remains required so that profiles carrying several subjects in one +direction, such as a full conversation, can distinguish them. Replacement may affect only the selected text; unrelated metadata and declared replacement restrictions must be preserved. From d565e48a928c5ec02ecb610a03fd4fa65dd53012 Mon Sep 17 00:00:00 2001 From: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com> Date: Tue, 6 Oct 2026 12:52:37 +0200 Subject: [PATCH 5/8] docs(server): state one capability profile per contract Signed-off-by: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com> --- nemoguardrails/server/experimental/contracts/reference.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nemoguardrails/server/experimental/contracts/reference.md b/nemoguardrails/server/experimental/contracts/reference.md index b924746b4a..4c3ed41928 100644 --- a/nemoguardrails/server/experimental/contracts/reference.md +++ b/nemoguardrails/server/experimental/contracts/reference.md @@ -153,6 +153,11 @@ kind, its role, and its replacement policy. This version supports one kind, `text`, because rails currently inspect text only. Other kinds require new capability profiles. +A contract declares exactly one `profile`, which governs its request, response, +and stream. Profiles are not combined: an operation that carries text and images in +one turn needs its own profile that defines how those subjects relate, rather +than a text profile and an image profile side by side. + The `single_text.v1` profile identifies one user or assistant text subject. Selected native representations do not permit multiple independent subjects. Because each direction has exactly one subject, its role currently matches the From c37bd92cf870cc723d53e51c9958b81b2b9b1db4 Mon Sep 17 00:00:00 2001 From: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:21:58 +0200 Subject: [PATCH 6/8] fix(server): require object roots in minimal guard contract Signed-off-by: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com> --- .../server/experimental/contracts/README.md | 2 ++ .../contracts/minimal.guard.example.yaml | 2 ++ .../experimental/test_guard_contract.py | 22 +++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/nemoguardrails/server/experimental/contracts/README.md b/nemoguardrails/server/experimental/contracts/README.md index c179da2a18..6d2ca54fba 100644 --- a/nemoguardrails/server/experimental/contracts/README.md +++ b/nemoguardrails/server/experimental/contracts/README.md @@ -20,6 +20,7 @@ version: '1.0.0-alpha.1' operationId: createText profile: single_text.v1 request: + type: object required: [prompt] properties: prompt: @@ -34,6 +35,7 @@ request: x-nemo-guardrails: opaque_fields: [model] response: + type: object required: [text] properties: text: diff --git a/nemoguardrails/server/experimental/contracts/minimal.guard.example.yaml b/nemoguardrails/server/experimental/contracts/minimal.guard.example.yaml index 1cb321e26e..a9e4409059 100644 --- a/nemoguardrails/server/experimental/contracts/minimal.guard.example.yaml +++ b/nemoguardrails/server/experimental/contracts/minimal.guard.example.yaml @@ -2,6 +2,7 @@ version: '1.0.0-alpha.1' operationId: createText profile: single_text.v1 request: + type: object required: [prompt] properties: prompt: @@ -16,6 +17,7 @@ request: x-nemo-guardrails: opaque_fields: [model] response: + type: object required: [text] properties: text: diff --git a/tests/server/experimental/test_guard_contract.py b/tests/server/experimental/test_guard_contract.py index 20f2c988ff..2563b1727f 100644 --- a/tests/server/experimental/test_guard_contract.py +++ b/tests/server/experimental/test_guard_contract.py @@ -54,6 +54,28 @@ def test_minimal_guard_contract_matches_schema(guard_contract_schema: dict, mini Draft202012Validator(guard_contract_schema).validate(minimal_guard_contract) +@pytest.mark.parametrize("projection", ["request", "response"]) +@pytest.mark.parametrize("payload", [None, True, 42, "text", []]) +def test_minimal_guard_projection_rejects_non_objects( + minimal_guard_contract: dict, projection: str, payload: object +) -> None: + """Both example projections require object payloads.""" + with pytest.raises(ValidationError): + Draft202012Validator(minimal_guard_contract[projection]).validate(payload) + + +@pytest.mark.parametrize(("projection", "field"), [("request", "prompt"), ("response", "text")]) +def test_minimal_guard_projection_requires_its_text_field( + minimal_guard_contract: dict, projection: str, field: str +) -> None: + """Object payloads pass only when the required text field is present.""" + validator = Draft202012Validator(minimal_guard_contract[projection]) + validator.validate({field: "hello"}) + + with pytest.raises(ValidationError, match="required property"): + validator.validate({}) + + def test_guard_contract_rejects_unknown_version(guard_contract_schema: dict, minimal_guard_contract: dict) -> None: """The schema rejects contracts authored for an unsupported version.""" contract = minimal_guard_contract | {"version": "1.0.0"} From 91e2b1d7431c67616a0a384fcec93845720fbf1f Mon Sep 17 00:00:00 2001 From: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:48:40 +0200 Subject: [PATCH 7/8] docs(server): simplify guard contract format documentation Signed-off-by: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com> --- .../server/experimental/contracts/README.md | 255 +++--- .../contracts/guard-contract.schema.json | 12 +- .../contracts/minimal.openapi.example.yaml | 39 - .../experimental/contracts/reference.md | 774 ------------------ .../experimental/test_guard_contract.py | 78 +- 5 files changed, 169 insertions(+), 989 deletions(-) delete mode 100644 nemoguardrails/server/experimental/contracts/minimal.openapi.example.yaml delete mode 100644 nemoguardrails/server/experimental/contracts/reference.md diff --git a/nemoguardrails/server/experimental/contracts/README.md b/nemoguardrails/server/experimental/contracts/README.md index 6d2ca54fba..286d83e93f 100644 --- a/nemoguardrails/server/experimental/contracts/README.md +++ b/nemoguardrails/server/experimental/contracts/README.md @@ -1,155 +1,106 @@ -# Guarded provider contracts - -A guard contract makes the guardrail boundary of a provider operation readable: -which fields contain guarded content, which values are constrained, and which -data stays opaque under provider authority. You do not need to read Python to -understand that policy. Your deployment separately decides which rails inspect -the selected content. - -Status: experimental, `1.0.0-alpha.1`. The provider OpenAPI remains authoritative -for HTTP shapes. A guard contract is a custom format for the NVIDIA NeMo -Guardrails library that uses JSON Schema vocabulary with `x-nemo-guardrails` -annotations. It is neither an OpenAPI document nor an -[OpenAPI Overlay](https://spec.openapis.org/overlay/v1.1.0.html), and generic -OpenAPI or JSON Schema tools do not apply its guardrail semantics. - -## A complete small contract - -```yaml -version: '1.0.0-alpha.1' -operationId: createText -profile: single_text.v1 -request: - type: object - required: [prompt] - properties: - prompt: - type: string - minLength: 1 - x-nemo-guardrails: - classification: guarded - subject: - kind: text - role: user - replaceable: true - x-nemo-guardrails: - opaque_fields: [model] -response: - type: object - required: [text] - properties: - text: - type: string - minLength: 1 - x-nemo-guardrails: - classification: guarded - subject: - kind: text - role: assistant - replaceable: true - status: - const: complete - x-nemo-guardrails: - classification: constrained - reason: projection_policy.complete_text -integration: - endpoint: - unsupported_request_code: unsupported_text_request - unsupported_response_code: unsupported_text_response -``` - -This [executable example](minimal.guard.example.yaml) is paired with a -[small provider description](minimal.openapi.example.yaml). It guards the -request's `prompt` and response's `text`, delegates `model` to the provider, and -constrains response `status` to `complete`. `replaceable: true` declares that -the selected text may be replaced while preserving unrelated data. It records -policy only: the guarded proxy does not execute replacement yet and rejects every -replacement decision. - -`operationId` selects one operation in the provider OpenAPI. -`request` and `response` are required; add `stream` for streaming and -`excluded_stream_events` for deliberately unsupported event schemas. -`integration` holds runtime bindings, separate from policy. - -## Reading the policy +# Guard contracts + +A guard contract describes a provider operation's guardrail boundary: which +content is inspected, which values are restricted, and which data remains under +provider authority. It makes that boundary reviewable without reading Python. + +The Python projection models, bindings, and endpoint implementation are the +source of truth. A contract is a description of their policy, not server +configuration or a second policy to maintain by hand. Deployment configuration +selects the rails that inspect supported content. + +This directory defines the experimental document format and an illustrative +example. Provider exports belong with their integrations; the format alone does +not implement an endpoint, an exporter, or runtime YAML loading. + +## Document structure + +Start with [minimal.guard.example.yaml](minimal.guard.example.yaml). It marks +request `prompt` and response `text` as guarded, leaves `model` opaque, and +constrains response `status` to `complete`. It is a format example, not the +output of a provider integration. + +| Section | Describes | +| --- | --- | +| `version`, `operationId`, `profile` | Format revision, provider operation, and framework capability. | +| `request`, `response` | Request and buffered-response payload projections. | +| `stream` | Optional event payloads, classifications, and transport framing. | +| `excluded_stream_events` | Explicitly unsupported event schemas and the reasons for exclusion. | +| `integration.endpoint` | Endpoint metadata such as route, rejection codes, and implementation bindings. | + +Payload projections use a subset of JSON Schema vocabulary: `type`, +`properties`, `required`, `items`, `oneOf`, constants, enums, and bounds. +NeMo policy is carried by `x-nemo-guardrails`. + +## Field policy | Declaration | Meaning | | --- | --- | -| `classification: guarded` | Contains guarded content directly or through explicitly described children. | -| `classification: constrained` | Locally restricts structure or values without making that field a subject. | -| `classification: opaque` | Delegates the entire value to the provider; the library does not inspect its nested shape. | -| `opaque_fields: [id, model]` | Shorthand for explicit opaque properties on this object only. | -| `subject` | Identifies guarded content: its kind, its user/assistant role, and its replacement policy. | -| `reason` | Records why a restriction exists as a namespaced identifier, such as `core_capability.tool_content`. | - -Each selected object accounts for every known provider property exactly once. -This version supports one subject kind, `text`, because rails currently inspect -text only. -The [reference](reference.md) defines each annotation with its semantics, -examples, and edge cases, including opaque values, unknown fields, requiredness, -nulls, defaults, replacement, and streaming. - -## Version and stability - -The only accepted contract version is `"1.0.0-alpha.1"`. Unsupported versions and -unknown keys are errors; there are no version aliases or alternate layouts. -Authoring and semantics may change before a stable `1.0.0`. Increment the alpha -revision when you publish an incompatible contract revision, and never -reinterpret a published revision. This follows -[Semantic Versioning](https://semver.org/#spec-item-9). - -Contract versions are independent of capability profile identifiers such as -`single_text.v1`, which identify semantic boundaries rather than the document -format. Each operation has one maintained contract. - -## Editor support and validation - -[guard-contract.schema.json](guard-contract.schema.json) is a self-contained -JSON Schema 2020-12 schema. Associate it with `*.guard.yaml` in your editor. For -the [YAML language server](https://github.com/redhat-developer/yaml-language-server#associating-schemas), -the equivalent workspace setting is: - -```json -{ - "yaml.schemas": { - "./nemoguardrails/server/experimental/contracts/guard-contract.schema.json": "**/*.guard.yaml" - } -} -``` - -The schema offers key/value completion and catches misspellings, wrong types, -unsupported schema keywords, blank exclusion reasons, and malformed integration -metadata. It does not prove provider compatibility; see the -[validation limits](reference.md#validation-and-compatibility-limits). - -## Authoring and review - -Start from the [minimal contract](minimal.guard.example.yaml), review it against -the selected provider operation, and validate it with the schema while you edit. -When you change a constraint, edit that field's schema and reason together. Do -not mark a newly documented content-bearing field opaque to silence a review -error. - -Before you submit a new or changed operation, verify: - -- The provider source is immutable, pinned, and digest-verified. -- The guard contract uses standard Schema Objects for structure. -- Every provider field at every guarded object boundary is classified once. -- Every opaque field has been reviewed as independent of guarded content. -- Every constrained field is necessary to make the current capability safe and - unambiguous. -- Every disabled feature has a structured reason. -- Every subject has the correct kind, role, and explicit replacement policy. -- Arrays and unions identify exactly the supported subject. -- Stream event variants are disjoint and cover every accepted provider branch. -- Stateful stream behavior remains in handwritten hooks rather than the - contract. -- Endpoint symbols live inside the provider's runtime package. -- Schema validation and the relevant runtime tests succeed. - -## Provider contracts - -Provider-specific contracts are introduced with their provider capabilities. -Each operation contract covers its request, buffered response, and optional -streaming boundary as one policy document. Use the [reference](reference.md) -for precise syntax and meaning. +| `classification: guarded` | A text subject, or a container leading to it. | +| `subject` | Identifies text, its user/assistant role, and replacement eligibility. | +| `classification: constrained` | Restricts a value or shape without making it a rail subject. | +| `gate: disabled` | Marks a constrained, null-only field for an unsupported feature. | +| `classification: opaque` | Leaves the value under provider authority, without guardrail inspection. | +| `opaque_fields` | Lists reviewed opaque names on this object without declaring each property. | +| `reason` | Explains a restriction: capability, provider integrity, or projection policy. | + +Opaque does not mean safe or trusted. Its nested content is not independently +reviewed. Opaque names should not overlap declared properties or use wildcards. +`extension: true` identifies a local compatibility field outside the pinned +provider schema, not an exemption from review. + +An object's `required` list controls presence; nullability is separate. Disabled +fields can be omitted when optional, or supplied as null, but cannot enable the +feature. A `default` describes an internal default, not permission to insert +fields into the forwarded provider payload. + +`replaceable` records text replacement eligibility. `replacement_blocked_by` +identifies metadata that can prevent replacement, with a `replacement_reason`. +The integration determines the blocking predicate and whether replacement +outcomes can be executed. Neither marking a subject nor marking it replaceable +enables a detector or a runtime capability. + +## Object and stream metadata + +`model` identifies the Python model; `source` identifies a provider schema +component. `unknown_fields: configurable` marks an object whose unknown-field +handling depends on runtime validation context. Generic JSON Schema validators +do not enforce this annotation. `additionalProperties` remains the ordinary +schema-level object closure rule. Provider-required opaque fields may be left +to provider validation; a projection is not a full provider request validator. + +Object `variant` metadata describes discriminator matching and selected-variant +cardinality. This is distinct from ordinary array length constraints. +`stream_selector_field` identifies a request flag, not streaming support. + +A stream's `oneOf` branches describe event families: `guarded_delta`, `snapshot`, +`opaque`, or `provider_error`. Their variants identify source schemas, matching +rules, and classifier shape names. Missing-text policy is distinct from schema +nullability. Transport metadata describes SSE discriminators and sentinels. +Event exclusions and external-source reasons record scope and provenance; they +do not establish that every provider event has been reviewed. + +Stateful event ordering, lifecycle checks, conditional replacement, and native +error encoding remain implementation behavior. Endpoint symbol references are +descriptive metadata, not instructions to load code from an untrusted document. + +## Validation and stability + +[guard-contract.schema.json](guard-contract.schema.json) validates the document +format and rejects unsupported keys in the reserved guardrail vocabulary. Other +`x-*` schema annotations are allowed. The schema can be associated with +`*.guard.yaml` in a JSON Schema-aware editor. + +Format validation does not establish complete provider coverage, correct text +extraction, preservation of unrelated data, or equivalence to arbitrary Python +validators and coercion. These require implementation and source-aware tests. +Provider revision and digest checks are separate from document validation. + +The format is experimental: `1.0.0-alpha.1`, not stable version 1. Its version +is separate from the capability profile `single_text.v1` and provider API +revision. A profile identifies the supported content capability; it does not +choose deployment rails or enable runtime profile selection. + +This is a NeMo Guardrails document using JSON Schema vocabulary, not an OpenAPI +document or an OpenAPI Overlay. Generic schema tools do not execute its guardrail +annotations. diff --git a/nemoguardrails/server/experimental/contracts/guard-contract.schema.json b/nemoguardrails/server/experimental/contracts/guard-contract.schema.json index b6e3c80230..d75ed8f7f7 100644 --- a/nemoguardrails/server/experimental/contracts/guard-contract.schema.json +++ b/nemoguardrails/server/experimental/contracts/guard-contract.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "urn:nemo-guardrails:guard-contract:1.0.0-alpha.1", "title": "NeMo Guard Contract 1.0.0-alpha.1 (experimental)", - "description": "Policy-first authoring format. Schema validation checks syntax, not provider coverage or runtime capability.", + "description": "Guard contract document format. Schema validation checks syntax, not provider coverage or runtime capability.", "type": "object", "additionalProperties": false, "properties": { @@ -380,16 +380,6 @@ "event": { "$ref": "#/$defs/event" }, - "event_schema_binding": { - "enum": [ - "operation_response", - "component" - ] - }, - "event_schema_reason": { - "type": "string", - "pattern": "^(core_capability|provider_integrity|projection_policy)\\.[a-z][a-z0-9_]*$" - }, "transport": { "$ref": "#/$defs/transport" } diff --git a/nemoguardrails/server/experimental/contracts/minimal.openapi.example.yaml b/nemoguardrails/server/experimental/contracts/minimal.openapi.example.yaml deleted file mode 100644 index 831c98315a..0000000000 --- a/nemoguardrails/server/experimental/contracts/minimal.openapi.example.yaml +++ /dev/null @@ -1,39 +0,0 @@ -openapi: 3.1.0 -info: - title: Synthetic buffered text example - version: 1.0.0 -paths: - /text: - post: - operationId: createText - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/TextRequest' - responses: - '200': - description: Generated text - content: - application/json: - schema: - $ref: '#/components/schemas/TextResponse' -components: - schemas: - TextRequest: - type: object - required: [prompt, model] - properties: - prompt: - type: string - model: - type: string - TextResponse: - type: object - required: [text] - properties: - text: - type: string - status: - type: string - enum: [complete, partial] diff --git a/nemoguardrails/server/experimental/contracts/reference.md b/nemoguardrails/server/experimental/contracts/reference.md deleted file mode 100644 index 4c3ed41928..0000000000 --- a/nemoguardrails/server/experimental/contracts/reference.md +++ /dev/null @@ -1,774 +0,0 @@ -# Guard contract reference - -This is the syntax and semantic specification for guard contract version -`1.0.0-alpha.1` (experimental) in the NVIDIA NeMo Guardrails library. Start with -the [README](README.md) for a complete small example and the editing workflow. -This reference defines what an implementation must preserve. - -The [authoring schema](guard-contract.schema.json) defines accepted syntax: -a limited subset of JSON Schema with local `x-nemo-guardrails` annotations. - -## Authority and scope - -The pinned provider description owns HTTP paths, source schemas, and provider -facts. A guard contract selects a supported boundary; it does not redefine the -whole provider API or claim to configure all detectors, prompts, input/output -rails, or deployment settings. - -Request and response policy describes structural acceptance and guarded subjects. -The deployment separately selects which rails inspect that content and whether a -rail result permits, rejects, or replaces it. A marked subject is a guardable -surface, not a guarantee that every deployment runs a particular safety check. - -## Examples - -Begin with the [minimal contract](minimal.guard.example.yaml), paired with -its [provider OpenAPI](minimal.openapi.example.yaml). Complete examples are -introduced with their provider capabilities. - -For authoring and validation steps, use the [guard contract guide](README.md). - -## Payload Projections - -A Payload Projection is a normal OpenAPI Schema Object with local guardrail -annotations. It describes either a request or a buffered JSON response. - -### Minimal request example - -```yaml -request: - type: object - additionalProperties: true - title: example.chat.request.text.v1 - properties: - messages: - type: array - minItems: 1 - maxItems: 1 - items: - type: object - additionalProperties: true - title: ExampleUserMessageProjection - properties: - role: - const: user - x-nemo-guardrails: - classification: constrained - content: - type: string - minLength: 1 - x-nemo-guardrails: - classification: guarded - subject: - kind: text - role: user - replaceable: true - required: [role, content] - x-nemo-guardrails: - model: ExampleUserMessageProjection - source: '#/components/schemas/ProviderUserMessage' - unknown_fields: configurable - x-nemo-guardrails: - classification: guarded - stream: - type: boolean - default: false - x-nemo-guardrails: - classification: constrained - model: - x-nemo-guardrails: - classification: opaque - required: [messages] - x-nemo-guardrails: - model: ExampleChatGuardedRequestProjection - stream_selector_field: stream -``` - -The schema describes the local request policy. Do not declare the source -schema, direction, or capability profile on the projection root: they come from -`operationId`, the `request` or `response` key, and the top-level `profile`. -Optional `title` and `x-nemo-guardrails.model` give stable projection and -class-name overrides. `stream_selector_field` identifies the boolean request -field that selects streaming. - -### Complete field accounting - -Every field declared by a bound provider object must be classified exactly -once: - -| Classification | Meaning | -| --- | --- | -| `guarded` | Contains or leads to content that rails inspect. | -| `constrained` | Narrows the provider shape so guarded extraction, replacement, or response handling is safe and unambiguous. | -| `opaque` | Remains provider-owned and is forwarded without interpretation by the library. | -| local extension | Does not exist in the provider source; it is an explicitly capability-gated compatibility field. | - -For example: - -```yaml -properties: - input: - type: string - minLength: 1 - x-nemo-guardrails: - classification: guarded - subject: - kind: text - role: user - replaceable: true - - response_format: - type: 'null' - default: null - x-nemo-guardrails: - classification: constrained - gate: disabled - reason: projection_policy.plain_text_output - - temperature: - x-nemo-guardrails: - classification: opaque -``` - -This is the provider-drift boundary: a field that the provider adds later must -not reach the guarded boundary without an explicit review decision. -Provider-aware validation, which compares the contract with the pinned provider -source, enforces this; it is not part of this format definition. - -`opaque` does not mean "unknown" or "ignored." It records a deliberate decision -that the field does not affect the guarded capability and can remain under -provider authority. Opaque does not mean trusted or safe: the entire value is -delegated to the provider. - -`opaque_fields` is shorthand for individually annotated opaque properties on -this object. Names must be explicit and unique, cannot overlap `properties`, -and cannot use wildcards. It does not review nested fields inside an opaque -value. New known fields at a selected boundary require review even when -runtime unknown fields are allowed. - -### Guarded subjects - -A subject marks the content that rails inspect and declares its meaning: its -kind, its role, and its replacement policy. This version supports one kind, -`text`, because rails currently inspect text only. Other kinds require new -capability profiles. - -A contract declares exactly one `profile`, which governs its request, response, -and stream. Profiles are not combined: an operation that carries text and images in -one turn needs its own profile that defines how those subjects relate, rather -than a text profile and an image profile side by side. - -The `single_text.v1` profile identifies one user or assistant text subject. -Selected native representations do not permit multiple independent subjects. -Because each direction has exactly one subject, its role currently matches the -direction: request subjects are `user` and response subjects are `assistant`. -`role` remains required so that profiles carrying several subjects in one -direction, such as a full conversation, can distinguish them. -Replacement may affect only the selected text; unrelated metadata and declared -replacement restrictions must be preserved. - -A direct guarded string declares its provider-neutral meaning: - -```yaml -content: - type: string - minLength: 1 - x-nemo-guardrails: - classification: guarded - subject: - kind: text - role: assistant - replaceable: true -``` - -Subject keys are: - -| Key | Meaning | -| --- | --- | -| `kind` | Required content kind. This version accepts only `text`. | -| `role` | Required. `user` for input or `assistant` for generated output. | -| `replaceable` | Whether a buffered rail decision may replace this provider field. The `single_text.v1` profile requires it; the authoring schema does not enforce that requirement. | -| `replacement_blocked_by` | Related provider field that may make replacement unsafe. | -| `replacement_reason` | Structured reason for a restriction on replacement. | - -For example, replacing cited text may invalidate its citations: - -```yaml -subject: - kind: text - role: assistant - replaceable: true - replacement_blocked_by: citations - replacement_reason: provider_integrity.cited_text -``` - -Replacement is permitted only when the related field is absent. If -`replaceable` is false, or replacement is conditional, a `replacement_reason` -is required. - -Replacement policy is declared, not yet executed: the guarded proxy rejects every -replacement decision until safe replacement is implemented. Declaring it now -keeps each contract's replacement review next to the field it protects. - -### Constraints and capability gates - -Standard JSON Schema keywords describe the narrowed provider value. The format -accepts these selection and validation keywords: - -```text -type, const, default, enum, -minItems, maxItems, minLength, maxLength, pattern -``` - -The authoring schema rejects other schema keywords and unknown -`x-nemo-guardrails` keys. Other `x-*` schema annotations are available to other -tooling and ignored by the library. Keys beginning with `x-nemo-guardrails` are -reserved; only the exact `x-nemo-guardrails` key is accepted. Descriptive annotations such as `description` and `examples` are not -treated as constraints. Opaque properties cannot carry validation assertions: -classify a property as constrained when the guard projection should validate it. -A disabled gate must agree with its null-only schema. - -Examples include selecting one role: - -```yaml -role: - const: user - x-nemo-guardrails: - classification: constrained -``` - -and restricting generation to one output: - -```yaml -n: - const: 1 - default: 1 - x-nemo-guardrails: - classification: constrained - reason: core_capability.single_text_target -``` - -A disabled capability uses a null-only projection: - -```yaml -tools: - type: 'null' - default: null - x-nemo-guardrails: - classification: constrained - gate: disabled - reason: core_capability.tool_content -``` - -`gate: disabled` means the guarded operation does not support that capability; -it does not mean the provider lacks it. A gated field must be constrained and -must provide a reason. A disabled field may be absent or null but must not -enable the unsupported feature. Do not disguise this local restriction as a -provider fact. - -Reason identifiers use one of these namespaces: - -| Prefix | Use | -| --- | --- | -| `core_capability.*` | The shared runtime capability does not yet support the provider feature. | -| `provider_integrity.*` | Accepting or changing the value could invalidate related provider data or protocol guarantees. | -| `projection_policy.*` | The chosen guarded projection deliberately narrows otherwise supported provider behavior. | - -Use a stable lower-case identifier after the prefix, for example -`core_capability.tool_content`. Reasons are reviewable policy identifiers, not -free-form explanations. - -### Defaults - -`default` is an annotation, not permission to insert an absent property into -the forwarded provider payload. An implementation may use defaults internally; -that does not authorize rewriting provider input. See the -[JSON Schema annotation guidance](https://json-schema.org/understanding-json-schema/reference/annotations). - -### Requiredness - -The standard object-level `required` array determines which properties must be -present in the guarded projection. Properties omitted from this array are -optional, even when the provider requires them. Keep a policy `reason` on a -property when deliberately changing provider requiredness. - -```yaml -required: [messages] -``` - -Field-level `x-nemo-guardrails.required` is not accepted. Optionality does not -imply nullability in JSON Schema: the value schema must explicitly permit null. -Provider-required opaque values remain under provider authority rather than -being promoted to local validation. - -### Nested objects and arrays - -Attach object metadata to every nested object that the projection traverses: - -```yaml -message: - type: object - properties: - content: {} - x-nemo-guardrails: - classification: guarded - model: ExampleMessageProjection - source: '#/components/schemas/ProviderMessage' - unknown_fields: configurable -``` - -For an array, put the nested object metadata on `items` and classify the array -field itself: - -```yaml -messages: - type: array - minItems: 1 - maxItems: 1 - items: - type: object - title: ExampleMessageProjection - properties: {} - x-nemo-guardrails: - model: ExampleMessageProjection - source: '#/components/schemas/ProviderMessage' - unknown_fields: configurable - x-nemo-guardrails: - classification: guarded -``` - -Author a direct guarded array with `minItems: 1` and `maxItems: 1` when the -guarded target is its first item. For union variants, the current -`single_text.v1` profile requires an `exactly_one` matching selection. -Cardinality is therefore part of safety, not merely validation convenience. - -### Union variants - -Arrays and alternatives must identify the supported text unambiguously. -Explicit match selectors must agree with declared constant fields. Authored -alternative order must not supply identity. The `single_text.v1` profile also -requires nonempty text and an unambiguous traversal. - -Use variants when an array contains a provider union and only selected object -variants participate in the guarded capability: - -```yaml -content: - type: array - minItems: 1 - maxItems: 1 - items: - oneOf: - - type: object - title: ExampleTextBlockProjection - properties: - type: - const: text - x-nemo-guardrails: - classification: constrained - text: - type: string - minLength: 1 - x-nemo-guardrails: - classification: guarded - subject: - kind: text - role: assistant - replaceable: true - required: [type, text] - x-nemo-guardrails: - model: ExampleTextBlockProjection - source: '#/components/schemas/ProviderTextBlock' - unknown_fields: configurable - variant: - match: - type: text - cardinality: exactly_one - x-nemo-guardrails: - classification: guarded -``` - -`variant.match` can be omitted when required properties with `const` values -identify the variant. An explicit match must correspond to those selected -constants. `cardinality` is one of `any`, `at_most_one`, or `exactly_one`, although -the current payload profile requires `exactly_one` for the guarded target. - -### Alternative text representations - -Use a field-level `oneOf` when the same semantic text may use different native -encodings, such as a string or a one-element text-block array: - -```yaml -content: - oneOf: - - type: string - minLength: 1 - x-nemo-guardrails: - subject: - kind: text - role: user - replaceable: true - - type: array - minItems: 1 - maxItems: 1 - items: - type: object - properties: - text: - type: string - minLength: 1 - x-nemo-guardrails: - classification: guarded - subject: - kind: text - role: user - replaceable: true - x-nemo-guardrails: - model: ExampleInputTextBlockProjection - source: '#/components/schemas/ProviderInputTextBlock' - unknown_fields: configurable - x-nemo-guardrails: {} - x-nemo-guardrails: - classification: guarded -``` - -Each representation must lead to exactly one subject with the same -provider-neutral meaning. - -### Unknown-field policies - -Use standard `additionalProperties` for object closure. Add -`x-nemo-guardrails.unknown_fields: configurable` only for a content boundary -whose closure the runtime may override: - -| Value | Behavior | -| --- | --- | -| `additionalProperties: true` or omitted | Accept additional provider fields. | -| `additionalProperties: false` | Reject additional fields, while retaining declared opaque properties. | -| `unknown_fields: configurable` with `additionalProperties: true` | Accept additional fields by default and allow the runtime's closed-content policy to reject unreviewed extras. | - -Use `additionalProperties` for allow/forbid behavior rather than duplicating it -in the extension. - -These policies do not replace field accounting against the pinned provider -contract. Known provider fields at a guarded boundary must still be marked -guarded, constrained, or opaque. - -### Local extension fields - -Occasionally a compatibility layer adds a field that is not claimed by the -provider source. Mark it explicitly: - -```yaml -reasoning_content: - type: 'null' - default: null - x-nemo-guardrails: - classification: constrained - extension: true - gate: disabled - reason: core_capability.reasoning_content -``` - -A local extension must be a disabled constrained capability. Do not use -`extension: true` to avoid proving that a real provider field exists. - -## Stream Projections - -Stream policy describes decoded provider data events and declared SSE framing. -It does not implicitly upgrade the source to OpenAPI 3.2 transport `itemSchema`. - -A Stream Projection describes complete SSE event payloads, one event at a -time. It does not describe cross-event ordering. Stateful sequence validation -and provider-native error framing belong to the stream hooks bound under -`integration.endpoint.stream_hooks`. - -### Stream root - -```yaml -stream: - title: example.chat.stream.text.v1 - oneOf: - - type: object - title: ExampleStreamPayloadProjection - properties: - object: - const: chat.chunk - x-nemo-guardrails: - classification: constrained - choices: - type: array - maxItems: 1 - items: - type: object - properties: - delta: - type: object - properties: - content: - type: string - x-nemo-guardrails: - classification: guarded - subject: - kind: text - role: assistant - x-nemo-guardrails: - classification: guarded - model: ExampleStreamDeltaProjection - unknown_fields: configurable - source: '#/components/schemas/ProviderStreamDelta' - x-nemo-guardrails: - model: ExampleStreamChoiceProjection - unknown_fields: configurable - source: '#/components/schemas/ProviderStreamChoice' - x-nemo-guardrails: - classification: guarded - required: [object, choices] - x-nemo-guardrails: - model: ExampleStreamPayloadProjection - source: '#/components/schemas/ProviderChatStreamEvent' - event: - classification: guarded_delta - variants: - - source_schema: ProviderChatStreamEvent - shape: chat.chunk:content - match: - object: chat.chunk - required_fields: [] - missing_text: opaque - missing_text_shape: chat.chunk:metadata - x-nemo-guardrails: - event_schema_binding: operation_response - transport: - require_sse_event: false - non_data_shape: '[DONE]' - sentinels: - '[DONE]': '[DONE]' -``` - -Root stream keys are: - -| Key | Meaning | -| --- | --- | -| `event_schema_binding` | `operation_response` when the SSE response reaches the event schema; `component` only when justified by external evidence. | -| `event_schema_reason` | Required reason for a component-only binding and forbidden for an operation-bound one. | -| `transport` | SSE discriminator, event-field, non-data, and sentinel behavior. | - -Transport keys are: - -| Key | Meaning | -| --- | --- | -| `data_discriminator` | JSON property used by the provider stream union discriminator, when present. | -| `require_sse_event` | Require the SSE `event:` field to match the JSON discriminator. | -| `non_data_shape` | Shape name for SSE events without a `data:` field. | -| `sentinels` | Map exact non-JSON `data:` values to stable shape names. | - -When `require_sse_event` is true, `data_discriminator` is required. - -### Stream event review - -Every schema in the selected provider stream union must be accepted or named -in `excluded_stream_events` with a nonblank author reason: - -```yaml -excluded_stream_events: - - reason: outside_single_text_stream_profile - sources: [ProviderAudioDeltaEvent, ProviderAudioDoneEvent] -``` - -Unknown, duplicate, stale, or simultaneously accepted/excluded entries are -errors. A new event schema must require a new author decision; implicit -rejection is insufficient as source-update review. External provider-error -variants still need an `external_reason`. Nested provider unions are covered by -the ordinary selection rules; the exclusion list covers top-level event schemas, -not every future nested union alternative. - -### Event families - -Each root `oneOf` entry, such as `ExampleStreamPayloadProjection` in the -preceding example, is an event family: one projection model and one guardrail -role shared by one or more native provider variants. - -Event classifications are: - -| Classification | Meaning | -| --- | --- | -| `guarded_delta` | Carries a new text delta that rails inspect. | -| `snapshot` | Carries accumulated text checked against previously observed deltas. | -| `opaque` | Contains no guarded content. | -| `provider_error` | Provider-native in-stream failure. | - -Each event variant has: - -| Key | Meaning | -| --- | --- | -| `source_schema` | Provider component represented by this variant. | -| `shape` | Stable shape name returned after classification. | -| `match` | Dotted JSON selectors and scalar values that identify the event. | -| `required_fields` | Top-level fields that must be required to distinguish the event. | -| `external_reason` | Evidence reason for a provider-error schema accepted outside the primary stream union. Other external variants are not supported. | - -A variant must have at least one `match` entry or required field. Selectors -must be provable against the provider schema and must not be duplicated. Rules -must also be semantically disjoint: an event that matches more than one rule is -rejected. - -### Text paths are derived - -Authors do not write a text path. They mark the nested text property with a -guarded subject, and the path follows from that marking. In the preceding -example it is `choices[0].delta.content`. Keeping the path derived prevents -extraction from drifting away from the schema that validates it. - -### Events without text - -An opaque event family is not an opaque property: the family still has a -selected schema and constrained fields. Those constraints remain enforced even -though the family has no subject. - -A structurally valid event in a guarded family may omit its text field. Choose -that behavior explicitly: - -| `missing_text` | Behavior | -| --- | --- | -| `reject` | Reject the event. This is the default. | -| `opaque` | Reclassify it as metadata; `missing_text_shape` may give it a distinct shape, otherwise the original shape is reused. | -| `empty` | Treat it as an empty text snapshot. | - -Only `guarded_delta` families may use `opaque`; only `snapshot` families may -use `empty`. - -For example, the chat chunks in the stream root example may carry role, usage, -or finish metadata without a content delta: - -```yaml -event: - classification: guarded_delta - variants: [...] - missing_text: opaque - missing_text_shape: chat.chunk:metadata -``` - -## Integration and stable identities - -`integration.endpoint` holds error codes and optional route, label, stream-hook, -and API-revision bindings. When `route_path` or `operation_label` is omitted, -the route defaults to the selected provider path and the label is derived from -the operation name. Stateful stream behavior is implemented by the declared -hook; changing a hook is a behavior change. - -```yaml -integration: - name: chat - endpoint: - route_path: /v1/chat - operation_label: Example Chat - unsupported_request_code: unsupported_example_chat_shape - unsupported_response_code: unsupported_example_chat_response_shape - stream_hooks: - module: example_guardrails_provider.chat.stream_hooks - name: ExampleChatStreamHooks - api_revision: - module: example_guardrails_provider.chat.api_revision - name: EXAMPLE_API_REVISION -``` - -Endpoint keys are: - -| Key | Meaning | -| --- | --- | -| `route_path` | Runtime route; it must match the provider operation, optionally with a concrete leading API-version segment. | -| `operation_label` | Human-readable name used in diagnostics. | -| `unsupported_request_code` | Provider-facing error code for unsupported request shapes. | -| `unsupported_response_code` | Provider-facing error code for unsupported response or stream shapes. | -| `stream_hooks` | Optional provider-package Python symbol for stateful streaming behavior. | -| `api_revision` | Optional provider-package symbol that binds the provider API revision. | - -Python symbols must live inside the provider's runtime package. Stream hooks are -required when a stream projection is bound and forbidden when no stream -projection exists. - -`integration.name` names the integration. When omitted, it is derived from -`operationId`, for example `createText` becomes `create_text`. `title` and -`x-nemo-guardrails.model` give projections and models stable names; renaming -them is a visible change. Unnamed object alternatives need an explicit source or -one event variant so their derived identity does not depend on list position. - -Types may be inferred only where unambiguous. A scalar whose provider type is -nullable, a union, or absent needs an explicit type. Selecting an object union -may require `x-nemo-guardrails.source`. No classification or subject is inferred. - -## `x-nemo-guardrails` quick reference - -The meaning of a key depends on where the extension is attached. The same -extension name is intentionally local rather than one large embedded document. - -### Projection object metadata - -These keys appear on a root projection or nested object Schema Object: - -| Key | Applies to | Meaning | -| --- | --- | --- | -| `model` | payload and stream objects | Optional Python model-name override. An identifier-valued `title` supplies the default. | -| `source` | payload and stream objects | Provider component reference. Root bindings are derived; use it on nested objects that select a named provider schema and omit it for inline objects. | -| `unknown_fields` | payload and stream objects | Optional runtime configurability; ordinary closure comes from `additionalProperties`. | -| `stream_selector_field` | request root | Field whose boolean value selects a streaming response. | -| `event_schema_binding` | stream root | `operation_response` or `component`. | -| `event_schema_reason` | stream root | Required structured reason for a component-only stream binding. | -| `transport` | stream root | Stream framing and discriminator metadata. | - -### Field metadata - -These keys appear on a property Schema Object: - -| Key | Meaning | -| --- | --- | -| `classification` | `guarded`, `constrained`, or `opaque`. | -| `subject` | Direct guarded-text meaning and replacement policy. | -| `reason` | Structured capability, integrity, or projection-policy rationale. | -| `gate` | Currently only `disabled`; requires a constrained field and reason. | -| `extension` | Marks a local compatibility field absent from the provider source. Only valid for a disabled constrained field. | -| `model` | Optional model name for a nested object, overriding its `title`. | -| `source` | Binds that nested object to a provider component. | -| `unknown_fields` | Sets that nested object's unknown-field policy. | - -Array item objects carry `title`, optional `model`, `source`, and `unknown_fields` on the -`items` Schema Object. A field-level representation branch carries either a -direct `subject` or an annotated `items` object, not an ordinary field -classification. - -### Variant, event, and endpoint metadata - -| Location | Key | Meaning | -| --- | --- | --- | -| object union branch | `variant.match` | Optional selector override; defaults to required properties with `const` values. | -| object union branch | `variant.cardinality` | `any`, `at_most_one`, or `exactly_one`. | -| stream event family | `event.classification` | `guarded_delta`, `snapshot`, `opaque`, or `provider_error`. | -| stream event family | `event.variants` | Provider schemas and selectors represented by the family. | -| stream event family | `event.missing_text` | `reject`, `opaque`, or `empty`. | -| stream event family | `event.missing_text_shape` | Optional alternate shape used when guarded text is absent. | - -## Validation and compatibility limits - -There are three different checks: authoring syntax, provider-aware semantic -validation, and runtime enforcement. A generic JSON Schema validator knows only -standard assertions; it does not enforce subjects, configurable policies, -variant cardinality, review decisions, or hook behavior. Provider coverage, -source reachability, text-target cardinality, and hook compatibility require -validation against the provider source. - -This version supports component-reference operation roots, JSON requests, HTTP -200 JSON responses, and optional HTTP 200 SSE responses. Only the documented -single-text profiles are supported. Tools, images, audio, multiple independent -guarded messages, structured output, and other semantic content kinds require -explicit shared runtime capability designs. - -Public JSON Schema and runtime acceptance are not identical in every case. -Selected-variant cardinality, optional/null/default handling, provider-owned -requiredness, and disabled gates are known areas where the two can differ. - -If an implementation and this specification disagree, treat it as a defect and -resolve it explicitly; do not silently weaken either definition. - -The alpha label makes no stable compatibility promise; see the -[version policy](README.md#version-and-stability). diff --git a/tests/server/experimental/test_guard_contract.py b/tests/server/experimental/test_guard_contract.py index 2563b1727f..96a958ceee 100644 --- a/tests/server/experimental/test_guard_contract.py +++ b/tests/server/experimental/test_guard_contract.py @@ -27,9 +27,8 @@ CONTRACTS_ROOT = REPOSITORY_ROOT / "nemoguardrails/server/experimental/contracts" SCHEMA_PATH = CONTRACTS_ROOT / "guard-contract.schema.json" MINIMAL_CONTRACT_PATH = CONTRACTS_ROOT / "minimal.guard.example.yaml" -REFERENCE_PATH = CONTRACTS_ROOT / "reference.md" MARKDOWN_LINK = re.compile(r"\[[^]]+\]\(([^)]+)\)") -STREAM_ROOT_EXAMPLE = re.compile(r"### Stream root\n\n```yaml\n(?P.*?)\n```", re.DOTALL) +EXTENSION = "x-nemo-guardrails" @pytest.fixture(scope="module") @@ -44,6 +43,37 @@ def minimal_guard_contract() -> dict: return yaml.safe_load(MINIMAL_CONTRACT_PATH.read_text(encoding="utf-8")) +@pytest.fixture +def streaming_guard_contract(minimal_guard_contract: dict) -> dict: + """Describe a text event and SSE sentinel without an endpoint implementation.""" + contract = deepcopy(minimal_guard_contract) + event = deepcopy(contract["response"]) + event[EXTENSION] = { + "event": { + "classification": "guarded_delta", + "variants": [ + { + "source_schema": "ExampleTextEvent", + "shape": "text.delta", + "required_fields": ["text"], + } + ], + "missing_text": "reject", + } + } + contract["stream"] = { + "oneOf": [event], + EXTENSION: { + "transport": { + "require_sse_event": False, + "non_data_shape": "[DONE]", + "sentinels": {"[DONE]": "[DONE]"}, + } + }, + } + return contract + + def test_guard_contract_schema_is_valid(guard_contract_schema: dict) -> None: """The guard contract schema is a valid JSON Schema 2020-12 document.""" Draft202012Validator.check_schema(guard_contract_schema) @@ -77,7 +107,7 @@ def test_minimal_guard_projection_requires_its_text_field( def test_guard_contract_rejects_unknown_version(guard_contract_schema: dict, minimal_guard_contract: dict) -> None: - """The schema rejects contracts authored for an unsupported version.""" + """The schema rejects documents with an unsupported format version.""" contract = minimal_guard_contract | {"version": "1.0.0"} with pytest.raises(ValidationError): @@ -115,24 +145,46 @@ def test_guard_contract_rejects_guardrails_extension_lookalikes( Draft202012Validator(guard_contract_schema).validate(contract) -def test_stream_root_documentation_example_matches_schema( - guard_contract_schema: dict, minimal_guard_contract: dict +def test_stream_contract_matches_schema(guard_contract_schema: dict, streaming_guard_contract: dict) -> None: + """The format supports a stream section independently of runtime support.""" + Draft202012Validator(guard_contract_schema).validate(streaming_guard_contract) + + +@pytest.mark.parametrize("section", ["transport", "event"]) +def test_stream_contract_requires_framing_and_event_policy( + guard_contract_schema: dict, streaming_guard_contract: dict, section: str ) -> None: - """The documented stream root forms a schema-valid contract section.""" - reference = REFERENCE_PATH.read_text(encoding="utf-8") - match = STREAM_ROOT_EXAMPLE.search(reference) - assert match is not None - contract = deepcopy(minimal_guard_contract) - contract.update(yaml.safe_load(match.group("yaml"))) + """Stream roots need transport metadata and each family needs event policy.""" + stream = streaming_guard_contract["stream"] + metadata = stream[EXTENSION] if section == "transport" else stream["oneOf"][0][EXTENSION] + del metadata[section] - Draft202012Validator(guard_contract_schema).validate(contract) + with pytest.raises(ValidationError): + Draft202012Validator(guard_contract_schema).validate(streaming_guard_contract) + + +@pytest.mark.parametrize( + ("key", "value"), + [ + ("event_schema_binding", "operation_response"), + ("event_schema_reason", "provider_integrity.component_source"), + ], +) +def test_stream_contract_rejects_source_resolution_settings( + guard_contract_schema: dict, streaming_guard_contract: dict, key: str, value: str +) -> None: + """Source resolution settings are not part of the exported policy vocabulary.""" + streaming_guard_contract["stream"][EXTENSION][key] = value + + with pytest.raises(ValidationError): + Draft202012Validator(guard_contract_schema).validate(streaming_guard_contract) def test_contract_documentation_has_no_broken_local_links() -> None: """Relative links in the contract documentation resolve locally.""" broken_links: list[str] = [] - for document in CONTRACTS_ROOT.glob("*.md"): + for document in CONTRACTS_ROOT.rglob("*.md"): for link in MARKDOWN_LINK.findall(document.read_text(encoding="utf-8")): if "://" in link or link.startswith("#"): continue From 5cbd6c4fc44d96f4537aa13200092e676dafe11d Mon Sep 17 00:00:00 2001 From: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:17:26 +0200 Subject: [PATCH 8/8] fix(server): reserve the x-nemo extension namespace Misspelled Guardrails keys such as x-nemo-guardrail matched the generic vendor-extension pattern and bypassed policy validation. Reject every other key starting with x-nemo while still allowing unrelated x- extensions. Signed-off-by: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com> --- nemoguardrails/server/experimental/contracts/README.md | 3 ++- .../experimental/contracts/guard-contract.schema.json | 2 +- tests/server/experimental/test_guard_contract.py | 9 ++++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/nemoguardrails/server/experimental/contracts/README.md b/nemoguardrails/server/experimental/contracts/README.md index 286d83e93f..f8ac3798c2 100644 --- a/nemoguardrails/server/experimental/contracts/README.md +++ b/nemoguardrails/server/experimental/contracts/README.md @@ -30,7 +30,8 @@ output of a provider integration. Payload projections use a subset of JSON Schema vocabulary: `type`, `properties`, `required`, `items`, `oneOf`, constants, enums, and bounds. -NeMo policy is carried by `x-nemo-guardrails`. +Guard annotations use the `x-nemo-guardrails` key. Other `x-` extensions are +allowed, but every other key starting with `x-nemo` is reserved and rejected. ## Field policy diff --git a/nemoguardrails/server/experimental/contracts/guard-contract.schema.json b/nemoguardrails/server/experimental/contracts/guard-contract.schema.json index d75ed8f7f7..7e507a7f78 100644 --- a/nemoguardrails/server/experimental/contracts/guard-contract.schema.json +++ b/nemoguardrails/server/experimental/contracts/guard-contract.schema.json @@ -482,7 +482,7 @@ } }, "patternProperties": { - "^x-(?!nemo-guardrails)": {} + "^x-(?!nemo)": {} } }, "field": { diff --git a/tests/server/experimental/test_guard_contract.py b/tests/server/experimental/test_guard_contract.py index 96a958ceee..4117394915 100644 --- a/tests/server/experimental/test_guard_contract.py +++ b/tests/server/experimental/test_guard_contract.py @@ -134,12 +134,15 @@ def test_guard_contract_allows_non_guardrails_schema_extensions( Draft202012Validator(guard_contract_schema).validate(contract) +@pytest.mark.parametrize("key", ["x-nemo-guardrail", "x-nemo-guardrails-extra", "x-nemoguardrails"]) +@pytest.mark.parametrize("on_field", [False, True]) def test_guard_contract_rejects_guardrails_extension_lookalikes( - guard_contract_schema: dict, minimal_guard_contract: dict + guard_contract_schema: dict, minimal_guard_contract: dict, key: str, on_field: bool ) -> None: - """The Guardrails extension namespace rejects misspelled sibling keys.""" + """The reserved x-nemo namespace rejects misspelled Guardrails keys on objects and fields.""" contract = deepcopy(minimal_guard_contract) - contract["request"]["x-nemo-guardrails-extra"] = {} + target = contract["request"]["properties"]["prompt"] if on_field else contract["request"] + target[key] = {"classification": "opaque"} with pytest.raises(ValidationError): Draft202012Validator(guard_contract_schema).validate(contract)