You needed production deploys to require a human sign-off. So you wrote it: an if in the deploy tool that checks a flag, posts to Slack, and waits. It worked. Then the refund tool needed the same gate. Then the delete-user tool. Now the rule lives in three places, each a little different — and nobody can tell you, in one sentence, what your approval policy actually is.
That’s the trouble with governance written as application code. It feels like the natural place to put it. It’s the wrong place.
Three things go wrong when governance is code
It drifts. Three hand-written copies of “require approval” do not stay identical. One forgets to write an audit line. One has a 30-second timeout, another waits forever. Six months in, the gates are cousins, not clones — and the differences are all accidental.
It can’t be reviewed as a whole. Ask “what in this system needs human approval?” and there’s no answer to point at. There’s only a grep. Your policy isn’t a document or a config — it’s an emergent property of scattered if statements, and emergent properties are exactly what you don’t want a security policy to be.
It’s tangled with the real work. The gate lives in the same function as the deploy. You can’t test “the approval policy” in isolation, because there is no such thing — there’s only the deploy function with a gate knotted through it. Auditors can’t read it. New engineers don’t see it until they trip over it.
The same gate, declared
Here is that approval gate in Praxec — a transition marked actor: human with a human executor:
deploying:
transitions:
ship:
target: deployed
actor: human # only a human can submit this
executor:
kind: human
queue: prod-approvals
With that, ship stops deploying when the model asks for it. The kernel records a human.approval.requested event, returns a pending status, and halts. The action does not run. A person resolves the queue.
The rule isn’t buried inside the deploy logic. It’s a property of the exposure, sitting in a config file, where anyone can read it, diff it, and review the whole set of gates at once. Need the refund and delete-user tools gated the same way? They reference the same shape — one policy, declared once, not three cousins that drifted. Governance stops being an emergent property of your code and becomes data you can actually point at.