Separation of duties is one of the oldest controls in compliance: the person or credential that operates a system day to day must not be the one that can, alone, change what the system is permitted to do. ISO 27001 and SOC 2 both ask for it, and any system a regulator looks at is expected to have it.

Many AI agent platforms quietly fold it away. There is one API, one admin surface, one set of credentials, and the plane that runs the agent against live customer data is the plane where you edit the policy that governs the agent. Operating and configuring share a trust boundary. Teams meet the requirement on top of that with process: role assignments in a single admin panel, periodic access reviews, a promise about who clicks what. Process is better than nothing, and it fails in the cases that matter: an over-broad token, a compromised operator session, a bug in the one surface that does both.

The single-plane model, fairly stated

A unified control surface is simpler to build and simpler to reason about, and for a lot of software it is the right call. The argument here is narrow. When the same plane both acts on data and defines the rules for acting on data, separation of duties depends on the discipline of the people using it. You can have it on Monday and lose it on Tuesday with one mis-scoped grant, and nothing in the architecture will tell you.

The inversion: two planes, separated where it counts

The structural alternative splits the tenant-facing surface into two planes that do not share a database role, a binary, or a route:

Two planes: the runtime plane executes intents under one database role; the contract plane holds contracts and role definitions under another, and the database refuses the runtime role any write to the contract tables.

runtime plane    {tenant}.zarel.ai         runtime-api    PG role: zarel_runtime    token_class: tenant
                 records · chat · tools · flows · events · role assignments

contract plane   {tenant}.admin.zarel.ai   contract-api   PG role: zarel_contract   token_class: contract
                 contracts · entity catalog · role definitions · grants

The runtime plane operates under the rules: it runs the agent and reads and writes operational data. The contract plane defines the rules: what entities exist, what roles mean, what the agent may do. They are different binaries, reached on different hosts and authenticated by different token classes, and underneath they connect to the database as different roles with different grants. The runtime role can read every contract table, because it enforces what they say, and holds no grant to write any of them; no runtime route would let it try. A token minted for the runtime plane is rejected by the contract plane on its class and audience, and the reverse. Cross-tenant isolation is a separate axis, also enforced, by Postgres Row-Level Security: RLS separates tenants, and the plane split separates functions. The runtime plane does decide who holds a role (only when the contract grants that action, and never for an owner role), but what a role may do is decided on the contract plane alone.

So neither a runtime token nor the runtime's database role can rewrite the agent's rules: they hold neither the grant nor the route.

Why this moves the trust boundary

The other pieces in this series make the same move at other seams: the agent proposes and the runtime executes, so a compromised reasoner cannot take an action its role was not granted; the state-machine and flow events are hash-chained with signed checkpoints, so an auditor can check that log instead of trusting it. Here the seam is between operating and configuring. Because they are different planes, a leaked token or an over-broad grant on the operational surface cannot reach the control surface, and a property that the common design leaves to process moves into the architecture.

The honest limits

Both planes share one database. They run over one PostgreSQL instance. The separation is enforced by distinct database roles with least-privilege grants, distinct binaries, and distinct hosts and token classes; there are no separate datastores, so "physically separate systems" would overstate it. Separate databases would complicate RLS and operations, and what separation of duties asks is who can write what, which the grants already decide.

One key signs both token classes. The runtime service reads the per-tenant key that signs runtime and contract tokens alike, because it verifies its own. A stolen runtime token cannot cross planes; a fully compromised runtime service could mint itself a contract token. What it still cannot do is write a contract table with its own database role.

It does not separate one operating step from another. Two runtime steps, each authorized on its own, can still add up to a dual-control breach. Venkat Peri, who works on agentic AI infrastructure for wealth management at Advisor360°, describes it in The Agentic Auth Problem Nobody Is Actually Solving: a violation that "only becomes visible when you look at the arc of the execution rather than any single moment in it." No split between planes reaches that. It has to be a rule the flow itself carries.

There are more planes than two. Operator and tenant-lifecycle actions run on a separate platform surface, and channel secrets sit in their own custody service; both are reachable only from the internal network. The two in this article are the tenant-facing pair.

No certification yet. ISO 27001 and SOC 2 ask for segregation of duties, and two planes is how this architecture is designed to answer that. Zarel holds no certification today; it is designed toward them.

The guarantee lives in the grants and routes. It is enforced by configuration that ships with the deployment, and it can be verified by inspecting that configuration. A deployment that mis-grants the runtime role would weaken it, which is why the configuration has to be open to inspection.

The stakes

The separation-of-duties question in a real audit is blunt: show me that the credentials operating this system cannot, on their own, change what it is permitted to do. A single-plane platform answers with an access-control matrix and a process for reviewing it. A two-plane deployment answers by showing the two roles: here are the grants the runtime credential has, and none of them writes a contract table; here is the route that would change policy, on a host that credential cannot authenticate to.

Checking that does not require our code, because the grants live in the database's own catalog. On a Zarel deployment, has_table_privilege('zarel_runtime', 'contract.roles', 'INSERT') is false, like every other write on a contract table, and a runtime token sent to the admin host is refused on its class before its signature is checked. To see it on a running deployment, ask us.

This is the separation-of-duties case of a broader line: communication and proposal may be probabilistic, but authority, consequence, the record, and the right to change the rules must be deterministic, declared, and structurally separated. The general argument is in You can't fight probabilism with probabilism; the enforcement case is in The agent proposes; the runtime presses the button; the proof layer is in Audit you don't have to trust.


Nicolás Moreno builds Zarel: governed AI operations, where the AI proposes and the contract decides.