Agent-Safe Intent conformance
An implementation conforms when it reproduces the canonical bytes and the intent hash of every vector in the corpus, and hashes every mutation of a binding differently from the binding and from each other. Run the corpus offline with the AgentSafe runtime.
- Corpus
- v0.5.0 (e1e702f)
- Checked
- 2026-10-29
Run it
With AgentSafe installed, from a clone of the repository at the corpus release. Exit 0 means every pinned hash was reproduced; 1 names the vector that was not, with the expected and the computed value.
git clone --depth 1 --branch v0.5.0 https://github.com/decionis/agent-safe-pipeline
cd agent-safe-pipeline
agentsafe verify intent conformance/vectors conformance/agent-safe-intent-v1.json
agentsafe verify intent conformance/frameworksOr with the independent verifier, no AgentSafe install
npx @decionis/verify intent conformance/vectors conformance/agent-safe-intent-v1.json conformance/frameworks@decionis/verify implements the specification with no code shared with the runtime, so the two agreeing is what shows the corpus pins the specification rather than one implementation of it.
A conformance claim names the corpus it was made against, because the corpus grows. For this page that is v0.5.0 (e1e702f). Passing says nothing about an authority's decisions or a runtime's enforcement; the boundary test covers enforcement.
Framework coverage
What each framework's tool call already carries
A description, not a grade: none of these frameworks was designed to carry execution authority, and none has to change for a producer built on it to conform. Each column is one vector, the same kind of consequential call captured from that framework's own record.
| Capability | Binding property | OpenAI Responses API / Agents SDK | Vercel AI SDK | LangChain | MCP (tools/call) |
|---|---|---|---|---|---|
action_identityaction.typeIs the action named by a stable identifier the policy can refer to? | action.type | native | native | native | native |
parametersaction.parametersAre the arguments a JSON object that canonicalizes as they are, or text to be parsed? | action.parameters | adapter | native | native | native |
target_identityaction.resourceIs what is acted on named apart from the parameters? | action.resource | adapter | adapter | adapter | adapter |
principalactorIs the acting identity carried with the action, by the runtime and not by the agent? | actor | adapter | adapter | adapter | adapter |
expiryexpires_atIs the intent bounded in time? | expires_at | adapter | adapter | adapter | adapter |
idempotencycontext.idempotency_keyDoes a retry present the same intent and a new attempt a new one, by a runtime-chosen key? | context.idempotency_key | adapter | adapter | adapter | adapter |
intent_digestthe hashIs there one digest over the whole of the above that authority can be bound to? | the hash | adapter | adapter | adapter | adapter |
effect_correlationthe resultCan the effect that followed be tied to exactly this call? | the result | native | native | native | native |
- native
- the framework's own tool-call record carries it
- adapter
- the trusted runtime adds it when it captures the call
- not represented
- neither the record nor the adapter carries it
- not tested
- no vector covers it yet
The capabilities and their questions are §8 of the specification. CrewAI, AutoGen, Mastra and others have no vector yet; adding one is a pull request under conformance/frameworks/.
OpenAI Responses API / Agents SDK
An OpenAI Responses API function_call item, the record a function tool produces (the OpenAI Agents SDK's function tools produce the same item). The arguments arrive as a JSON-encoded string; everything but the action name and the arguments is the trusted runtime's to add.
- parameters
- arguments is a JSON string; the adapter parses it into the object that is hashed
- effect_correlation
- function_call_output echoes call_id
- idempotency
- call_id identifies the model's call, not a retry boundary; the runtime chooses the key
The framework's record
{
"id": "fc_synthetic_1",
"call_id": "call_synthetic_1",
"type": "function_call",
"name": "refund_order",
"arguments": "{\"orderId\":\"synthetic-1\",\"amountMinor\":35000,\"currency\":\"USD\"}"
}Intent hash
sha256:fbb961ed4a8e5a8b39ff92328c6573b9e688a4aaa9069ab5aaf5bd6ea5b08fea
Vercel AI SDK
A Vercel AI SDK tool-call part, as generateText and streamText return it. The input is already an object; everything but the tool name and the input is the trusted runtime's to add.
- effect_correlation
- the tool-result part carries the same toolCallId
- idempotency
- toolCallId identifies the model's call, not a retry boundary; the runtime chooses the key
The framework's record
{
"type": "tool-call",
"toolCallId": "call_synthetic_2",
"toolName": "refund_order",
"input": {
"orderId": "synthetic-1",
"amountMinor": 35000,
"currency": "USD"
}
}Intent hash
sha256:147eb95792bdc1be431e3512c607c8def3dcdec1055c4c8e817c49540de7228f
LangChain
A LangChain ToolCall, as an AIMessage carries it in tool_calls (LangGraph agents pass the same object to a ToolNode). The args are already an object and the id is optional; everything but the name and the args is the trusted runtime's to add.
- effect_correlation
- the ToolMessage carries the same id as tool_call_id; the id is optional on the call
- idempotency
- id identifies the model's call, not a retry boundary; the runtime chooses the key
The framework's record
{
"type": "tool_call",
"id": "call_synthetic_3",
"name": "refund_order",
"args": {
"orderId": "synthetic-1",
"amountMinor": 35000,
"currency": "USD"
}
}Intent hash
sha256:72e34c9754c82b3cfa820410a470874f7a364dd113cf3be82ed829aebccb8673
MCP (tools/call)
An MCP tools/call request, at the moment the model has chosen a tool and constructed its arguments. Unlike the other three frameworks, the record-to-proposal mapping is not written by the integration: @decionis/agentsafe ships it as bindMcpInvocation, and the tool binding below is what an operator declares.
MCP names the tool and carries its arguments as an object, so the action and the parameters are native and the JSON-RPC id is the correlation the result echoes. What MCP has no place for is what makes an invocation authorizable: which resource the action lands on, who proposed it, when the proposal stops being current, and the key an effect is bound to. The shipped binder derives the target from the arguments rather than accepting one, and the trusted runtime supplies the rest.
The framework's record
{
"jsonrpc": "2.0",
"id": "call_synthetic_1",
"method": "tools/call",
"params": {
"name": "delete_customer",
"arguments": {
"customerId": "synthetic-1"
}
}
}Intent hash
sha256:b275810adef3e8f9192430270f15e294f1fe41dbea789c916e89d6726b521b30
The corpus
Hash vectors
Each vector pins a JSON value, its canonical bytes and its hash. The first is a whole binding; the canonicalization cases each isolate one rule of the canonical form (§4); the adversarial vector holds §6 to account.
| Vector | What it pins | Intent hash |
|---|---|---|
| agent-safe-intent-v1 | Cross-implementation hash vector for agent-safe.intent/1 | sha256:aff2fd3aa1a7557a77f20afa5583f56eb0c39529304b31f69ee78e1e7b53e316 |
| composed-vs-decomposed | NFC (café) and NFD (café) are intentionally DISTINCT — no implicit Unicode normalization | sha256:4783c9bb9e92da817ff30c7f0e10dac952b0b1893e654cb006a067aa4f8d7595 |
| fractional-exponent | Fractional and exponent-form numbers use JS JSON encoding (1e+21) | sha256:2bf2414e38ed0efe498fa735f624fd758adf2a647b1a14b1208e0734976849f8 |
| negative-zero | Negative zero canonicalizes to 0 per JS JSON encoding | sha256:e6a312b1d452aa705e11d045f19ba9f7097fbbc3a7ef956573c98e6d81203b6a |
| nested-arrays | Nested arrays canonicalize depth-first | sha256:7e576e1ac9e486dccda1d68df4baf27b2ed22f8d23a39e63748bafe2e4e76e26 |
| policy-projection-absent | The same intent as policy-projection.json with no policy projection: the binding, bytes and hash an intent without one has always produced (it is byte-identical to @decionis/agent-safe-pipeline 0.4.0's), and the authority evaluates its action type as the decision type, transaction type and workflow key. | sha256:bbd2b45a06920fe1a0caf82a51bd2d70394114ed8f687bec1277784537e21717 |
| unicode-astral | Unicode escapes and astral characters (emoji) round-trip verbatim — no normalization applied | sha256:59910e9010a014d9723172c03d890d7527fb7797110d8efa1772a5657437a5e6 |
| utf16-sort-order | Keys sort by UTF-16 code units: 'Z'(0x5A) < 'za'(0x7A) < 'zé'(0x7A,0xE9) < 'Éclair'(0xC9) | sha256:87fb08703bfcfc2249c4aa2ed144c53b900de74f7bedb432ea690e0b5388f3fa |
compromised-principal
The Compromised Principal Test as a hash vector: an infrastructure agent's deployment.scale intent, and eight single-field mutations a valid principal could make after authorization. Every mutation MUST hash differently from the base and from every other, so that a grant for the base authorises none of them.
- The authorized binding
sha256:c1124c3c6fa4ace58edb2e053017d4d0413df0816c254d3ef3902a8104480826 action.parameters.replicasthe replica count, raised after authorization: the mutation the demo is named forsha256:75cc1a9ae4444e54c565fc3e7f93007154c94329d7d4618882b6d72545285b27action.parameters.servicethe service: a deployment outside the principal's remitsha256:5b00674723b6a6235f690340ad05a160dd1f166792998496f15324743baa4127action.parameters.clusterthe cluster: another team's, with the resource left as it wassha256:8022f9b0ff144e469a3245732d15710d6c7c2305c4a7f89fc7926fdc209e79f0action.resourcethe resource the action names, with the parameters left as they weresha256:e137482d371ae3cbb5ab7796a639872ea8a17ca83d85428e5a851132bc9616c6actor.idthe principal: the same request from a stronger identity is another intent, not a stronger claim to this onesha256:295c59bd21addea4e25f603bf8fd2625ff2c64ffdf7340d566703aae5fbe0e44expires_atthe expiry: an hour more is another intentsha256:be36e7144f85751721c554084dd12b524703eb60b3cef89c56722853d290f8f3context.idempotency_keythe idempotency key the trusted runtime chose: a retry is not a re-authorizationsha256:e43d6e5fcb2e4cfb73cf6c0490fab02a76b6989eafb17d8ea435114faed36cf7downstream_target.environmentthe environment the runtime binds: production and staging are different intents with the same parameterssha256:6a73fb0f7071c6d104e58b9283c9fbaae9a4d6f4c41299eaa17c11f99519d015
9 hashes, all different: a grant for the authorized binding covers none of the mutations.
policy-projection
A binding that states its policy projection: a Walmart order acknowledgement the authority decides as an ORDER_ACCEPTANCE, not under its action type. The projection is inside the hash, so a grant decided on one policy path authorises no other. Every mutation MUST hash differently from the base and from every other; the last, the projection removed, is exactly policy-projection-absent.json.
- The authorized binding
sha256:adda2a7a9483e4b357579ee06f415e2bb73e3c35492084ea7f2adee1f0568706 policy_projection.decision_typethe decision type: the release of a held order, which policy sends to a person to decidesha256:9da2d7ad7e97299f2407bf7e98dfd463dd755c0fe6f62f8f89723f9be7c9608dpolicy_projection.transaction_typethe transaction type left out of the evaluation, where the base names onesha256:eba00ff4e76705a832807ec25574789681af41d3769ebb9158e499618a716068policy_projection.workflow_keythe workflow key: a broader workflow than the one the grant was decided undersha256:bfbfe472c032e066d20d8413b9f8a33805da5359da893fa01b0bc99a6fb2f44apolicy_projection.channelthe channel left out of the evaluation, where the base names onesha256:e8e82e73a4587e6292ca396d991e92d526b59e7ba2ea569d1e87214a74dc9729policy_projectionthe projection removed: the authority would evaluate the action type insteadsha256:bbd2b45a06920fe1a0caf82a51bd2d70394114ed8f687bec1277784537e21717
6 hashes, all different: a grant for the authorized binding covers none of the mutations.
target-precondition
A binding that states the target's state as the runtime read it before asking: a Business Central customer about to be blocked, read at its @odata.etag with blocked empty. The precondition is inside the hash, under the reserved context key agent_safe, so a grant decided against one version of the target authorises a write against no other. Every mutation MUST hash differently from the base and from every other; the last, the precondition removed, is the same intent captured with none, which carries no agent_safe key at all.
- The authorized binding
sha256:b0284a98f9a0dfebbc6dfb6db6cdf1d1744f6994e24dd61b568ffec68dbce67e context.agent_safe.precondition.versionanother version of the customer: someone else wrote to the record after it was readsha256:e1da0941b50bae23ebdbc436752a6630503de6304e1fdca153718196989c249acontext.agent_safe.precondition.stateanother state read: the customer was already blocked for invoicing, a different transitionsha256:876f6744d8b9915ba3e8833452d5c8c07365478ca8238799e76064d7b31a11cdcontext.agent_safe.precondition.read_atanother read of the same version: when it was read is inside the hash toosha256:53e2547110aecfeda0ea2f7734083613d89b20b04e51f67d62316582d9b7d628context.agent_safethe precondition removed: the intent an executor that binds no target state capturessha256:d0856e043f28eda20dda0a8fd985cf4f0658fabcb4f440c2ff7c9791f6609fdf
5 hashes, all different: a grant for the authorized binding covers none of the mutations.
Beyond the binding
The other suites in the repository
Verifying providers
Vectors for a downstream service that checks, on its own side, that a request carries a valid execution grant for exactly that request before it acts.
Runtime
The same governed requests run against each way AgentSafe is deployed, to show that every packaging enforces the boundary the same way.
