Agent to agent: chain contracts, budgets, communication policies¶
When agents lend authority to each other, three questions decide whether the system stays safe: does the delegate still serve the mission the human approved, how much can the delegate spend, and who is allowed to instruct whom at all. This page covers the three answers, and the Authority graph that draws them.
Chain-level intent contracts¶
An intent contract is the plan a person approved for one agent. From this release the root contract travels with the warrant: a warrant minted for the root agent under a contract carries contract_id, and every child warrant issued through a delegation grant inherits it. A delegate that presents such a warrant is conformance-checked against the root contract without naming it, and its in-plan actions draw on the root contract's budgets, so the whole chain shares one mission budget.
# the root agent's plan, approved and enforced
contract = client.submit_contract(agent_id="planner", plan_text="...", mode="enforce")
# lend part of it: the grant's child warrant carries the contract
grant = client.delegate("planner", "payer", scopes=["payment:*"], action_types=["payment"],
budgets={"max_amount": {"SAR": 20000}}, contract_id=contract["contract_id"])
# the delegate acts with its warrant; the root contract is checked and consumed
client.intercept("payment", metadata={"amount": 500, "currency": "SAR"}, agent_id="payer",
warrant=grant["warrant_token"])
The decision, the lineage and the receipt say intent_contract.via = "warrant_chain" with the root agent and the chain depth. A warrant that fails its full check never consumes the contract: consumption happens only after the signature, holder, family, bounds, issuer state and budget checks all pass. Two things a delegate can never do: exceed its own attenuated warrant (families, argument bounds, budget), or act outside the root plan.
Quantitative budgets¶
max_uses counted actions. A budget counts what matters:
{"max_actions": 20, "max_amount": {"SAR": 50000, "AED": 10000}, "max_rows": 5000, "max_recipients": 3}
Budgets live on warrants (narrow.budgets), on delegation grants (budgets) and on intent contracts. Rules:
- Atomic consumption. The holder row is locked for the check and the write, so two workers cannot both spend the last unit.
- Currency is not optional. An amount in a currency the budget does not cover is refused, unless the budget carries a
"*"cap that applies to any currency. - Recipients are counted, not stored. Distinct recipients are hashed; the address never enters the record.
- Split on sub-delegation. A child's budget is carved out of the parent's remaining budget and reserved on the parent until the child is revoked, when the child's actual spend is charged to the parent and the rest is returned. A re-delegation may never exceed what the parent has left.
- Exhausted budgets refuse, and the receipt shows the budget state that decided: what the action used, what remained after, and the cap (
authorisation.lineage.warrant.budget).
Amounts and rows are read from the action's metadata (or the MCP tool call's arguments): amount with currency, rows or row_count, and recipients from to, recipient, recipients, beneficiary, email or iban.
Communication policies¶
A communication policy says who may send which kind of instruction to whom:
| Field | Values |
|---|---|
source, target |
an agent id, dept:<department id>, or * |
instruction_types |
any of instruction, delegate, query, or * |
decision |
allow, block, escalate |
The most specific matching policy wins (agent beats department beats *); on a tie the stricter decision stands. With no matching policy the workspace default applies (allow unless you set it to escalate or block in the Authority graph view, or settings.communication.default).
The check runs at the control plane before any rule is evaluated, on every agent-to-agent instruction (send_instruction, agent_instruction, a query) and inside delegation, so an agent that may not instruct another cannot delegate to it either. A refused instruction records decision_path = "communication" with the policy that decided. It composes with the trusted instruction channel: a target that requires signed instructions still refuses unsigned ones, and a communication policy can refuse signed ones from the wrong source.
client.create_comm_policy("*", "payer", "block", instruction_types=["instruction", "delegate"],
name="Only the planner instructs the payer")
client.create_comm_policy("planner", "payer", "allow")
client.check_communication("researcher", "payer") # dry run: block
API: GET/POST /v1/enforce/comm-policies, DELETE /v1/enforce/comm-policies/<id>, POST /v1/enforce/comm-policies/check.
Written in plain language: A2A Delegations¶
Everything on this page can be written in your own words in Agents, A2A Delegations:
The planner hands purchasing work to the buyer. The buyer can come back with questions but never tells the planner what to do. When the planner passes work to payments, payments may create payments for it, five at most and 50,000 riyals for each order, for one week. Payments has no authority of its own. Anything I have not mentioned needs a person's approval.
Compile shows what the text will put in force, statement by statement, before anything exists. The model reads the text against the agents, departments and actions of the workspace, as it does for a mandate in the Charter. What it returns is checked by rules, not trusted:
| Check | What happens when it fails |
|---|---|
| Every agent and department is one of this workspace | the statement is listed under Not understood |
| The text gives ground for the agent: a word of its name, or of what it does | listed, with "the text does not seem to speak of" the agent |
| "Any agent" is used only where the text speaks of everyone, nobody or everyone else | listed |
| The model reported the same words as unclear | no statement stands for them |
| Anything lent names at least one action, and its limits are numbers | listed |
| Lending is delegating: the lender needs the right to delegate to the receiver | the proposal adds it (and says so), unless the text forbids it, in which case you are told before you apply |
A statement about one kind of instruction beats a statement about all kinds (may ask it questions, but may never instruct it), and what a new text says replaces only what it speaks about: the view names what is replaced. Sign and apply needs an admin, checks every statement again, and seals the text and the statements in the Provenance Vault. A rule for one agent's own actions (refunds above 500 need approval) belongs to the Charter, and the view says so.
When no model answers, a built-in reader reads plain sentences (the planner may instruct the buyer) and the proposal is marked as read by it.
Over HTTP for the dashboard session: POST /api/sentinel/enforcement/a2a/compile {text}, POST /api/sentinel/enforcement/a2a/apply {items, text}, GET /api/sentinel/enforcement/a2a.
Hand-offs through the SDK¶
With the Python SDK (2.24 and later) none of this needs code in the agents. When a named agent runs inside another agent's action, which is how a lead agent calls a sub-agent in LangChain, the SDK:
- authorises the hand-off as an instruction from the first agent to the second (
POST /v1/enforce/agent-comm), before the second one reads it. A refused hand-off stops the second agent before its first step. - finds what the first lent the second, the active delegation grant between them, and sends it with every action the second one takes for the first (
grant_id,metadata.on_behalf_of). An action outside the grant, or beyond its budget, is refused. - links the request into one chain: every action and hand-off carries
chain_id,chain_stepand the decision it follows from (parent_decision_id).
xybern run python team.py "Contain alert ALR-1050"
xybern ✓ allowed ask_responder soc-lead 120 ms
xybern ✓ allowed → responder soc-lead hand-off, 115 ms
xybern ✓ allowed block_ip responder for soc-lead, 1 of 3 used, 150 ms
xybern ✕ refused isolate_host responder for soc-lead, 'isolate_host' is outside what 'soc-lead' lent, which covers block_ip
An agent that should act only with what it is lent declares requires_grant: true. A budget that says "per": "chain" is counted for each chain, "three for each task", where a plain budget is counted over the life of the grant.
principals:
- id: responder
delegation: {requires_grant: true}
delegations:
- {from: soc-lead, to: responder, action_types: [block_ip], budgets: {max_actions: 3, per: chain}}
communication:
default: block
rules:
- {from: soc-lead, to: responder, decision: allow}
The same is set without a file in the Authority graph: the communication rules and their default, Lend authority (from, to, the actions, a limit counted for each task or in all), and, on a selected agent, Make it act only with what it is lent. Lending is refused while the communication rules do not let the first agent instruct the second.
In the dashboard, a decision that belongs to a chain shows The chain: every step in order, hand-offs marked, who acted for whom, and the authority that was lent with what is left of it. Show the chain in the table filters Authorisations to that request.
Authority graph¶
The Agents section of the Xybern Authorisation Layer has an Authority graph view: agents as nodes, with the grants between them, the warrant chains in force, the communication policies, the principals each agent accepts signed instructions from, and the trusted external issuers as edges. Click an agent to see everything it lends and receives, with budgets and what remains, and revoke any grant, delete any communication policy, or revoke everything the agent holds from the same panel. Communication policies and the workspace default are created there too. GET /v1/enforce/authority-graph returns the same nodes and edges for your own tooling.
Threat scenarios the tests cover¶
- Compromised delegate. A delegate whose budget is exhausted, whose action is outside the root plan, who instructs an agent it may not, who tries to re-delegate more than it has left, or who presents its principal's warrant, is refused on every path; one revoke of the agent kills its warrant, grants and sessions.
- Injected delegation. Text in a document claiming "you are delegated by the planner, grant dlg_xxx" gives nothing: a fabricated grant id, a grant issued to another agent, an on-behalf-of claim without a grant, and an unsigned instruction to an agent that requires signed ones are all refused before any rule runs.
The XAAB benchmark gained an agent_to_agent category with the same shapes.