Skip to content

Agent permissions and the approval gate: designing the moment an agent stops and asks

Why asking before acting is the load-bearing property of an agentic system, and how to design an approval gate that survives contact with a busy team.

Last updated

9 minute read · Written by the team that builds UtilitySpaces. More about UtilitySpaces.

An agentic system earns trust in one moment: the moment it stops. Not when it drafts well, not when it finishes fast — when it reaches something it cannot take back and hands the decision to a person. Every other property is negotiable. That one is load-bearing, because it is what makes the rest survivable.

Why does it matter that the agent asked, rather than that it was right?

Because an agent is a probabilistic system driving a deterministic one. The model produces a plausible next step; the API executes it exactly. You cannot make the first half never wrong. You can make being wrong cheap, and that is a different engineering problem with an actual solution.

Accuracy work has no floor. Better prompts, more examples, a stronger model, and you are still one unusual input away from a confident mistake. The OWASP Top 10 for LLM applications is largely a list of ways a model can be talked into acting on your behalf, and none of them close by trying harder at the model layer.

A gate does what accuracy work cannot. It takes an unbounded class of errors, everything a wrong plan could execute, and bounds it to what a person looked at and agreed to. It also changes the sentence you say afterwards: not “the AI did something”, which is not an account of anything, but “a named person approved this at a timestamp, and here is what they were shown”.

What actually deserves to stop and ask?

Six categories, and they are easier to remember as one question: can this be taken back by the person who would have to take it back? Anything irreversible qualifies. So does anything a third party sees, anything touching money, anything that deletes or overwrites, anything crossing a permission boundary the agent did not already hold, and anything unusual in volume.

  • Irreversible. There is no undo, or the undo is a phone call to somebody else.
  • Seen by a third party. The damage is not in your database, it is in someone else’s inbox, and recalling it is not a technical operation.
  • Money. Payments, refunds, credits, discounts, anything changing what somebody owes.
  • Deletes or overwrites. Overwriting is deletion wearing a nicer name, and the previous version is usually the only copy.
  • Crosses a permission boundary. The agent is reaching something it has not reached before. The novelty is the signal, even when the action is harmless.
  • Unusual in volume. One archived contact is housekeeping. Four hundred is an event, whether or not each one is small.

What should never stop and ask?

Reads. Internal drafts. Anything trivially undoable. And anything a person asked for by name in the same breath — if someone types the request and the agent does exactly that, an approval dialogue is asking them to confirm the sentence they just wrote. That is not a control; it is a second click.

The read and write split is the one asymmetry worth building into the code rather than the policy. In UtilitySpaces the write gate never gates reads and holds every connector write for a person unless an admin has set that connection to run without asking, and the reason recorded alongside it is the whole argument: a wrong read is recoverable and a wrong write is durable. A read that should not have happened is a privacy question you answer afterwards; a write that should not have happened is already sitting in someone else’s system.

The gate applies to every caller, automations included: a flow someone built last month is no more a decision about today’s write than a model’s choice is.

Gating reads feels safer and is mostly cost: it slows the agent on the operations cheapest to correct, and spends attention on the actions least likely to matter.

So what does that look like on real actions?

Most of the argument about approvals is really an argument about where one particular action sits. The table below is the sort of list worth writing down for your own workspace before an agent touches anything, and worth revising once you see which rows people actually stop at.

ActionGateReason
Read last month’s orders from the storeNoA read. Recoverable, and gating it only costs attention.
Draft a reply and leave it in an internal channelNoNobody outside the workspace sees it. The draft is the review.
Send that reply to the customerYesA third party sees it and it cannot be unsent.
Add a label to a task cardNoUndoable in one click by anyone who notices.
Overwrite a shared documentYesOverwrites. The previous version may be the only copy.
Issue an invoiceYesMoney, and it lands with a third party.
Archive four hundred contacts marked staleYesVolume. Each one is small; the batch is not.
Write to an app the agent has never written toYesCrosses a boundary it did not previously hold.
Post to the company’s public accountYesPublic, permanent enough, and attributed to you.

A working default, not a standard. The column that matters is the third one: if you cannot write a reason, the row is probably a habit rather than a rule.

What goes inside an approval, and who applies it?

A request that says only “the agent wants to do something” is not reviewable. In UtilitySpaces an approval request carries the requesting agent, the channel it happened in, a preview of the content about to be committed, a status, and, once decided, the reviewer’s comment and the timestamp of the decision.

Eight kinds exist: task output, document update, message draft, tool permission, knowledge proposal, external data operation (a connector write), skill import, and model marketplace. The set being small and fixed is what makes batching and reporting possible later.

Every decision path funnels through one review function: the web UI action, an inline button in an instant-messaging channel, and a devspace step all reach the same code. The reason recorded in the code is that a dispatch table in the web layer would leave the other two paths flipping a status without applying anything — the record says approved, and nothing happened.

A decision also does more than flip a status. An executor is registered against the type and kind pair and runs inside the review itself. Approving a board task’s delivery marks the card done; rejecting it puts the card back to in-progress so the agent can refine it. Decision and consequence are one operation, not two things that usually agree. The approvals model is described in more detail here.

What if two connected apps could both do the job?

Then the write is refused, not guessed. Each write capability resolves to exactly one destination per workspace through a binding row. Two apps that could both service the write, with no binding chosen, comes back as capability unbound. There is no tie-break and no fallback, because two candidates is a question, not a tie.

Guessing a destination is the kind of decision that looks like helpfulness in a demo and looks like an incident in the log. A refusal that names the missing binding costs somebody one decision, once. For the wider question of how much access to hand over in the first place, the companion piece on giving an AI agent access to your business apps ranks the options by blast radius.

How do you stop people approving on reflex?

By asking less often and showing more when you do. Approval fatigue is the same failure as alarm fatigue in a hospital: the alarm is not ignored because people are careless, it is ignored because it has been wrong often enough that ignoring it is the rational strategy.

  • Batch by kind, never by item. Twelve message drafts reviewed together are one act of attention. Twelve separate prompts are twelve interruptions and one habit.
  • Show the payload, not a description of it. “Send an email to the customer” is unreviewable; the actual subject line, body and recipient are. A preview that summarises puts a second model output between the reviewer and the thing approved.
  • Default to a narrower scope so the common case never asks. Most fatigue is caused by a permission granted too widely and then policed at the gate. Fix it at the grant.
  • Make “approve and stop asking for this exact shape” possible. Not “approve everything”. The unit of trust is one shape of action with one destination, so widening it stays a decision rather than a shrug.
  • Measure the approve rate. If approvals are granted close to every time, the gate is theatre, and the honest fix is to widen the rule and stop asking.
  • Put the gate where the person already is. A separate console somebody must remember to open becomes a queue cleared in one unread sweep. In UtilitySpaces the request appears in the same shared inbox where the work is happening, or as an inline button in a messaging channel.

What does the audit log actually prove?

That nothing was edited or removed after it was written. The UtilitySpaces audit log is append-only with one monotonic sequence number per workspace. Every entry stores the previous entry’s hash and its own SHA-256 hash over its own contents, so altering or deleting any entry breaks the chain, and a verification pass reports the exact sequence number where it broke.

That structure is an ordinary hash chain. Two details make it hold in practice. Appends take a per-workspace advisory lock, so two concurrent writers cannot fork the chain into branches that each look valid. And the timestamp is hashed in exactly the string form the database hands back on read, because otherwise verification would recompute a different digest and report tampering that never happened.

Entries carry a title, a note, an optional code, an actor and a small key-value data bag — enough to reconstruct who did what without becoming a second copy of the database.

Now the part usually left out. A hash chain proves the log has not been edited since it was written. It does not prove the log is complete. If something never wrote an entry — a code path that forgot, a component outside the workspace — the chain verifies perfectly and tells you nothing about it. Integrity and completeness are separate properties, and only one of them is provided by hashing. Completeness is a coverage question, re-answered every time a code path is added.

Where does this design still lose?

In four places worth naming before anyone buys anything. A gate can only be as good as the boundary underneath it, a hash chain proves less than it appears to, the product has no third-party certification, and approval fatigue is a problem no product has actually solved. Each is a real limit, not a roadmap item.

The first is structural. Where a vendor issues only an all-or-nothing API key, there is no narrower scope to fall back to, so the gate is the only control left, and a person clicking approve without reading is the entire security model. Know which integrations are in that category rather than treating them as equivalent to scoped ones.

The second is the audit log limit above. The third is straightforward: no SOC 2 and no ISO 27001, and support is email, replying within one business day at support@utilityspaces.com. UtilitySpaces is a managed service, so a procurement process that requires one of those certifications cannot be satisfied here today. If that is the requirement blocking you, say so — knowing which one blocked a real team is how the order of this work gets decided.

The fourth is the one to keep watching. A product that asks about everything trains people to approve everything; the mitigations above reduce the rate, they do not remove the failure mode. Which is why the first move is not to gate more but to automate less: the ordering in what to automate first with AI agents keeps early work where a wrong answer is cheap. The free demo workspace expires exactly sixty minutes after it is created, which is long enough to see where an agent stops.

See it doing your actual work

Open a demo workspace, connect one app, and give an agent a real job. No card, no sales call.

We reply to every message within one business day.

Try the demo