When an AI agent starts calling real systems – deploy pipelines, billing, CRM, data workflows – “governance” stops being optional.
Over the last year, a few practical patterns have emerged for teams that got agents into production without terrifying security and infra.
Pattern 1: Treat capabilities as a catalog, not just functions
Instead of wiring agents to ad‑hoc endpoints, define a capability catalog:
- stable capability IDs
- machine‑readable schemas for inputs and outputs
- metadata (owner, risk, environments)
The same capabilities are callable by humans (CLI), services (HTTP/RPC), and agents. That shared catalog becomes your map of what the organization can actually do.
Pattern 2: Build a centralized enforcement pipeline
Sprinkling ACL checks and approvals across N services doesn’t scale.
Introduce a runtime layer that every capability invocation passes through. It should:
- resolve identity and caller context
- enforce ACLs
- evaluate approval conditions
- run middleware for logging / metrics / policies
Downstream services implement business logic; the runtime decides whether the call is allowed and how it should be wrapped.
Pattern 3: Keep protocols as adapters on the edge
MCP, HTTP, CLIs, internal RPC – all are just ways to reach the same capabilities.
If each protocol carries its own governance semantics, you’ll get drift. Instead, let each protocol adapter:
- map protocol requests into capability calls
- inject identity and context
- route through the same enforcement pipeline
That way, adopting a new tool protocol is mostly an adapter exercise, not a rewrite of core rules.
Pattern 4: Evidence as a first‑class output
When something breaks, teams want more than logs. They want evidence.
Your runtime can emit:
- structured “invocation” events (who, what, where)
- consistent error shapes with trace IDs
- hooks for usage / cost tracking
Because this happens in one place, you don’t depend on every service getting logging exactly right.
Pattern 5: Offload determinism to an orchestrator
Agents are planners; they’re not great at guaranteeing that a fan‑out of 1,000 jobs completes exactly once.
Pair agents with a workflow engine that handles determinism:
- retries and idempotency
- timeouts and cancellation
- compensation logic
The agent assembles a plan in terms of governed capabilities; the orchestrator executes that plan reliably.
Pattern 6: Iterate on one end‑to‑end path
A realistic path for many teams:
- pick a non‑trivial, high‑value capability (e.g., canary deploy)
- give it a schema, policies, and full tracing
- expose it to a human interface and an agent
- run it in production with tight monitoring
Use the friction you hit there to refine your runtime, then scale out to more capabilities.
These patterns don’t depend on a specific framework. They do depend on drawing a clear line between:
- capabilities (what can be done)
- the governed runtime (who can do it, under what rules)
- protocols and agents (how it’s requested)
Once that separation exists, agent governance becomes a matter of evolving policy and runtime behavior – not rewriting every integration when a new agent pattern appears.
Top comments (4)
One useful acceptance test for the shared runtime is to invoke the same denied action through every adapter and assert that the downstream client was never called. A denial event alone does not prove the side effect stopped. I would include evaluation failures in that test matrix too, so a missing bundle or unavailable approval source cannot silently turn into permission.
@hlinor's test matrix is the right first move, and the evaluation-failure row is the one I would write first, because a missing bundle turning into permission is the failure most likely to be in somebody's build already. What a matrix cannot reach is the claim pattern 4 is making, and that is worth separating from pattern 2.
Every capability invocation passes through the runtime layer. That word every is doing all the work and nothing in the design proves it. A service reachable directly, by an old client, a retried job, a debug script, a second protocol adapter somebody added last quarter, serves a call the runtime never saw. The runtime emits no invocation event for it, which is correct behaviour and is also the problem: no evidence and no activity produce the same output. You cannot tell a quiet week from a bypassed pipeline by reading the pipeline.
A denial test covers the adapters you enumerated. A bypass is a path you did not enumerate, so the adapter somebody adds next quarter is not in the matrix on the day it ships, and the person adding it is not thinking about the denial test. The matrix and the counting below are not the same control: one proves the known adapters obey, the other finds the unknown one.
Because this happens in one place is the line I would change. It is what makes the evidence cheap, and it is why the evidence cannot detect the failure it most needs to.
The fix sits at the other end and it is cheap. Each downstream service counts the calls it served, by capability, and that count gets reconciled against the runtime's invocation count for the same window. Two numbers from two components that would have to be wrong together. A gap is a bypass, found without the bypassed path having to announce itself. This is ordinary reconciliation and it is boring, which is the recommendation. It does not need every service to get logging right, which was your reason for centralising in the first place. It needs them to be able to count.
On pattern 5, exactly once is worth being careful with. A workflow engine gives you exactly once inside its own durable state. Across a trust boundary you get at least once plus idempotency at the receiver, and the key space has to belong to the receiver rather than the orchestrator. If the orchestrator's record and the downstream's record disagree about whether a job ran, the orchestrator is not the authority on that, whatever its durability guarantees say. In payments that gap is the whole reason reconciliation exists as a job function.
The capability catalogue with owner and risk as metadata is the part I would steal outright. Risk as a field on the capability, set once, rather than a judgement each caller makes, is what lets approval conditions be a property of the thing instead of a rule in a file somewhere.
Agreed on the distinction: my matrix tests known paths; it cannot establish that those are the only paths. A quiet runtime is not evidence of a quiet downstream service.
Receiver-side reconciliation is a useful second control. I would be careful with "a gap is a bypass", though. First define what both sides count: allowed dispatches, received attempts, or committed effects. Retries, rejected requests, and reporting delays can produce a gap without a bypass. Equal totals can also hide different calls. Counts are a good alarm; receiver records linked to request IDs and idempotency keys make the discrepancy easier to investigate.
That still detects after the fact. Preventing bypass needs a separate boundary at the service: credentials, network access, or receiver-side authorization that direct callers cannot skip. I'd keep adapter refusal tests, independent reconciliation, and access restrictions as separate controls with separate evidence.
I agree on exactly-once too: durable orchestration alone does not prove that an external effect happened once. The receiver's commit and deduplication records matter, especially when a timeout leaves the outcome uncertain.
And yes, owner/risk metadata makes the catalogue useful for review. I'd keep baseline risk there while allowing the actual request's amount, target, and environment to raise the approval requirement.
Do you reconcile raw attempts or committed effects in practice?
Bài viết chạm đúng vào nỗi đau thực tế khi cho agent "tự do" trên production. Mình thấy ba pattern còn thiếu thốn trong hầu hết triển khai:
Idempotency key bắt buộc cho mọi action mutating — agent gọi API billing/deploy hai lần do retry hoặc hallucination thì hệ thống downstream phải tự deduplicate, không thể phụ thuộc vào agent "nhớ" đã gọi rồi.
Policy-as-code cho scope quyền — thay vì hardcode allowlist trong prompt, dùng OPA/Cedar định nghĩa
agent:deploy:canaryvsagent:deploy:prodriêng biệt. Khi agent request action, policy engine evaluate context (environment, time, user approval status) rồi trả về allow/deny + reason. Dễ audit, dễ rollback policy mà không touch code agent.Compensating transaction log — mỗi action cần ghi event
action_id, agent_id, input, output, status, rollback_plan. Khi rollback, orchestrator đọc log ngược lại và execute compensating action (refund, scale-down, revert migration). Không có này thì incident response thành "mò mẫm".Thêm một điểm: human-in-the-loop không phải chỉ approve/deny. Cần pattern "propose → human review → agent execute với token one-time" để tránh agent tự execute sau khi human đã approve xong context đã đổi (ví dụ: canary metrics đã degrade PS: the tool I meant is on labagent .tech