Skip to content

Typed rules and collective limits

Four capabilities that make the Charter precise about what an agent may pass to a tool and how much a group of agents may do together. All of them are deterministic: no model is involved in registering a schema, validating a rule, evaluating an argument, counting a shared limit, or previewing impact.

Tool schema registry

Every tool an agent can call has a shape. The Xybern Authorisation Layer learns it in three ways:

  • At the MCP gateway. When a tools/list response passes through the gateway, each tool's inputSchema is registered automatically.
  • From the SDK. client.register_tool("payment", schema) registers the JSON Schema of a tool's arguments. The same schema again is a no-op; a changed schema becomes the next version, and the previous version is kept for the receipts that referenced it.
  • By hand. The Charter view has a Registered tools card with a Register a tool button, and POST /api/sentinel/enforcement/tools accepts a pasted schema.
from xybern import XybernClient
client = XybernClient(api_key="...")
client.register_tool("payment", {
    "type": "object", "required": ["amount", "currency"],
    "properties": {
        "amount": {"type": "number", "minimum": 0, "maximum": 1000000},
        "currency": {"type": "string", "enum": ["SAR", "AED", "USD"]},
        "beneficiary": {"type": "object", "properties": {"iban": {"type": "string", "pattern": "^SA"}}},
    },
})
client.list_tools()          # every current schema
client.get_tool("payment")   # current schema and version history

API: GET /v1/enforce/tools, POST /v1/enforce/tools, GET /v1/enforce/tools/<name>.

Each schema is flattened into argument paths (amount, beneficiary.iban) with their type, whether they are required, enum values, numeric bounds, pattern and format. That flattened view is what the compiler, the validator, the constraint form and pre-flight use.

Typed argument rules

A mandate can now compile to an argument primitive: a rule over a registered tool's arguments, validated against the schema before it is signed.

{"type": "argument", "tool": "payment", "decision": "block",
 "conditions": {"operator": "AND", "rules": [
   {"field": "amount", "operator": ">", "value": 50000, "currency": "SAR"},
   {"field": "currency", "operator": "not_in", "value": ["SAR"]}]}}

What "typed" buys you over a metadata rule:

Property Metadata rule Typed argument rule
Field must exist in the tool's schema no yes, rejected at compile time otherwise
Operator must fit the field's type no yes (> on a string, matches on a number are rejected)
Enum values checked no yes (currency in ["EUR"] is rejected when the enum is SAR, AED, USD)
Missing argument does not match (except !=, not_in) counts as a violation (fails closed)
Argument of the wrong type compared as text counts as a violation
Amount in another currency than the cap compared as a number counts as a violation unless the currencies match
Nested paths, list containment, ranges dotted paths, contains dotted paths, contains_any, contains_all, subset_of, between, not_between, starts_with, ends_with, length_gt, length_lt

Operators: == != > >= < <= between not_between in not_in contains not_contains contains_any contains_all subset_of matches starts_with ends_with exists not_exists. A rule may carry "on_missing": "ignore" when an optional argument should not fail closed. String comparisons ignore case and surrounding whitespace so a case variant cannot slip past an enum.

Arguments are read from metadata.arguments (the MCP gateway puts the call's arguments there) or from the metadata itself (an SDK intercept). The compiler emits typed primitives whenever the outcome names a tool with a registered schema; the wizard's review step shows an editable constraint form per typed primitive (field, operator, value, live-validated against the schema), and the build-it-yourself tab has a Typed rule on a tool card. Generated Charter tests, probes and the signed diff treat typed rules like metadata rules. Pre-flight quotes the argument's type and declared range in "what it would take": amount at most 1000 (number, declared range 0..1000000).

Cross-agent impact preview

The backtest now reports impact: for every agent in the workspace, how many recorded decisions in the window would have come out stricter under the new rules, with the new decision split. The wizard shows it under the backtest result and the Backtest button on an existing mandate shows it too. An agent-scoped mandate only changes that agent's decisions; a workspace mandate is replayed across everyone. Semantic guards are not replayed (no model calls), and the report says so; the deterministic share is exact.

Collective limits

A velocity rule counts an agent's matching actions in a window and fires when the count, including the current action, exceeds the limit. With a scope the counter is shared:

{"type": "sequence", "decision": "escalate",
 "conditions": {"mode": "velocity", "match_action_types": ["hvac_setpoint_*"],
                "max_count": 12, "window_seconds": 600, "scope": "workspace"}}
  • scope: "agent" (default) the acting agent alone.
  • scope: "department" every agent in the department the rule names (department_id) or, when it names none, the departments the acting agent belongs to. An agent in no department is counted alone, never ignored.
  • scope: "workspace" every agent in the workspace, a swarm limit.

The source of truth is the decision log, shared by every worker and relay. When Redis is present the same history is mirrored into a sorted set per agent so a swarm limit does not query the table on every action; the mirror is only trusted once it has been recording for longer than the window asked for, and any doubt falls back to the table. Dry runs (pre-flight, Charter tests, probes, backtests, Ask the Charter) read the history and never write it.

The counter state every rule saw is recorded with the decision and shown on the receipt under authorisation.collective_limits: scope, group, members, count, limit, window, remaining, source. Pre-flight returns the same for each step so an agent can see how much of a shared budget is left before it acts. The Dubai airport demo ships with a swarm limit on setpoint changes across the HVAC swarm, and the build-it-yourself tab has a Collective limit card (family, max, window, scope).

Compliance notes

Typed rules and collective limits are evaluated inside the deterministic layer, sealed to the Provenance Vault with the decision, and carry no external dependency. The receipt of a decision taken under a collective limit is self-describing: it states the shared count that decided it.