The Governance Gap Is a Boundary Problem A developer building open-refinery, a control plane for AI-driven work, discovered that audit trails alone cannot prevent unwanted actions—governance must gate actions before they occur. The tool enforces policies at the boundary between the agent harness and the target, ensuring no action reaches its target without passing a gate that can refuse it. One of my early factory prototypes had a clean audit trail. Every action an agent took, recorded. Who did it, what it touched, and when it ran. I was proud of it. Then I watched an agent remove a status enum from a database column. Not a value I wanted changed. It was a status that other records depended on. The audit trail caught it. Logged the delete, the actor, the timestamp. Caught it perfectly, and caught it after the status was already gone. That’s the moment the “governance gap” stopped being a phrase I’d read in other people’s posts and became something I’d built by accident. I had governance you could read. I didn’t have governance that could say no. This is what I learned about that gap by building a tool to close it. The tool is open-refinery https://www.tacoda.dev/open-refinery/ , a control plane that governs how AI-driven work reaches repos, models, and tools. The lesson is simpler than the tool. The gap everyone’s naming isn’t a missing policy. The gap is that nothing makes an agent respect it. And a policy nothing enforces is a suggestion. People use “governance gap” to name the space between the policy you wrote and the agent doing the work. You have a policy. The agent runs. The policy and the run never meet, so nothing makes the agent respect it. The common fix is to add something. A policy document, an approval step, or a content filter. Each of these is a thing you write down. None of them is a thing that stops an action. Here’s the distinction that took me three prototypes to see. An audit log observes: it tells you what happened. Governance has to gate . It must stop the thing before it happens. My prototype with the clean audit trail was all observation. When the agent deleted that status, nothing stood between the intent and the delete. The log wrote the event down a moment too late to matter. Once you see governance as gating instead of recording, the question changes. It’s no longer “what policy should we write.” It’s “where does the policy get enforced before the action does.” That question has a location in it. Most governance talk doesn’t. A gate is a control that runs before an action and can refuse it. To run before the action, it has to sit between the thing that wants to act and the thing being acted on. It needs a seam. In most agent setups, nobody drew the seam. So the first real decision in open-refinery was to split two things that usually run together. The harness does the work . It’s the agents, the prompts, the memory, the tools, the orchestration. It lives in your application. It decides what to try. The platform governs how work reaches a target — the repos, the models, the tools the harness wants to touch. The platform does none of the work. open-refinery executes nothing. It sits on the seam and decides whether an action reaches its target. The harness never touches the target directly. Every action passes a gate that can say no, and the platform owns that gate. Draw that line and the gate has a home. The harness asks to do something. The platform checks the policy. The action reaches the target, or it doesn’t. Skip the line and your policy floats free of the work. That was the prototype I started with: the policy lived in one place, the actions happened in another, and nothing connected them. None of enforce-before-execute is new. RBAC, IAM, and Kubernetes admission controllers are all examples. What’s new is who’s on the other side of the gate. A service either has permission or it doesn’t. An agent reads your policy in prose and decides how to apply it. The boundary is what lets you stop trusting that interpretation: the policy gets enforced on the seam, not left to the agent to honor. The boundary is the whole idea. Everything else here is a consequence of it. I wanted the platform to run without a human watching. Lights-out. I also wanted every action fully auditable. Those two goals fight each other the moment the system makes a choice you can’t explain. You can’t audit “the model decided to.” You can audit a function that takes recorded inputs and returns a recorded output. So the core loops in open-refinery are plain code, not agent choices. The policy is encoded there, deterministically, where the agent can’t reinterpret it away. There are two loops: transition, which moves a work item from one stage to the next, and produce, which runs an action against a target. The agent fills in the inputs. The code decides what happens. This audits the mechanism, not the judgment. Which input the agent picks is still the agent’s call. Determinism doesn’t make the decision safe. It makes the decision inspectable, and it puts a gate between the decision and its effect. Every action runs the same sequence: php authorize - enforce policy - execute - record The order is important. Authorize before the run, or you can’t prevent it. Record after the run succeeds, or a crash mid-action loses the event. The check itself is small: decision = enforce role, action, resource, mode="strict" if decision.denied: raise PolicyDenied decision.reason A policy names who may do what to which resource. In strict mode, nothing’s allowed unless a policy allows it. In audit mode, everything’s allowed unless a policy denies it. Strict mode fails closed : if nothing explicitly allows an action, it’s denied. Audit mode fails open : an unknown action runs. Which one you pick is the whole difference between a gate and a log. Audit mode is where you learn what your agents do. Strict mode is where you decide what they may do. That enum from the top of the post? A denied action under a strict policy on the status resource. One line, and it never happens. One of the things I got wrong early was treating human-in-the-loop as on or off. Either a person approved every action, which didn’t scale, or nobody did, which was the prototype that deleted the status. Both are bad. The useful setting sits between them, and it should be a setting. open-refinery has five oversight levels, set per process. At L0, manual, every action needs a human to approve it. L1 and L2 loosen the grip: assisted, then supervised, where only the gated steps wait for a person. L3 is autonomous, no approval. L4 is dark, fully lights-out. You pick the level for a process based on how much you trust it — not for the whole system at once. What stays the same across all five is the machinery. The gates run the same way at every level, and the records share one schema. What changes is how many approval events the trail carries, and how many actions wait for a person before they execute. Because the loop is deterministic, moving the dial doesn’t change what gets checked or how it’s recorded; only who has to say yes, and how often. The boundary shows up again here. The dial sits on the platform, not in the harness. You can turn a process from supervised to dark without touching a line of agent code, because the approval step lives on the seam. Once the platform could gate actions, I needed to know which policies were actually doing anything. A policy you wrote and a policy an agent respects are not the same policy. open-refinery models this with claims. A governance claim is a statement about a repo: agents may not delete production data, all migrations get reviewed. Each claim can be backed two ways. An instruction states the intent. A gate enforces it. A claim is covered only when it has both. That gives three states, and the middle one is the whole problem. Instruction and gate: covered. The policy is written, and something stops violations. Instruction, no gate: partial. You wrote the policy. Nothing enforces it. This is the governance gap, made countable. It was the most common state I found. It’s the one that feels safe and isn’t. Neither instruction nor gate: an imitation surface. It reads as governed. It isn’t. open-refinery scores a repo on this. How many claims have a gate, and not just an instruction? I won’t pretend I have clean fleet-wide numbers to wave at you. What the model buys me is the ability to ask the question per repo instead of assuming the answer. Partial coverage isn’t a failure of intent. It’s a gap you can name, which means you can close it. Gating decides what happens. Provenance decides whether you can trust the record of what happened. These are different jobs, and I discovered that after ran them together at first. An audit log you can edit isn’t evidence. If someone can delete the record of the delete, the record is worth nothing. So the records in open-refinery are immutable, and each one chains to the one before it with a hash. Change a past event and every hash after it breaks. One function walks the chain and checks it: verify chain events raises if any link's hash doesn't match This doesn’t stop someone from doing something bad. It stops them from hiding that they did. I want to be honest about which one this is: tamper-evidence , not tamper-proofing. It only holds if two things are true: someone actually checks the chain, and its head is anchored somewhere the writer can’t reach. Put the log and the verifier on the same box an attacker owns, and they can rewrite an event and recompute every hash after it. A local chain raises the cost of quiet tampering. It doesn’t make it impossible. The gate isn’t one thing in one place, either. It shows up at three seams. The transition gate checks a work item moving from one stage to the next. The invoke gate checks an executor about to run a command. And there’s a call the harness makes before it acts at all: POST /authorize { role, action, resource, intent: "deploy to prod" } The harness asks before it deploys. It gets back an allow, or a PolicyDenied. The policy does nothing until it sits on one of these seams. A policy with no seam is the partial-coverage case from the last section, seen from the code side. This is the part the governance-gap conversation skips. If governance lives on the boundary, then changing the boundary is itself an action. So who governs that? In my early prototypes, the policy lived in a config file. Anyone who could edit the file could change it, and there was no record of who changed what, or why. I’d built a system that gated every agent action and left the policy itself wide open. That’s not a small hole. That’s the same gap, just moved up one level. In open-refinery, a policy change is a governed action like any other. It walks an approval chain. It lands in the same audit trail. Every version of a policy records its author, its timestamp, and what changed. Ask “who changed this policy, and when, and why,” and there’s an answer. A policy file anyone can edit with no record isn’t governance. It’s a suggestion with a version history, at best. Governance that can’t govern itself doesn’t hold. This is the second-order problem, and it’s where the boundary earns its keep: moving the seam is an action, so it passes a gate too. You don’t need a platform to start closing your own governance gap. You need to stop treating your audit log as a control. Pick one policy you’ve written for your agents. A real one: in a doc, a prompt, or a README. Find where it’s enforced. If the honest answer is “it isn’t,” you’ve found a partial-coverage surface on your own system. That’s the gap, and now you can see it. Add one gate: a check that runs before the action and can refuse it. Start with the action you’d least want an agent to take without you watching. For me that was a destructive database change. Pick yours. Make the policy change leave a trace. Even a commit with a reason in the message is a start on second-order governance. The goal is that “who changed this policy” always has an answer. Do those three things and you’ve moved from governance you can read to governance that can say no. That’s the whole gap, and it closes one gate at a time.