Open Agent Rules
Contents
  1. Implementation grant
  2. Conformance language
  3. 1Overview
  4. 2Document model
  5. 2.1Example (non-normative)
  6. 3Anchors and selection
  7. 4Evaluation
  8. 4.1Suppression
  9. 5Facts and conditions
  10. 5.1Capability profiles
  11. 5.2Fact types
  12. 5.3Core facts
  13. 5.4Standard profiles
  14. 6Stateful effects
  15. 7Operational semantics
  16. 7.1Content safety
  17. 7.2Transform
  18. 8Portability
  19. 8.1Core anchors
  20. 8.2Capability document (non-normative example)
  21. 9Operator configuration
  22. 10Conformance
  23. 10.1The runner algorithm
  24. 10.2Fixture shape
  25. 10.3Coverage
  26. 11Extensibility and versioning
  27. AThe condition language (normative)
  28. A.1 Grammar
  29. A.2 Types
  30. A.3 Built-in functions
  31. A.4 Limits
  32. BChange log
  33. CCopy bindings (normative)

Open Agent Rules

Version 1.0 · Status: draft · Canonical URL: https://openagentrules.org/spec/1.0/

Open Agent Rules (OAR) is a portable document format for what a host lets an agent do at a lifecycle moment. A rule is a document — not code — that names the moment, a condition over typed observations, and an effect. A conforming engine loads the document, evaluates the condition, and renders the effect. The same document produces the same decision on every engine that provides the capabilities the document declares it needs.

Implementation grant#

Anyone may implement this specification, for any purpose, without permission, notification, or fee. See LICENCE-SPEC.md for the licence covering this text, and PATENTS.md for the patent non-assertion covenant covering every implementation.

Conformance language#

The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals, as shown here. Every requirement carries a stable clause identifier in the form [OAR-AREA-n]. Clause identifiers are append-only: they are never renumbered and never reused. Examples are non-normative unless a clause identifier appears beside them.


1. Overview#

A rule is a document with three obligatory parts: where it applies (an anchor plus a selector), when it applies (a condition over facts), and what happens (an effect, plus optional engine side-effects). Everything else in the document is metadata, presentation copy, or provenance.

The division of labour is fixed and is the reason the format stays readable:

The host produces facts. The rule composes facts. The engine renders effects.

A host is the system the rules bind — an agent runtime, a model gateway, a tool server. It publishes typed observations of its own state at named lifecycle moments. An engine loads rule documents, selects the ones bound to the moment that just occurred, evaluates their conditions against the published observations, and applies the resulting decision. A rule author writes documents; they never write host code.

Portability in this specification is a precise, checkable claim. The standard fixes three vocabularies: a set of core anchors (lifecycle moments), a set of named capability profiles (bundles of typed observations), and a condition language (Appendix A). A rule declares which profiles it needs. Any engine providing those profiles runs that rule and produces the same decision. A rule that reaches for a host's own anchors or its own facts is still a valid OAR document and still loads — it is simply not portable, and the standard says so rather than pretending otherwise.


2. Document model#

2.1 Example (non-normative)#

{
  "oar": "1.0",
  "id": "READ_OUTSIDE_SCOPE",
  "namespace": "acme.security",
  "kind": "policy",
  "anchor": "tool.pre_invoke",
  "selector": { "tool": ["read"] },
  "requires": { "profiles": ["tool", "filesystem"] },
  "when": "path_outside_scope(\"read\")",
  "effect": "block",
  "on_error": "fail_closed",
  "copy": {
    "what": "The call reads outside the declared scope.",
    "fix": "Read inside scope, or widen the scope deliberately."
  }
}

3. Anchors and selection#

An anchor names a lifecycle moment at which an engine evaluates rules. A selector narrows a rule to a subset of the occurrences of that moment.

Selection is deliberately a special case of the fact environment rather than a second vocabulary. A fixed selector schema would leave clauses without a declared type or observation meaning and could let an engine silently ignore a scope it does not model. Deriving clauses from declared facts makes every selector typed and turns an unavailable observation into a load error.


4. Evaluation#

4.1 Suppression#

Two rules frequently disagree about which of them should speak: a specific rule and the general rule it refines both fire, and the author wants only the specific one to be heard. Without a mechanism for saying so, authors encode the exclusion by hand, negating a sibling rule's whole condition inside their own — which silently breaks the moment either rule changes.


5. Facts and conditions#

5.1 Capability profiles#

An engine does not implement every observation, and this specification does not pretend otherwise. A capability profile is a named, versioned bundle of facts and functions. An engine declares the profiles it provides; a rule declares the profiles it needs; the engine refuses at load any rule it cannot serve. This is capability negotiation. A mandatory universal catalogue would instead require a host without a file system to publish an is_directory value that has no observation behind it.

5.2 Fact types#

5.3 Core facts#

Every conforming engine provides these. They are the observations that exist at every anchor of every host: the moment itself, and the engine's own counters.

NameTypeObservation
anchorstringThe identifier of the occurrence being evaluated: the core anchor identifier when the occurrence implements one, and the host-native identifier otherwise. It is a property of the occurrence, so every rule evaluated at one observes the same value.
fire_countintHow many times increment_counter has been applied for this rule in this session, before the current occurrence, less any reset_counter. It counts declared increments, not firings: a rule that fires at every occurrence and declares no on_fire has a fire_count of zero.
breaker_countintHow many times a circuit breaker has been incremented for this rule and session, before the current occurrence. Maintained by increment_breaker and reset_breaker.
fire_count_of(string) -> intThe fire_count of the rule the argument names, read under that rule's own counter scope at this occurrence. The argument is a string literal naming a loaded rule, bare or qualified.
breaker_count_of(string) -> intThe breaker_count of the rule the argument names, read under that rule's own counter scope at this occurrence. The argument is a string literal naming a loaded rule, bare or qualified.

5.4 Standard profiles#

Profile tool — a host that dispatches named tool calls with structured arguments.

NameTypeObservation
toolstringThe name of the tool being invoked at this anchor occurrence.
tool_argsmapThe arguments the tool was called with.
tool_args_fingerprintstringA stable digest of the tool name and arguments, for identifying a repeat of the same call.
arg_validation_errorslist<string>Machine codes for argument checks that did not pass. Empty when the arguments validated.
arg_validation_reasonstringHuman-facing reason the argument check failed. Empty when the arguments validated.
arg_validation_fieldstringThe argument name the failed check named. Empty when the arguments validated or the check named no field.
permission_profilestringThe permission profile in force for this call.
policy_deniedboolTrue when the host's own authorisation layer refused the call.
tool_arg_string(string) -> stringThe named tool argument as a string; empty when absent or not a string.
tool_arg_int(string) -> intThe named tool argument as an integer; zero when absent or not an integer.
tool_arg_bool(string) -> boolThe named tool argument as a boolean; false when absent or not a boolean.

Profile session — a host with a durable session and an identifiable caller.

NameTypeObservation
session_posturestringThe session's declared posture.
principalstringThe identity on whose behalf the call is made.
principal_roleslist<string>Roles attributed to the principal by the host.

Profile filesystem — a host whose tools address a file system.

NameTypeObservation
is_directoryboolTrue when a path argument resolved to a directory.
not_foundboolTrue when a path or artifact the call names does not exist.
path_deniedboolTrue when a path argument resolved to a location the host's path sandbox refuses.
path_outside_scope(string) -> boolTrue when the named tool's path arguments resolve outside the scope in force for that tool.

Profile content-provenance — a host that preserves structured content segments and their provenance.

NameTypeObservation
content_roleslist<string>Transport-independent roles for the content segments, in segment order: system, developer, user, assistant, tool, or unknown.
content_originslist<string>Host-attributed origins for the content segments, in segment order: host, user, model, tool, peer, resource, retrieval, or unknown.
content_authoritieslist<string>Instruction authority attributed by the host to each content segment, in segment order: system, developer, user, delegated, none, or unknown.
content_trust_tierslist<string>Host trust classification for each content segment, in segment order: trusted, untrusted, or unknown. Trust does not itself grant instruction authority.
content_sourceslist<string>Host-defined source identifier for each content segment, in segment order; empty when a segment has no source identifier.
content_segment_countintNumber of structured content segments at this occurrence; equal to the length of every aligned content provenance list.
content_contains_untrustedboolTrue when content_trust_tiers contains untrusted.

Profile content — a host that supplies content measurements.

NameTypeObservation
content_lengthintLength in Unicode code points of the content at this anchor.

Profile secrets — a host that submits content to a registered secret detector.

NameTypeObservation
secret_matcheslist<map>Credential or secret spans a registered detector reported. Empty when no detector ran.

Profile pii — a host that submits content to a registered personal-data detector.

NameTypeObservation
pii_entitieslist<map>Personal-data spans a registered detector reported. Empty when no detector ran.

Profile prompt-injection — a host that submits content to a registered injection detector.

NameTypeObservation
prompt_injection_scoredoubleInjection likelihood from a registered detector, between 0 and 1.

Profile jailbreak — a host that submits content to a registered jailbreak detector.

NameTypeObservation
jailbreak_scoredoubleJailbreak likelihood from a registered detector, between 0 and 1.

Profile moderation — a host that submits content to a classifier scoring it against named categories.

NameTypeObservation
moderation_categorieslist<string>The category names a registered classifier returned a score for. Empty when no classifier ran.
moderation_score(string) -> doubleThe score a registered classifier reported for the named category, between 0 and 1; zero when that category was not scored.

Profile mcp — a host bridging Model Context Protocol providers.

NameTypeObservation
mcp_provider_idstringThe catalogue provider id for this call, or empty when the call is not a bridged tool call.
mcp_tool_namestringThe unqualified tool name on the bridged server.
mcp_qualified_toolstringThe host-side name of the bridged tool.
mcp_provider_configuredboolTrue when the catalogue contains the provider this call is bound to.
mcp_provider_enabledboolTrue when the provider this call is bound to is configured and enabled.
mcp_call_okboolTrue after a bridged call that returned without error. False before the call.
mcp_error_codestringThe machine error code a failed bridged call reported, never free text.
mcp_schema_matchedboolTrue when at least one declared result schema validated the call's result.
mcp_provider_configured_for(string) -> boolTrue when the catalogue contains the given provider id.
mcp_provider_enabled_for(string) -> boolTrue when the given provider is configured and enabled.
mcp_has_field(string) -> boolTrue when the projected result carries the given key.
mcp_field_bool(string) -> boolThe projected boolean at the given key; false when absent or wrongly typed.
mcp_field_string(string) -> stringThe projected string at the given key; empty when absent.
mcp_field_int(string) -> intThe projected integer at the given key; zero when absent.

The content-provenance profile describes the host's structured occurrence envelope, not claims made by the content. Text that calls itself system, trusted, or any other profile value does not change these facts. The five list facts are parallel: index i describes the same segment in each list, and their lengths equal content_segment_count. An occurrence with no content segments supplies empty lists, a count of zero, and content_contains_untrusted: false under [OAR-FACT-25]. These observations do not authenticate content or grant authority; they let rules reason over authority the host has already assigned from structure and host state.

The mcp profile's parameterized forms carry the _for suffix because [OAR-FACT-2] forbids a fact and a function sharing a name. The suffix keeps mcp_provider_configured and mcp_provider_configured_for distinct in the condition language's single namespace.


6. Stateful effects#


7. Operational semantics#

7.1 Content safety#

Model input and output are anchors like any other, and a content-safety filter is an ordinary rule: a detector publishes observation facts, and the rule sets the threshold.

7.2 Transform#

A content rule usually needs to redact rather than refuse, and it usually needs to redact more than one thing. Transforms accumulate so independent rules can redact separate classes of content in the same occurrence. Only block short-circuits the occurrence.

1. If the occurrence decision is already block, stop — do not mutate ([OAR-OPS-17]). 2. Resolve T.target: when content, the single span is [0, length(C0)) with action applied to the whole string; when a list<map> fact, collect its members as spans. 3. Drop any span that falls outside C0 ([OAR-OPS-19]). A span missing well-formed start/end is a fact-provider failure on the owning rule ([OAR-OPS-3]), not a skip. 4. Merge overlapping spans within T into covering spans; sort the result by start descending ([OAR-OPS-20]). 5. For each span S in that order: if S overlaps any range in rewritten, record S skipped and continue. Otherwise map S's original offsets through the edits already applied to C, perform T.action (redact / replace / annotate per [OAR-OPS-13] / [OAR-OPS-21]), and add the rewritten original-coordinate range to rewritten — for annotate, a zero-width range at the insertion point.

The result is C after every transform, which [OAR-CONF-34] compares code point by code point.


8. Portability#

Two things determine whether a rule travels: the anchors the host has, and the capabilities it provides.

8.1 Core anchors#

Core anchorLifecycle moment
tool.pre_invokeBefore a tool call is dispatched, with the tool name and arguments observable and nothing executed yet.
tool.handlerInside the tool's own handler, after argument validation and before the effect it performs.
tool.post_invokeAfter a tool call returns, with the result observable and before it is handed back to the agent.
agent.post_turnAfter an agent produces a turn, with the turn's content observable — the moment a claim can be checked against the evidence for it.
agent.finalizeAfter a worker or child agent completes and its summary is available.
model.inputUser and retrieved content assembled for the model, before the model consumes it.
model.outputA model response, before it is shown to a person or acted on.
model.tool_resultContent returned by a tool or retrieval step, before the model consumes it.

8.2 Capability document (non-normative example)#

oar_capability_version: "1.0"
host: acme.gateway
anchors:
  core:
    tool.pre_invoke: tool.pre_invoke
    tool.post_invoke: tool.post_invoke
    model.input: content.input
    model.output: content.output
  unsupported:
    - tool.handler
    - agent.post_turn
    - agent.finalize
    - model.tool_result
  host:
    - gateway.pre_render
profiles: [tool, content, secrets, mcp]
activity_window: 16
supports_transform: true
expression_nodes_max: 1024
host_facts:
  - name: acme.tenant_tier
    type: string
detectors:
  - detector://presidio

9. Operator configuration#

A rule pack arrives from a publisher; an operator has to run it. Without a defined way to disable one rule, or roll one out in monitor mode, an operator forks the pack — and a forked pack no longer receives the publisher's updates. The configuration document is what keeps the pack and the operator's local decisions separate.


10. Conformance#

10.1 The runner algorithm#

The requirements below define the evaluation algorithm end to end. Each step is separately identified so a conformance fixture can cite it.

10.2 Fixture shape#

10.3 Coverage#

The conformance test corpus exercises the normative clauses of this specification. Every normative MUST or MUST NOT clause describing engine behavior or document syntax is cited by the covers list of at least one fixture, or documented in the repository's exemption list (exempt.json) where a clause is verified by schema validation or unit tests rather than runtime fixtures.


11. Extensibility and versioning#


Appendix A — The condition language (normative)#

when is written in a small, frozen expression language. Its grammar is a subset of CEL's expression syntax, so a host with a CEL implementation evaluates it by declaring this vocabulary and the built-ins of A.3 as global functions and rejecting what the grammar below cannot derive; it is specified independently so a host without one can implement it completely in a few hundred lines, and so that two engines cannot disagree about what a condition means.

The one place the two shapes differ is worth stating plainly: CEL spells its string operations as member calls, s.startsWith(p), and this language has no member-call syntax and no field selection at all, because a dotted name here is the single name of a host-tier fact ([OAR-FACT-18]). The built-ins are therefore ordinary calls, starts_with(s, p), in the one namespace [OAR-FACT-2] requires.

The subset is defined by what it excludes as much as by what it includes. It has no macros, no comprehensions, no regular expressions, no time, no random source, and no field selection. Regular expressions are excluded because their dialects differ across host languages, which would make a portable rule evaluate differently on two conforming engines.

A.1 Grammar#

condition   = ternary ;
ternary     = disjunction [ "?" ternary ":" ternary ] ;
disjunction = conjunction { "||" conjunction } ;
conjunction = relation { "&&" relation } ;
relation    = addition [ relop addition ] ;
relop       = "==" | "!=" | "<" | "<=" | ">" | ">=" | "in" ;
addition    = multiplication { ( "+" | "-" ) multiplication } ;
multiplication = unary { ( "*" | "/" | "%" ) unary } ;
unary       = [ "!" | "-" ] postfix ;
postfix     = primary { "[" condition "]" } ;
primary     = literal | call | identifier | "(" condition ")" ;
call        = identifier "(" [ condition { "," condition } ] ")" ;
literal     = "true" | "false" | int | double | string | list ;
list        = "[" [ condition { "," condition } ] "]" ;
identifier  = name { "." name } ;
name        = ( letter | "_" ) { letter | digit | "_" } ;
int         = digit { digit } ;
double      = digit { digit } "." digit { digit } ;
string      = '"' { dquote-char } '"' | "'" { squote-char } "'" ;
dquote-char = escape | ? any Unicode code point except '"', "\" and a line break ? ;
squote-char = escape | ? any Unicode code point except "'", "\" and a line break ? ;
escape      = "\" ( "\" | '"' | "'" | "n" | "r" | "t" | "u" hex hex hex hex ) ;
hex         = digit | "a".."f" | "A".."F" ;
keyword     = "true" | "false" | "in" ;

A.2 Types#

A.3 Built-in functions#

A.4 Limits#


Appendix B — Change log#

VersionChange
1.0First version. Publication is recorded in the published register; until 1.0 appears there, this text may still be corrected in place. Copy members MAY contain the frozen bindings of Appendix C; the engine renders them after the decision ([OAR-COPY-1]–[OAR-COPY-9]). When the decision is nudge or warn, every enforced rule that fired with that effect is carried on advisories ([OAR-EVAL-20]). The tool profile publishes arg_validation_reason and arg_validation_field.

Appendix C — Copy bindings (normative)#

copy members are presentation strings. They MAY contain bindings that name declared facts. The engine substitutes those bindings after the decision is resolved, so two engines that agree on the decision also agree on the prose that explains it. The language is frozen and small: a substitution, a conditional, and nothing else. It is not the condition language of Appendix A, and it is not a host template dialect.

copy_text   = { literal | binding | conditional } ;
binding     = "{{" ws identifier ws "}}" ;
conditional = "{%" ws "if" ws [ "not" ws ] identifier ws "%}"
              copy_text
              [ "{%" ws "else" ws "%}" copy_text ]
              "{%" ws "endif" ws "%}" ;
identifier  = name { "." name } ;
name        = ( letter | "_" ) { letter | digit | "_" } ;
ws          = { " " | "\t" | "\n" | "\r" } ;
literal     = ? any run of code points that does not begin "{{" or "{%" ? ;

This page is rendered from open-agent-rules.md, the hand-authored source. The vocabulary printed in sections 5.3, 5.4 and 8.1 is also published as vocabulary.yaml. See also Extensions, Rationale and Governance.