Back to main

Where Agent Permissions Become Enforceable

[ AUTHORIAL INTENT & AI DISCLOSURE ]

This draft was written with Codex after reading Katelyn Lesse's X Article on secure agents and discussing the proxy architecture.

Forensic Hygiene Active
View Policy Standard →

Katelyn Lesse wrote a clear version of the security pattern production agents are converging on: keep the agent brain out of the sandbox, and make the sandbox act through mediated external interfaces.

That is the right starting point. It is also where the harder product and infrastructure work begins.

Her article, “Secure agents: architecture and sandboxing”, makes a simple architectural claim. The agent harness, session state, credentials, model keys, and OAuth tokens should live outside the sandbox. The sandbox should be the place where untrusted execution happens: cloned repos, generated code, shell commands, package installs, scraped pages, and tool output.

When the sandbox needs to call an external service, it should not hold the real credential. It should send the request through a trusted boundary. That boundary can decide whether the request is allowed, attach the credential, perform the request, and log what happened.

In shorthand:

trusted control plane
  agent loop
  session state
  task scope
  policy engine
  credential vault
  audit log
  approval flow
  kill switch

untrusted sandbox
  shell
  repo checkout
  generated code
  package installs
  no raw credentials

external action boundary
  checks policy
  performs narrow operations
  injects scoped credentials
  records the action

This is a useful line to draw. Without it, every prompt injection problem turns into a credential problem. If a README, webpage, package script, or test log can influence the model while the sandbox contains real API keys, you are asking redaction and model obedience to carry too much weight.

The cleaner design is structural: the sandbox cannot leak a token it never received.

The boundary changes the failure mode

The important shift is not just where the token lives. It is where authority lives.

In the weak design, the sandbox has a GitHub token:

agent sandbox -> api.github.com
             uses raw GITHUB_TOKEN

If the agent is tricked, the token is available. If generated code runs, the token may be readable. If the sandbox has open egress, the token or data reachable through the token can leave.

In the better design, the sandbox has no GitHub token:

agent sandbox -> GitHub broker -> api.github.com
                 broker uses credential only if allowed

The agent can still ask for work to be done. It can ask to read a file, open an issue, push a branch, or create a pull request. But the request crosses a narrow boundary first.

That boundary is where permissions become enforceable.

It is not the whole permission model. That distinction matters. The permission model also needs identities, task-scoped grants, resource classifications, data classifications, delegation rules, revocation, approvals, logging obligations, stale-decision protection, and replay protection.

The gateway is the enforcement point for that model, not the model itself.

A network proxy is not enough

The easiest boundary to imagine is an egress proxy:

allow github.com
allow pypi.org
deny everything else

That is useful. It is much better than an open network. It can block unknown destinations, close DNS leaks, add logs, and reduce obvious exfiltration paths.

But it is not enough for production agents.

These two requests may both be POST requests to GitHub:

create a pull request
remove a deploy key

These may both be requests to Slack:

post a status update in an internal channel
send a customer data dump to a public channel

These may both be requests to Google Calendar:

find free time
invite an external guest to a confidential meeting

A host allowlist does not understand the difference. HTTP method rules do not understand the difference. A credential scoped only to a broad API often does not understand the difference either.

The enforcement point needs to understand actions.

Prefer narrow brokers over generic forwarding

A general outbound proxy that tries to infer meaning from arbitrary HTTP is a weak foundation.

Real APIs have redirects, alternate endpoints, GraphQL documents with multiple operations, batch APIs, compressed payloads, encoded bodies, provider-specific edge cases, and behavior that changes after version updates. There are also time-of-check/time-of-use races between approval and execution. The request a human approved has to be the request that actually runs.

The safer pattern is a set of typed brokers:

GitHub broker
Slack broker
Calendar broker
Email broker
Package-install broker

Each broker exposes strict operations and constructs the upstream request itself. The agent does not get to send arbitrary HTTP and hope the proxy understands it. It asks for a typed operation:

{
  "operation": "github.pr.create",
  "repo": "org/repo-a",
  "source_branch": "agent/SOUL-123-fix-typo",
  "target_branch": "main",
  "title": "Fix typo in README",
  "body": "..."
}

The broker can validate the schema, check the task grant, bind the request to a stable hash, perform the upstream call, and verify the postcondition.

Raw HTTP forwarding may still exist for low-risk discovery or debugging. But it should be treated as a weaker capability, not the default shape of agent authority.

Be precise about capabilities

I keep reaching for the word capability because it is the right user-facing abstraction:

this run can create a PR in repo A
this run cannot merge it
this run can read calendar availability
this run cannot read event descriptions

But in security engineering, capability has a stricter meaning. A real object capability is usually an unforgeable reference to authority. It can often be delegated, attenuated, and revoked.

So it helps to separate four things:

user-facing capability vocabulary
  github.pr.create, calendar.freebusy.read, slack.status.post

policy representation
  RBAC, ABAC, relationship rules, object capabilities, or another model

delegated authority carrier
  short-lived grants, handles, tokens, leases, or signed requests

enforcement point
  typed broker or gateway that performs the operation

For example, a run might receive a short-lived grant like this:

{
  "grant": "github.pr.create",
  "repo": "org/repo-a",
  "source_branch_prefix": "agent/SOUL-123-",
  "target_branch": "main",
  "max_uses": 1,
  "expires_at": "2026-09-14T16:00:00Z"
}

That is much narrower than giving the run a GitHub token or letting it send any request to api.github.com.

The implementation could use RBAC, ABAC, relationship-based access control, object-capability handles, or a custom policy engine. The important thing is not the label. The important thing is that the authority is narrow, explicit, revocable, and tied to the work.

Acting through a broker still means acting

This is the part that is easy to miss.

If the sandbox cannot read the token, that prevents direct token theft. It does not prevent authorized misuse.

An agent can still cause damage through allowed actions:

read sensitive file -> paste it into allowed GitHub issue comment
read internal docs -> summarize them into Slack
modify CI workflow -> create pull request that looks harmless
call CRM API -> email the wrong customer

The broker did not leak the token. It performed an action the system believed was allowed.

So the policy has to ask more questions:

Who is requesting this?
Which task is this attached to?
What operation is being requested?
Which repo, account, document, or channel is the target?
What data classification is entering or leaving?
How much data is leaving?
Is this read-only, write, destructive, or external-facing?
Does this need human approval?
What exact request did the human approve?
Where will the audit record live?

A good broker is closer to an operations desk than a pipe. The agent asks for an action. The broker checks whether this task has that authority. If yes, it performs the action and records it. If no, it refuses or asks for approval.

The hard problem is composition

Action-by-action authorization is not enough.

Many unsafe outcomes emerge from combinations:

read_private_document + post_internal_message
read_customer_record + create_github_issue
modify_source + trigger_ci
read_calendar_details + send_external_email

Each operation can be individually legitimate. The unsafe property is the information flow or sequence.

That means the broker layer needs help from other controls:

provenance labels on retrieved data
destination classifications
limits on cross-domain movement
workflow constraints
aggregate budgets across a run
policies over sequences, not only individual requests

For example, data returned from an internal docs broker might be labeled internal-confidential. A Slack broker could then prevent that data from being posted to an external channel. A GitHub broker could prevent it from being written into a public issue. A model broker could prevent it from being sent to a model provider that is not approved for that data class.

This is harder than host allowlists. It is also closer to the actual risk.

Inbound data is part of the boundary

Outbound control gets most of the attention because exfiltration is easy to picture. But inbound data matters too.

Responses from allowed services become inputs to the model and sandbox. A GitHub issue, Slack message, webpage, package log, or calendar invite can contain hostile instructions. The broker should not treat responses as clean just because the destination was allowed.

A safer broker should validate and label responses:

source service
resource identity
data class
trusted or untrusted text
whether content may be shown to the model
whether content may be written into the sandbox
whether content may be forwarded elsewhere

This does not solve prompt injection by itself. But it makes the trust boundary visible. Tool output is data, not instruction. The system should preserve that distinction in the interface and in the policy layer.

The model provider is also an egress path

Keeping secrets out of the sandbox does not prevent sensitive retrieved data from entering model prompts.

If the agent reads a confidential document and the harness sends the whole thing to a model API, the secret did not leave through curl. It left through the model provider boundary.

That boundary needs policy too:

which model providers may receive which data classes
whether prompts are retained by the provider
whether tool outputs need summarization or redaction first
whether a local model is required for a given run
whether user approval is needed before sending a large context bundle

In agent systems, the model call is not outside the security model. It is one of the most important outbound calls.

Local effects still matter

A broker governs external services. It does not cover every way an agent can cause damage.

Local side effects can matter too:

filesystem writes
signing operations
mounted sockets
local IPC
build artifacts
poisoned generated files
cached credentials in developer tooling
outputs later consumed by trusted systems

A sandbox can produce a malicious build artifact without ever reaching the internet. It can edit a workflow file that looks harmless until CI runs with stronger credentials. It can poison generated documentation, config, or dependency locks.

That is why the external action boundary has to sit inside a larger run model: constrained filesystem, scoped workspace, reviewable diffs, safe signing boundaries, clean build phases, and downstream checks before trusted systems consume the output.

Approval is not automatically safety

Approval prompts can become theater.

Humans approve opaque diffs. They click through repetitive prompts. They approve summaries that omit the risky part. They approve request A and the system executes request B after a retry or redirect.

A useful approval flow needs more structure:

clear operation name
exact target resource
before/after summary
data-flow disclosure
request hash
expiry
maximum uses
risk class
post-execution result

For example, “Allow GitHub access?” is not a meaningful approval.

This is better:

Approve github.pr.create
Repo: org/repo-a
Source branch: agent/SOUL-123-fix-typo
Target branch: main
Files changed: README.md
External data included: none detected
Request hash: sha256:...
Expires: 10 minutes
Max uses: 1

The approval should bind to that exact request. If the request changes, the approval should not silently carry over.

Logs need their own policy

Logging is necessary. It can also create a new sensitive store.

If the broker logs full request and response bodies, it may copy source code, customer records, credentials, OAuth responses, internal documents, and regulated data into a second high-value system.

The audit layer needs its own rules:

minimize what is logged
redact secrets and obvious sensitive fields
separate metadata logs from payload capture
apply retention windows
control who can read the logs
make integrity tampering difficult
record enough to reconstruct decisions without duplicating everything

“Log everything outside the sandbox” is better than letting the sandbox erase the record. But it is not the whole answer.

Availability and recovery are part of the design

A centralized gateway can become a bottleneck and a failure domain.

The safe default for high-risk actions is fail closed. But real systems also need idempotency, retries, replay resistance, and reconciliation after partial success.

Consider a broker that creates a branch, pushes a commit, then fails before opening the pull request. On retry, it should know whether the branch already exists, whether the commit already landed, whether the request is still within its grant, and whether another approval is needed.

This is boring infrastructure work. It is also how security controls survive production traffic.

The install phase is a special risk

The article also recommends closing down egress and making sandboxes ephemeral. I agree.

One place I would be more careful is dependency installation.

It is tempting to say:

open the network
install dependencies
then lock the sandbox down

That may be practical, but dependency install is one of the riskiest parts of the session. Package install scripts can run code. Transitive dependencies can change. The agent can choose the wrong package. A compromised package can exfiltrate data before the stricter phase begins.

The safer shape is phased:

base image with known tools
locked dependency manifest
internal package mirror where possible
install scripts disabled where possible
separate low-privilege build phase
snapshot result
stricter task phase with narrower egress

This is not always pleasant. Agent systems are useful partly because they can recover from missing dependencies and messy environments. But if the system opens broad network access every time a model decides it needs a package, the design has a hole right at the beginning.

The sandbox boundary is not the only boundary

MicroVMs, user-space kernels, and disposable sandboxes are good engineering. Plain containers are often too weak a boundary for hostile code. Ephemeral sandboxes reduce what can persist after a bad run.

But microVMs are not magic. Hardware virtualization is a stronger isolation boundary than ordinary containers, but it is not physical separation. Hypervisors, device models, host kernels, firmware, and side channels still exist.

Sandbox isolation mostly answers:

Can untrusted code escape this execution environment?

Agent safety also has to answer:

Can untrusted text influence the decision layer?
Can the agent misuse an allowed tool?
Can sensitive data leave through an allowed destination?
Can a chain of allowed actions become unsafe?
Can the operator prove what happened?
Can a human stop the run from outside the sandbox?

Those are different questions. They need different controls.

I like the brain/hands split because it creates the right place to put those controls. Once the loop, state, credentials, logs, and kill switch live outside the sandbox, the system has a control point. Typed brokers can become the reference monitors for external action.

But the brokers have to enforce a broader authorization and information-flow model. They are not the model by themselves.

The pattern I want

The architecture I want looks like this:

trusted control plane
  agent harness
  authenticated principals and workloads
  session state
  task scope
  policy engine
  data classification
  grant issuance and revocation
  credential vault
  approval flow
  audit policy
  kill switch

narrow typed brokers
  validate operation schemas
  check per-task authority
  construct upstream requests
  bind approvals to request hashes
  label responses
  limit outbound payloads
  log minimally but durably
  verify postconditions

untrusted execution sandbox
  ephemeral microVM or equivalent
  no raw credentials
  no broad network
  constrained filesystem
  time and resource limits
  destroyed after the run

The agent can still be useful. It can still read, write, test, search, build, and propose actions. But the external world does not see “whatever the sandbox decided to do.” It sees actions mediated by policy.

That distinction matters.

The takeaway

Katelyn’s article gets the main architecture right: do not put the whole agent, its secrets, its tools, and its kill switch inside the same box as untrusted execution.

Split the brain from the hands. Keep credentials out of the sandbox. Make the sandbox disposable. Route external action through a trusted boundary.

Then do the next step: make that boundary typed and policy-aware.

The precise version is:

Keep credentials outside the sandbox, expose narrow typed operations through external reference monitors, and grant those operations per task, resource, destination, data class, and time window.

That is the part I care about most. The future of safe agent systems is not just better prompts or cleaner sandboxes. It is work-scoped authority, enforced at the points where actions become real.

Back to main