TechnicalAugust 13, 202614 min read

Engineering deep dive: detecting dangerous tool chains and run-level agent misuse

How KLA evaluates accumulated run facts at workflow checkpoints to catch dangerous tool-call combinations that per-call policy cannot see.

Antonella Serine

Antonella Serine

Founder, KLA

Founder of KLA, building the independent runtime governance control plane for regulated AI agents under the EU AI Act.

Detection unit

The run. Deterministic checkpoint rules evaluate accumulated tool facts: call counts, declared identifiers, numeric sums, and per-call maxima.

Checkpoint

A workflow policy_gate after an agent step. It resolves allow, warn, require_approval, or block before the run proceeds.

Fact boundary

Only registry-declared identifier values and finite numbers enter run facts. Raw tool input, free text, and call order stay out.

Evidence

A checkpoint detection persists the standard policy decision with rule ID, reason codes, and the evaluated run-fact fields, on the existing audit and incident path.

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.

Synthetic chains where every individual call passes per-call policy
ChainPer-call verdictsRun-level property that matters
Read PII record, summarize, send to external webhookallow, allow, allowSecret or sensitive retrieval co-occurs with an external send in one run
Sixty payments of 45 each under a 500 thresholdallow x 60Call count and aggregate amount cross a bound while every call stays under the per-call cap
List customers, then export 8,000 recordsallow, allowEnumeration co-occurs with an export whose declared record volume crosses a threshold
Read connector credential, then register a new outbound connectorallow, allowCapability 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.

Accumulated run facts as the checkpoint receives them (context.run)
{
  "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 calls entry 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.

Schema-valid checkpoint policy generated by buildDangerousToolChainRules
{
  "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.

Layered enforcement across the run
LayerEvaluatesCatchesMisses
Per-call gate (tool_call)One proposed call: tool, arguments, destination, principalUnauthorized tools, bad arguments, wrong destinationAnything visible only across calls
Output gate (tool_result / step_output)One produced output before releasePolicy-violating content leaving a stepAggregate effect of many small outputs
Run checkpoint (policy_gate over context.run)Accumulated counts, identifiers, sums, and maxima for the whole run so farDangerous co-occurrence, threshold-splitting, enumeration-plus-export volumeCall order, cross-run patterns, statistical anomalies
Containment (kill switch)Operator or triggered verdict on the agent itselfA compromised or drifting agent across runsRequires 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.

Run-level detections against OWASP ASI categories
Synthetic chainOWASP categoryRun-level control
Secret retrieval co-occurring with an external sendASI02 Tool Misuse and ExploitationCo-occurrence rule blocks the run at the checkpoint before later steps execute
Enumeration plus bulk export volumeASI02 Tool Misuse and Exploitation; ASI09 Human-Agent Trust ExploitationNumeric-sum threshold routes a Decision Request with the aggregate volume in view
Threshold-splitting paymentsASI01 Agent Goal Hijack; ASI09 Human-Agent Trust ExploitationCount, aggregate, and per-call-maximum rule restores the human approval the split evaded
Capability acquired in one tool, spent in anotherASI03 Identity and Privilege AbuseCo-occurrence rule over the acquiring and spending tools; containment escalates to the kill switch
Drift toward any of the above across a runASI10 Rogue AgentsCheckpoint 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.

See It In Action

Ready to automate your compliance evidence?

Book a 20-minute demo to see how KLA helps you prove human oversight and export audit-ready Annex IV documentation.

Engineering deep dive: detecting dangerous tool chains and run-level agent misuse | KLA Blog