Build1 publisher3 min readPublished
A seven-rung risk ladder pushes agent permissions out of the prompt and into an authorization layer
Hossein Hezami's ladder runs observe to destroy and gives sensitive reads a class of their own. The interesting part is the TypeScript, where risk arrives as a field on the request and someone other than the model has to set it.
The Engineer · Build desk

What happened
- A dev.to post by Hossein Hezami recasts the question of what an agent may do unasked as a permission design problem, sorting actions into pre-authorized, approval-required, and impossible.
- Instead of a flat tool list it proposes a seven-rung ladder running observe, analyze, propose, draft, execute, publish, destroy, with approval boundaries drawn between rungs.
- Reads are explicitly not treated as safe by default, because a read can expose secrets, PII, or data the agent goes on to leak somewhere else.
- The worked failure case is a system prompt saying ask before anything destructive while a delete_repository tool stays wired up, which the post argues influences the model without enforcing anything.
- The accompanying TypeScript defines eight risk classes and an authorizeAgentAction function that loads policy per user and agent, then returns either a denial or an approval-required decision.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- exposure Giving sensitive reads their own risk class removes read-only as a safe harbour: a scoped replica query can still move regulated data into a context window that later gets summarised into a ticket.
- decision Whoever populates the risk field becomes the trust boundary, so the build choice is between binding risk to the tool registry and letting the caller assert it.
- constraint Keying policy to a user and an agent means a grant has no natural expiry with the task it was granted for, which limits how narrowly you can widen permissions during an incident.
- cost Every action moved above the draft rung buys a human into the path, and the post's own critique of maximum caution says that human becomes the bottleneck, so the ladder relocates the queue rather than removing it.
Start with the field that does the work. `risk` is a property of `AgentActionRequest`, sitting beside `agentId`, `userId`, `action`, `resource`, an optional `estimatedCost` and a `context` bag [7]. The classification therefore arrives with the call. If the model is the thing that constructs that object, the authorization layer is grading the model's own homework, and the instruction to enforce outside the model has quietly been undone [4]. For the design to hold, `risk` has to be bound where the tool is registered, and the dispatcher has to refuse any action name it cannot find in that registry. The published excerpt does not say that, and it is the part I would write down first.
The policy lookup is `policyStore.forUserAndAgent(req.userId, req.agentId)` [8]. Two keys, neither of them a session or a task. A grant made at 03:00 while an agent is paging through a failing deploy is still in force on Monday, unless the policy object itself inspects `req.context`.
The two decompositions do not line up. The ladder has seven rungs, observe through destroy [2]. The `RiskClass` union has eight labels [6], and they are a different axis: `financial` and `privilege_change` describe consequence, not escalating commitment [17]. You need both, but no comparator maps one onto the other. Somebody maintains that table by hand.
Hezami lists seven actions an agent can usually take unasked and nine that should stop for approval [18]. The pre-authorized set is reads, summaries, drafts, a scoped replica query, a sandboxed test [9]. The ask-first set includes deleting a database record, deploying to production, refunding a payment and changing IAM permissions [10]. Treat that second list the way you would treat someone else's benchmark table. It is a claim about their blast radius. The stated test is whether an action is reversible, bounded, private and expected [11], so "delete a database record" only sits on the ask rung if a single delete in your system touches one row and has a restore path with a known recovery time. If it does not, it is destroy, and no policy file changes that.
The genuinely good call is `read_sensitive` as its own label rather than a subtype of read [6]. It matches the warning that a read is still dangerous when it exposes secrets, PII, or anything the agent can go on to leak [3]. Most permission models I have reviewed treat read as the safe default and spend their attention on writes.
The listing breaks off after the `approvalRequired` return [19], so the allow path is prose rather than code. That matters for one specific claim: budgets, rate limits, scopes and audit logs are described as part of the permission system [12], but `estimatedCost` is optional on the request [7]. An optional field cannot support a mandatory budget check, because the caller that omits it gets a decision anyway. The question mark is doing more policy work than the paragraph above it. Make the field required for every risk class above `write_draft`, or accept that spend enforcement is advisory.
What to watch
- Whether the allow path gets published: the excerpt stops after the approval branch, so where scope and budget checks sit is unspecified.
- Whether policy grants get keyed to a session or a task as well as to a user and an agent.
- Any published latency figure for the approval queue, which decides whether the ladder is usable mid-incident.