Webhook icon
Schedule icon
WorkingDirectory icon
Script icon
Process icon
If icon
Create icon
SlackIncomingWebhook icon
Loop icon
Log icon

Audit Every Agent Tool Call Against Policy Before It Goes Unnoticed

Judge every recorded agent tool call against denylist, allowlist and call budget rules, escalate each violation to GitHub and Slack, and publish the verdict.

Categories
Infrastructure

An agent that can call tools can also call the wrong one, and the summary it writes at the end of a run is not evidence of what it actually did. This blueprint audits the record instead of the report. The agent platform POSTs to a keyed webhook when a run ends, a disabled daily schedule replays the recorded run held by the flow as a fallback, and a python policy engine judges every tool call against denylist patterns, an allowlist of permitted tools and a per agent call budget. A run that breaches policy opens a GitHub issue, posts a Slack alert and appends each violation to an audit log, while a clean run is recorded as compliant, and every execution publishes its verdict.

How it works

  1. on_agent_run_end (io.kestra.plugin.core.trigger.Webhook, keyed with the AGENT_AUDIT_WEBHOOK_KEY secret) starts a run the moment the agent platform reports that a run finished, with run_id carried in the JSON body.
  2. daily_recorded_run_replay (io.kestra.plugin.core.trigger.Schedule, cron 0 6 * * *) ships with disabled: true and replays the recorded run every morning, acting as the safety net you enable after the webhook path is trusted.
  3. tool_call_workspace (io.kestra.plugin.core.flow.WorkingDirectory) gives both python tasks one directory, so the file written by the first task is the file the second task reads.
  4. write_run_fixture (io.kestra.plugin.scripts.python.Script on the Process runner) writes tool-calls.jsonl with twelve recorded calls from research-agent, ops-agent and support-agent using search_index, fetch_url, read_file, write_file, run_tests and exec_shell, including a link local metadata fetch, a recursive delete of a live path and a read of a key file under /etc/secrets/.
  5. audit_tool_calls (same type) parses the recording line by line and applies three rule sets. The denylist matches internal or link local URLs, rm -rf style deletes and secret material paths such as /etc/secrets/, /run/secrets/, .aws/credentials, private keys and .env; the allowlist rejects any tool name missing from allowed_tools; the budget flags every call an agent makes beyond max_calls. Unreadable lines become record:malformed violations, and the task emits verdict, violations, calls and agents through the outputs protocol.
  6. violation_gate (io.kestra.plugin.core.flow.If) branches on verdict == 'violation'.
  7. The escalation branch runs open_violation_issue (io.kestra.plugin.github.issues.Create) with a table of every violation, then alert_policy_breach (io.kestra.plugin.slack.notifications.SlackIncomingWebhook) with the counts and the breached rules, then append_audit_trail (io.kestra.plugin.core.flow.Loop) walks the violations while append_audit_line appends one JSON line per violation to audit-log.jsonl.
  8. The clean branch records log_compliant (io.kestra.plugin.core.log.Log) with the call and agent counts, so a quiet run still leaves proof the sweep happened.
  9. The errors handler pings Slack when the audit itself breaks, because a crashed audit must never read as a compliant run.

What you get

  • A policy verdict on what the agent did rather than what it reported, driven by denylist patterns, an allowlist of permitted tools and a per agent call budget in one file your reviewers can read.
  • The three breaches the shipped recording carries, a link local metadata fetch, a recursive delete and a secret file read, surfaced as a GitHub issue with a full violation table.
  • A Slack message naming the run, the counts and each breached rule while the run is still fresh enough to act on.
  • An append only audit-log.jsonl, one JSON line per violation, that a downstream compliance query can read without parsing chat.
  • verdict, violations, calls and agents as typed flow outputs for a merge gate, a dashboard or a weekly report.

Who it's for

  • Platform and operations teams that run tool using agents in production and need evidence of every call they made.
  • Security engineers who want the same denylist and allowlist discipline on agent tooling that they already apply to egress and infrastructure.
  • Compliance owners who must show an auditor a durable record of agent actions instead of a chat transcript.

Why orchestrate this with Kestra

Policy checks written as a library call inside the agent runner disappear the moment the agent misbehaves, and the log they write is the log the agent controls. Kestra moves the judgement out of the run itself into a versioned flow that takes the record, applies the rules, branches on the verdict, and routes the result to GitHub, Slack and an append only file, all visible in execution history. The disabled schedule is a second pair of eyes you can switch on, the typed outputs let another flow gate on the verdict, and the errors handler keeps a broken audit from looking like a clean one.

Prerequisites

  • Python 3 on the Kestra worker for the Process runner, or switch the script tasks to a Docker task runner.
  • A GitHub personal access token able to open issues in the repository you point github_repository at.
  • A Slack incoming webhook for the breach alert and for failure alerts.
  • The agent platform POSTing a JSON body of run_id to the webhook URL when a run ends.

Secrets

  • AGENT_AUDIT_WEBHOOK_KEY: key segment guarding the webhook URL, anyone holding it can close out an audited run.
  • GITHUB_TOKEN: token used by open_violation_issue to file the tracking issue.
  • SLACK_WEBHOOK_URL: Slack incoming webhook used for the breach alert and for failure alerts.

Quick start

  1. Add the three secrets to your namespace, import the flow and set github_repository to the repo that should receive violation issues.
  2. Trigger the webhook with a body like {"run_id": "run-4821"} and confirm the execution reports verdict: violation, three violations, twelve calls and three agents.
  3. Open the GitHub issue, read the Slack message and inspect the appended audit-log.jsonl, then confirm the loop wrote one line per violation.
  4. Replay a recording with no breaches and confirm the run ends on the compliant log line with verdict: compliant.
  5. Enable daily_recorded_run_replay once the webhook path is trusted, and raise or lower max_calls to match how chatty your agents are.

How to extend

  • Add denylist entries for the address ranges and paths that matter in your estate by appending a (rule, pattern) tuple to DENYLIST.
  • Move the tool allowlist into the webhook body so each team declares the tools its agents may call.
  • Send each audit line somewhere durable by swapping append_audit_line for a jdbc.postgresql.Queries insert or an aws.s3.Upload.
  • Fail the run outright with io.kestra.plugin.core.execution.Fail under the gate when a violation must stop the pipeline instead of opening a ticket.
  • Audit live recordings by replacing write_run_fixture with an http.Request that pulls the run export from your agent platform.
  • Page an on-call rotation for the critical rules by adding a second Slack message keyed on the rule name.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.