An agent can pass every per-call policy check and still do something no reviewer would sign off on. Each call in read customer PII -> summarize -> send to an external webhook can be individually permitted, and a hundred payments of 45 each can each clear a 500 approval threshold. KLA addresses this class with run-level checkpoints: the execution worker accumulates safe, registry-declared facts about the tool calls a run has completed, and a workflow policy gate evaluates deterministic rules over those accumulated facts before the run proceeds. A matched rule resolves to the same four outcomes as every other KLA policy decision (allow, warn, require_approval, or block) and persists the same decision, incident, and evidence records. This deep dive covers what the run facts contain, the three shipped rule templates, where the checkpoint sits, and the audit record a detection produces.
This article extends the per-call layer described in MCP audit: secure and govern every tool call and the containment layer in the AI agent kill-switch architecture. All example chains, tool names, and thresholds are synthetic.
The gap: individually permitted calls that compose into misuse
Per-call policy answers one question well: may this principal run this tool with these arguments here and now. A per-call allowlist has no memory, so it cannot answer whether this call is dangerous given what the run already did. Three synthetic patterns show the gap.
A support agent may read a customer record, may summarize text, and may call a notification webhook. Each capability has a legitimate use. The combination inside one run moves regulated data to an external destination. A payments agent may submit payments under a 500 approval threshold; sixty such calls in one run move 25,000 without a single human decision. A retrieval agent allowed to list customers and an export tool allowed to write files are each unremarkable; enumeration followed by a bulk export is a textbook exfiltration shape.
The failure mode is composition. Every gate saw a permitted call, and the dangerous object (the sequence, the aggregate, the cross-tool data flow) never existed as a single policy input. Run-level detection makes that object evaluable.
| Chain | Per-call verdicts | Run-level property that matters |
|---|---|---|
| Read PII record, summarize, send to external webhook | allow, allow, allow | Secret or sensitive retrieval co-occurs with an external send in one run |
| Sixty payments of 45 each under a 500 threshold | allow x 60 | Call count and aggregate amount cross a bound while every call stays under the per-call cap |
| List customers, then export 8,000 records | allow, allow | Enumeration co-occurs with an export whose declared record volume crosses a threshold |
| Read connector credential, then register a new outbound connector | allow, allow | Capability acquired through one tool is spent through another in the same run |
What a run checkpoint actually sees
KLA policies evaluate JSON Logic expressions over a typed GateContext. Per-call rules read context.action: the tool name, arguments, and destination of one proposed call. Run checkpoint rules read context.run.tools: an accumulator the execution worker maintains across a run, keyed by a safe per-tool fact key.
Each tool entry carries four facts. calls counts completed operations, with retried operation keys deduplicated so a Temporal retry cannot inflate the count. argValues holds deduplicated, capped, registry-declared identifier-shaped values. numericSums holds the sums of registry-declared finite numeric inputs. numericMaximums holds the per-field maximum across the run, an unordered aggregate that keeps a repeated-low-value rule from matching a small number of high-value calls.
The projection boundary is deliberately narrow. A registry tool opts in through metadata.run_fact_arg_keys, metadata.run_fact_numeric_keys, and an optional metadata.run_fact_key; each declared field list accepts at most 32 keys. Projection runs beside the tool executor, and the observation that crosses into the Temporal workflow contains the tenant ID, run ID, encoded tool key, an opaque operation digest, and the declared values. Raw tool input, free text, numeric strings, and event order never cross that boundary, so run facts cannot become a shadow copy of sensitive payloads.
{
"tools": {
"secret_read": {
"calls": 1,
"argValues": {},
"numericSums": {},
"numericMaximums": {}
},
"external_send": {
"calls": 1,
"argValues": {},
"numericSums": {},
"numericMaximums": {}
},
"payment_submit": {
"calls": 12,
"argValues": {
"beneficiary_id": [
"bene-2201",
"bene-2207",
"bene-2213"
]
},
"numericSums": {
"amount": 4620
},
"numericMaximums": {
"amount": 480
}
}
}
}- Ownership: the Temporal workflow owns the accumulator and rejects observations from another tenant or run before aggregation.
- Bounds: distinct operation identities and retained identifier values are capped, so a chatty tool cannot inflate workflow state.
- Approval binding: a tool whose output is gated contributes its fact only after its own output gate resolves to approve. A blocked call, a safe-display rejection, or an approval for a different gate admits nothing.
- Occurrence facts: a tool with an explicit run-fact declaration records a
callsentry even when it declares no identifier or numeric field, so a policy can require that screening actually ran.
Three deterministic rule templates, tenant-configured
buildDangerousToolChainRules in @kla/shared generates three deterministic rules for a tenant policy version. The tenant supplies registry tool names, the numeric field names, the thresholds, and the outcome each rule should resolve: warn, require_approval, or block, with an approver group where approval applies.
The first template matches a run in which a configured secret-retrieval tool and a configured external-send tool both completed before the checkpoint. The second matches customer enumeration co-occurring with an export whose declared record count reaches the configured volume. The third matches repeated low-value actions: the call count and aggregate amount reach their thresholds while the per-call maximum stays at or below the configured bound, which is the structural signature of threshold-splitting.
The generated rules are ordinary policy rules. They evaluate in the tenant’s normal policy version through the same engine as every per-call rule, in deterministic mode, with stable rule IDs and reason codes. The policy below embeds the exact output of the helper for a synthetic template; the sibling test in this repository regenerates it from @kla/shared and proves the two match and that the document validates against the published PolicyVersion schema.
{
"schemaVersion": "1.0.0",
"policyId": "pol_run_tool_chain_checkpoints",
"workspaceId": "workspace-regulated-operations",
"name": "Run tool-chain checkpoints",
"description": "Evaluates accumulated run facts at workflow checkpoints for dangerous tool-call co-occurrences and aggregates.",
"status": "draft",
"version": "1.0.0",
"policyKind": "guardrail",
"scope": {
"workflowIds": [],
"agentIds": [],
"stepIds": [],
"environments": [
"prod"
]
},
"defaultDecision": "allow",
"rules": [
{
"ruleId": "run-tool-chain-secret-then-external",
"name": "Secret retrieval and external send co-occur",
"description": "Detects configured secret retrieval and external-send calls that co-occur before a checkpoint. Version 1 does not establish call order.",
"when": {
"expression": {
"and": [
{
">": [
{
"var": "context.run.tools.secret_read.calls"
},
0
]
},
{
">": [
{
"var": "context.run.tools.external_send.calls"
},
0
]
}
]
}
},
"then": {
"decision": "block",
"reason": "The run retrieved a secret and reached an external destination before this checkpoint.",
"reasonCodes": [
"run_chain_secret_then_external"
]
},
"execution": {
"mode": "deterministic"
},
"evidence": {
"evaluatedFields": [
"context.run.tools.secret_read.calls",
"context.run.tools.external_send.calls"
],
"artifactRefs": []
}
},
{
"ruleId": "run-tool-chain-enumeration-then-export",
"name": "Customer enumeration and large export co-occur",
"description": "Detects configured customer enumeration and large export facts that co-occur before a checkpoint. Version 1 does not establish call order.",
"when": {
"expression": {
"and": [
{
">": [
{
"var": "context.run.tools.customer_enumeration.calls"
},
0
]
},
{
">=": [
{
"var": "context.run.tools.customer_export.numericSums.record_count"
},
500
]
}
]
}
},
"then": {
"decision": "require_approval",
"reason": "The run enumerated customers and exported records past the configured volume.",
"reasonCodes": [
"run_chain_enumeration_then_export"
],
"approverGroup": "security_reviewers"
},
"execution": {
"mode": "deterministic"
},
"evidence": {
"evaluatedFields": [
"context.run.tools.customer_enumeration.calls",
"context.run.tools.customer_export.numericSums.record_count"
],
"artifactRefs": []
}
},
{
"ruleId": "run-tool-chain-repeated-low-value-actions",
"name": "Repeated low-value actions exceed aggregate threshold",
"description": "Detects configured repeated actions whose every declared amount is at or below the configured per-call bound and whose aggregate crosses the configured threshold.",
"when": {
"expression": {
"and": [
{
">=": [
{
"var": "context.run.tools.payment_submit.calls"
},
10
]
},
{
">=": [
{
"var": "context.run.tools.payment_submit.numericSums.amount"
},
4000
]
},
{
"<=": [
{
"var": "context.run.tools.payment_submit.numericMaximums.amount"
},
500
]
}
]
}
},
"then": {
"decision": "require_approval",
"reason": "Repeated payments below the per-call bound crossed the aggregate threshold.",
"reasonCodes": [
"run_chain_repeated_low_value"
],
"approverGroup": "payments_reviewers"
},
"execution": {
"mode": "deterministic"
},
"evidence": {
"evaluatedFields": [
"context.run.tools.payment_submit.calls",
"context.run.tools.payment_submit.numericSums.amount",
"context.run.tools.payment_submit.numericMaximums.amount"
],
"artifactRefs": []
}
}
]
}Where the checkpoint sits and what happens on detection
KLA policy rules attach to interception points along the governed execution path: input, tool_call, tool_result, step_output, and workflow policy gates. Per-call enforcement stays at tool_call, before each side effect, exactly as the MCP audit guide describes. Run facts are supplied only to workflow policy_gate steps, so a chain rule leaves interceptionPoint unset and fires at the checkpoints the workflow author places after agent steps. A checkpoint sees the facts of calls completed before that gate, and its decision resolves before any later step executes; the calls that already ran keep their own per-call decision records.
Placement is a design decision with the same character as placing a database constraint. A checkpoint after each agent step bounds how much a run can compose between evaluations. A single checkpoint before the consequential final step (the send, the export, the batch release) concentrates review where the irreversible effect happens. Both placements use the same rules and produce the same records.
On a match, the decision follows standard KLA semantics. block terminates the operation, and an unreachable policy engine resolves fail-closed to the same terminal state. require_approval pauses the workflow and routes a Decision Request to the configured approver group, and the run resumes only on an authorized approve. warn lets the run proceed and creates a reviewable signal. Incident triggers fire through the existing policy-decision path, which is also where run-scoped containment such as the kill switch attaches when a detection warrants suspending the agent beyond the current run.
| Layer | Evaluates | Catches | Misses |
|---|---|---|---|
Per-call gate (tool_call) | One proposed call: tool, arguments, destination, principal | Unauthorized tools, bad arguments, wrong destination | Anything visible only across calls |
Output gate (tool_result / step_output) | One produced output before release | Policy-violating content leaving a step | Aggregate effect of many small outputs |
Run checkpoint (policy_gate over context.run) | Accumulated counts, identifiers, sums, and maxima for the whole run so far | Dangerous co-occurrence, threshold-splitting, enumeration-plus-export volume | Call order, cross-run patterns, statistical anomalies |
| Containment (kill switch) | Operator or triggered verdict on the agent itself | A compromised or drifting agent across runs | Requires a detection or operator signal to invoke |
The evidence a detection produces
A checkpoint detection creates no bespoke record type. The checkpoint evaluates through the same policy-gate activity as every other gate, so the persisted decision carries the policy ID and version, the matched rule ID, the resolved decision, the reason and reason codes, the determinism mode, and the evaluated fields, which for these rules are the run-fact paths themselves, such as context.run.tools.payment_submit.numericSums.amount. The decision lands on the existing audit, incident-trigger, and evidence path.
That reuse matters for review. In the Audit Trail, a blocked chain reads like any other blocked action: an actor, a gate, a policy version, a rule, a reason code such as run_chain_repeated_low_value, and a terminal outcome, joined to the run by the execution identifier. An auditor who can already verify a per-call decision can verify a chain decision with the same procedure, and the numbers the rule evaluated (twelve calls, an aggregate of 4,620, a per-call maximum of 480) are present in the decision context without any raw payment payload beside them.
When the outcome is require_approval, the Decision Request shows the reviewer the matched rule, the reason, and the aggregate facts that crossed the threshold, and the approval or rejection binds to that request through the standard flow. The sealed evidence export then contains the full arc: the per-call allows that admitted each fact, the checkpoint decision that caught the composition, the human decision where one was required, and the terminal state of the run.
Mapping the examples to OWASP agentic threat categories
The OWASP Top 10 for Agentic Applications, Version 2026 names the threat classes these controls address. The runtime controls and evidence guide maps all ten categories to KLA controls, and the EU AI Act crosswalk maps them to regulatory articles. The table below places only the run-level detections in that frame.
| Synthetic chain | OWASP category | Run-level control |
|---|---|---|
| Secret retrieval co-occurring with an external send | ASI02 Tool Misuse and Exploitation | Co-occurrence rule blocks the run at the checkpoint before later steps execute |
| Enumeration plus bulk export volume | ASI02 Tool Misuse and Exploitation; ASI09 Human-Agent Trust Exploitation | Numeric-sum threshold routes a Decision Request with the aggregate volume in view |
| Threshold-splitting payments | ASI01 Agent Goal Hijack; ASI09 Human-Agent Trust Exploitation | Count, aggregate, and per-call-maximum rule restores the human approval the split evaded |
| Capability acquired in one tool, spent in another | ASI03 Identity and Privilege Abuse | Co-occurrence rule over the acquiring and spending tools; containment escalates to the kill switch |
| Drift toward any of the above across a run | ASI10 Rogue Agents | Checkpoint decisions feed incident triggers, the standard path to run and agent suspension |
Boundaries of version 1
The shipped implementation states its limits, and a security engineer should design around them. Rules evaluate declared counts, identifiers, and numeric aggregates; version 1 does not retain or infer call order, so a co-occurrence rule fires whether the secret read happened before or after the external send. For an exfiltration control that asymmetry is acceptable because both orders deserve review. Rules do not inspect raw payloads, model reasoning, or statistical anomaly scores.
Facts live in the current Temporal workflow state. There is no ordered durable action-event store, no predecessor inference, and no correlation across independent runs, so a chain split across two runs evades a single-run checkpoint. A checkpoint sees only calls completed before that gate; a consequential call placed after the last checkpoint of a run is governed by its per-call and output gates alone. Route placement stays with the workflow author.
Production adoption requires three tenant-controlled steps: registry declarations for the real governed tools, a published tenant policy version carrying the generated rules, and a workflow policy_gate placed after the relevant agent step. The code path and its deterministic fixtures ship in the platform; the thresholds and outcomes are governance decisions each tenant makes for its own risk appetite.
Frequently Asked Questions
Why do per-call allowlists miss dangerous tool chains?
A per-call gate evaluates one proposed action with no memory of the run. Dangerous chains are made of individually permitted calls, so the object that matters (the combination, the aggregate amount, the export volume) never appears as an input to any single per-call decision.
Does KLA detect the order of tool calls in a run?
Version 1 evaluates unordered facts: call counts, declared identifier values, numeric sums, and per-call maxima. A co-occurrence rule matches regardless of which call came first, and the rule descriptions state this. Ordered action-event history is outside the shipped scope.
What data enters the run-fact accumulator?
Only values the tool registry declares: identifier-shaped strings under declared argument keys and finite numbers under declared numeric keys, plus a per-tool call count. Raw tool input, free text, numeric strings, and event order are excluded at the projection boundary before the observation reaches the workflow.
What happens when a chain rule matches?
The checkpoint resolves the tenant-configured outcome through standard policy semantics. block terminates the run, require_approval pauses it and routes a Decision Request to the configured approver group, and warn records a reviewable signal. Incident triggers and evidence records follow the existing policy-decision path.
How does a detection appear in the audit trail?
As a standard policy decision joined to the run: policy ID and version, matched rule ID, decision, reason codes, and the evaluated run-fact fields such as call counts and numeric sums. Sealed evidence exports include the per-call decisions that admitted each fact and the checkpoint decision that caught the composition.
Can an agent evade detection by splitting a chain across runs?
Yes, within version 1. Facts are scoped to one run, and independent runs are not correlated. Compensating controls include per-call limits, output gates on the consequential tools, checkpoint placement before irreversible steps, and agent-level containment through the kill switch.
Key Takeaways
Run-level detection closes the gap between per-call authorization and the behavior a reviewer actually worries about: composition. The shipped mechanism is small and auditable: registry-declared facts, three deterministic rule templates, a checkpoint on the existing policy gate, and the standard decision and evidence records. Layer it with the per-call controls in the MCP audit guide, the category-by-category program in the OWASP runtime controls guide, and the containment design in the kill-switch architecture. Test your current evidence against this class with the Agent Audit Readiness Assessment.
