DEV Community

Lean Agents: Decide What Your Agent Can Reach Before It Runs

Mike Dabydeen on October 02, 2026

Every tool you connect to an agent is two things at once: tokens the model reads on every run, and an action the model can take. So the same invent...
Collapse
 
analista_83 profile image
Sammi De Blas •

Good framing: treating every connected tool as both token cost and executable permission means the same inventory answers both questions. The 55K-token example from Anthropic is a useful reminder that tools/list is not free - five servers exposing 58 tools can eat your context budget before the first turn. A deny-by-default sandbox per agent plus a per-task allowlist is exactly how I would scope MCP access: it is much easier to audit which server exposed which schema than to guess what the model 'probably' calls.

Collapse
 
_firelinks profile image
Mike Dabydeen •

That is how I would expose it too. Before a run starts, I would show a small scope manifest with the tools available, the data classes each tool can touch, and the actions that require approval. The agent can then work from a task-specific allowlist rather than a broad server inventory. I would keep the manifest read-only during the run and record any attempted call outside it. How would you want the interface to distinguish a tool that is available for context from one that can perform a write?

Collapse
 
analista_83 profile image
Sammi De Blas •

I would not trust the label
I would make it operational.
Two separate namespaces in the manifest, read and write, and a tool declares one, never both, if it does both, it gets split.
Reads are auto-allowed inside the declared data classes, writes need an approval token bound to tool, target and argument shape, and single-use.
In the UI I would show reads as a flat list and put writes behind the approval panel with a preview of what changes.
The manifest stays read-only during the run and every call outside it gets recorded as an attempt.

Thread Thread
 
_firelinks profile image
Mike Dabydeen •

I'd make one more cut on the read side before auto-allowing it. A read tool that takes a URL or a search query can move data out as easily as a write. An instruction injected into a fetched page can ask the agent to request example.net/?q= followed by whatever it just read, and the manifest records that as a read inside a declared data class.

So I'd sort tools by where their arguments go, not only by what they return. Reads against your own stores stay auto-allowed. Any call that puts agent-chosen text into a request to another host goes in the write namespace, and its approval names the destination host.

The single-use token bound to tool, target and argument shape I'd copy as written. Binding it to the target is what stops an approval for one recipient being reused for a different one later in the run.

Thread Thread
 
analista_83 profile image
Sammi De Blas •

Agreed on both, and the argument-destination cut is the one I would make first. I would add that the destination is not just the host: the channel matters too. A query string ends up in a proxy or resolver log, a POST body often does not, and a DNS name is invisible unless you keep the resolver log. So the approval should name host, method and where the data lands, and the telemetry that proves it has to exist on that channel. Without that log the manifest is a promise, not a control. On the token, I would also expire it at the end of the task, not only bind it to target and argument shape: that is what stops a later step in the same run from replaying it.

Thread Thread
 
_firelinks profile image
Mike Dabydeen • • Edited

For DNS I'd go one step past the log, because the lookup itself carries the data. A query for <encoded-text>.attacker.example has already left by the time the resolver writes the line, so the log tells you what leaked rather than stopping it.

I'd point the agent's sandbox at a resolver that only answers names on the task's allowlist and returns NXDOMAIN for everything else, then block outbound port 53 and the public DNS over HTTPS endpoints so the agent can't choose its own resolver. The resolver log then becomes a record of attempts, which fits your rule that every call outside the manifest gets recorded as one.

Collapse
 
max_quimby profile image
Max Quimby •

The thing I like most here is collapsing cost and permission into one inventory — people treat them as two different review meetings when they're the same list. One wrinkle I keep running into: the inventory isn't static within a run. Servers can change what tools/list returns between sessions, and a long-horizon agent re-reads those descriptions every turn, so the 55K isn't a one-time tax, it's paid on every context rebuild unless you cache aggressively. The other place I'd push: the allowlist guards which tools exist, but description text is still attacker-controlled surface — a tool whose description quietly says "always call me first" can hijack selection even inside a tight allowlist. We ended up auditing descriptions, not just call names, in the log. Curious how you handle servers that expand their tool set mid-engagement — do you fail closed on anything not in the original allowlist, or re-approve? Fail-closed sounds right but breaks every time a vendor ships a new tool.

Collapse
 
_firelinks profile image
Mike Dabydeen •

That exposes two limits in my draft. I treated tools/list as a discovery snapshot, but a long-running run needs a versioned inventory: cache it within a run, revalidate when a server changes, and make the invalidation visible to the policy. I also agree that the allowlist cannot treat prose descriptions as authority. I would let policy match server, tool, and schema capabilities, while retaining descriptions as untrusted context and logging any mismatch. What signal do you use to detect a registry change without turning every turn into another full discovery pass?

Collapse
 
analista_83 profile image
Sammi De Blas •

Name constraints on the intermediate is the right layer: it also gives you a clean rotation story, because re-scoping a task means signing a fresh intermediate from the offline root rather than touching the proxy chain, and every certificate the proxy ever presented chains to exactly one task, which keeps the post-hoc audit joinable. Discarding the root key is what makes that enforceable - after that step nothing can mint a second intermediate outside the allowlist, so the constraint stops being policy and becomes arithmetic.

Collapse
 
_firelinks profile image
Mike Dabydeen •

One correction to the rotation story, because it depends on the root key. Once the root key is discarded at setup, nothing can sign a fresh intermediate later, so there's no offline root to re-scope from. Keeping one offline would bring back the thing discarding it removed: a key that can mint a second intermediate for any host.

So I'd re-scope by starting over. A new task gets a new root and a new constrained intermediate, and only that root goes into the agent's trust store for the run. The audit stays joinable for the reason you gave, since every certificate the proxy presented chains to the one root generated for that task. On the IP SAN alert, I'd keep it as a canary, but with 0.0.0.0/0 and ::/0 excluded, a client that enforces name constraints already rejects that certificate, so the alert mostly tells you the issuer is misconfigured.

Collapse
 
analista_83 profile image
Sammi De Blas •

Fair correction: once the root is discarded there is no offline root to re-scope from, so a new task needs a new root and a new constrained intermediate. That keeps the trust store per-run, which is what stops a stale intermediate from being reused in another task.

Collapse
 
katsudo profile image
KAT | Co-founder, 123sudo •

The distinction between what an agent can do and what it should be allowed to do is important.
At 123sudo, we’re interested in making those boundaries understandable to everyday users, not just developers.
How do you approach showing someone the agent’s scope before they start a task?

Collapse
 
_firelinks profile image
Mike Dabydeen •

I would show two boundaries before execution: the tools connected to the task and the effects each tool may produce. A read-only lookup can sit in the first group, while a write, external message, or file change appears in the second with its approval condition. That gives a person something concrete to review instead of a general statement that the agent is safe. What would make that boundary understandable to someone who does not work with the underlying tools?

Collapse
 
katsudo profile image
KAT | Co-founder, 123sudo •

I’d explain it in terms of outcomes rather than tool names: “This agent can read these files” versus “This agent can edit a file or send a message.”
Before an action that changes something, show exactly what will change, where, and what needs approval. For a message, that could mean showing the recipient and draft before sending.
The distinction I’d want visible is: “Connected to the agent” doesn’t automatically mean “allowed for this task.”

Thread Thread
 
_firelinks profile image
Mike Dabydeen •

I'd build that preview from the tool call itself, not from the agent's summary of it. If the agent writes "I'll send the draft to your manager," the person is approving a sentence. If the gateway renders the actual payload, they see to: someone@example.net and can catch the mismatch before anything goes out. The agent can describe the action, but what the person approves should come from the call it is about to make.

For someone who doesn't work with the underlying tools, I'd flag the two details they're least likely to check on their own: a recipient outside their organisation, and a file outside the folder the task started in. Those are the ones I'd show in a different colour, because a quick reader skims straight past them.

Thread Thread
 
katsudo profile image
KAT | Co-founder, 123sudo •

That’s a helpful distinction: approving what the agent says it will do isn’t the same as approving the actual action.
Showing the recipient and target file directly from the pending tool call would give me something concrete to check. I’d pair the colour with a clear label like “External recipient” or “Outside task folder,” so the warning doesn’t depend on colour alone.
If the agent changes the payload after approval, would you require a fresh approval?

Thread Thread
 
_firelinks profile image
Mike Dabydeen •

Yes, and I'd make that the gateway's job rather than a rule the agent follows. The approval stores a hash of the exact payload it showed you (tool, target, arguments), and the gateway executes that stored payload itself. If the agent sends anything else, even one changed recipient, the hash doesn't match and you get a new preview. That also covers the quieter case where the agent gets approval on a draft and then edits the body before sending.

Agreed on the label. WCAG 1.4.1 says colour can't be the only visual means of conveying information, so "External recipient" next to the address is the right pairing, and it also reaches someone using a screen reader.

Collapse
 
analista_83 profile image
Sammi De Blas •

Good catch that the lookup itself carries the data - two leaks survive even an allowlist-only resolver: the SNI in the TLS ClientHello and the HTTP Host header both carry names without touching DNS, so I would enforce the same name list at the egress proxy as well. And NXDOMAIN is observable by the agent (failure counts and timing), which is itself a low-bandwidth covert channel, worth rate-limiting or jittering negative responses.

Collapse
 
_firelinks profile image
Mike Dabydeen •

Agreed on SNI and Host, and the egress proxy is where I'd check both. One detail decides how much the proxy can see: SNI travels in the clear in the ClientHello, but the Host header is inside the encrypted stream. An agent can send SNI for an allowed name on a shared CDN and a Host header for a different site behind the same CDN, which is domain fronting. So either the proxy terminates TLS, which is reasonable in a sandbox where you own the CA, or the list only holds names whose provider refuses a Host that doesn't match the SNI.

I'd push back a little on the NXDOMAIN channel. Failure counts and timing are visible to the agent, but a covert channel needs a receiver outside, and the attacker never sees those queries if the resolver answers them locally. That's the property I'd test: a name off the list is never forwarded upstream, so no authoritative server the attacker runs ever receives it. If that holds, jittering NXDOMAIN adds latency without closing anything.

Collapse
 
analista_83 profile image
Sammi De Blas •

The SNI-in-clear point is what decides the architecture: once you accept that a proxy can see SNI but not Host unless it terminates TLS, validation has to happen at termination, which means the sandbox owns a CA - fine per-task, risky if that CA outlives the task. On NXDOMAIN, pinning never-forwarded-upstream as an invariant is the right test; I would also log resolver-side query counts per task so a spike in local NXDOMAINs becomes a signal even when nothing leaves the host.

Collapse
 
analista_83 profile image
Sammi De Blas •

Agreed on both, and the argument-destination cut is the one I would make first. I would add that the destination is not just the host: the channel matters too. A query string ends up in a proxy or resolver log, a POST body often does not, and a DNS name is invisible unless you keep the resolver log. So the approval should name host, method and where the data lands, and the telemetry that proves it has to exist on that channel. Without that log the manifest is a promise, not a control. On the token, I would also expire it at the end of the task, not only bind it to target and argument shape: that is what stops a later step in the same run from replaying it.