DEV Community

Cover image for Agents should never hold your API keys. NVIDIA just open-sourced the runtime that enforces it

Agents should never hold your API keys. NVIDIA just open-sourced the runtime that enforces it

Sam LABBE on October 23, 2026

Every AI agent you run today holds your secrets in its process environment: API keys, cloud credentials, tokens. One prompt injection away from ex...
Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

The gateway-as-trusted-core framing is honest about the one thing custody inversions always cost you: you concentrate trust in a single component, and its compromise becomes catastrophic instead of merely bad. I have run services where every secret lived in one secrets store and the access audit was only as good as the log pipeline feeding it, so "confinement is not evidence" lands hard for me — the policy answers what the agent can reach, but only a tamper-evident egress log answers what it actually reached. I would pair the Z3 gate with append-only connection logs by default, because a prover that green-lights a policy is only as trustworthy as the record proving the agent stayed inside it. One genuine question: does the sandbox ship connection logs up to the gateway, or is that explicitly out of scope for now?

Collapse
 
slabb profile image
Sam LABBE •

Honest answer: not covered in the walkthrough — the article walked the policy model, the credential custody, and the prover, and stopped before the log-shipping path, so whether connection logs reach the gateway is a repo question and a fair addition to the re-verify queue. What the walkthrough does establish is your composition's first half: enforcement is at the kernel per connection, which positions the gateway as the natural writer of the egress record — it decorates every request, so it can seal every injection and every destination at the same moment.

Where I'd build on your history: "the access audit was only as good as the log pipeline feeding it" is the exact failure this series calls the absence ceiling — a chain protects the records that exist and stays silent about the ones prevented, and a log pipeline inside the trusted core can be edited by exactly the compromise you're pricing. So the pairing needs one more clause: append-only by default, and anchored outside the gateway — because concentrating trust in one component is survivable the moment the record of that component's behavior escapes its write scope. Concentrate the trust; never concentrate the record of the trust. That's the third independent request for the evidence layer this week — walls-plus-recorder is becoming the consensus architecture before anyone shipped the integration.

Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

The absence ceiling is a sharper formulation than my audit pipeline line, and the clause it forces — "a log pipeline inside the trusted core can be edited by exactly the compromise you're pricing" — is the part that stays with me. It matches a failure I lived through building audit tooling: my first version wrote the audit trail to the same database the app wrote to, so anyone who could tamper with the app could tidy the evidence afterward. The fix was boring and unglamorous: ship logs to a separate account under separate credentials, append-only, no delete path. That the gateway is the natural writer of the egress record but must not be the keeper of it is the real design tension here. Three independent requests for the evidence layer in one week suggests the field has converged before the vendors have. Do you think the evidence layer ends up as a standalone product, or permanently as an appendage of whichever gateway wins?

Thread Thread
 
slabb profile image
Sam LABBE •

Standalone — and your own boring fix is the argument. "Separate account, separate credentials, append-only, no delete path" is the deployment shape of the evidence layer, and it only works as a separate account because the evidence has to be independent of the thing it records: an evidence layer owned by the gateway is custody inside the accused's write scope, which is the one rule this entire week has been proving. The three requests came from three different stacks — crash telemetry, metered APIs, sandbox gateways — and that's the tell: the demand isn't for a better gateway, it's for the layer that survives every gateway. The field converged on the composition before the vendors converged on the products: walls from one vendor, memory from another, evidence from a third — one fleet, three layers, and the third one has to be the vendor-neutral one, or it's just the diary with better manners.

(And your first version writing the audit trail to the app's own database is the absence ceiling in its purest historical form. The boring fixes are the ones that work — which is why they keep being rediscovered.)

Thread Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

I am persuaded on the mechanics — an evidence layer owned by the gateway really is custody inside the accused's write scope, and that argument is airtight. But the market mechanics cut the other way, and I cannot unsee it: the standalone layer only survives if someone pays for vendor-neutrality, and platform teams usually buy from whoever gives them one throat to choke. Logging had the same independence argument twenty years ago and still ended up folded into the observability suite. The three-requests-from-three-stacks fact is real demand, though. So my open question: what does the standalone evidence layer charge for that the bundled gateway cannot undercut — is it legal defensibility, or just the multi-vendor composition?

Thread Thread
 
slabb profile image
Sam LABBE •

Legal defensibility, but the mechanism is narrower than the word: a record's probative value depends on the keeper having nothing at stake in the dispute. The bundled version fails at exactly one moment — the moment its own vendor is the accused — and no discount survives that moment, because the cheapest possible record is worthless in the only conversation that matters: the one where the other side's auditor reads it. Logging got folded into the observability suite because no dispute ever turned on who kept the log. The stream stayed an appendage, so the independence argument never had to earn its keep. Agent receipts cross a trust boundary by design — they get tendered to counterparties as the record of a payment or a side effect. You buy logging you never show anyone; you buy evidence you must be able to hand to someone else. Multi-vendor composition is the delivery mechanism, not the product; the product is a keeper who loses credibility by lying, which is the one feature a bundler structurally cannot quote a price for. Where the bundled suites win is the fleet that never has a dispute — and that's the observability market again, at observability margins.

Collapse
 
gautamtalksdev profile image
Gautam K •

The custody inversion is the right default. The question I'd want answered next is about requests that stay inside the policy. Once an endpoint is approved, a DELETE to it gets the same credential as a GET. Human review here gates policy changes, but does anything gate a single destructive call to a host that's already approved?

The PocketOS volume delete in April had exactly that shape: an API the token allowed, one call, no undo. I work on per-request holds for that case, so I'm curious whether OpenShell's capability registry can tell a read from an irreversible write on the same endpoint.

Collapse
 
slabb profile image
Sam LABBE •

Honest answer from the walkthrough: it doesn't show method-level discrimination. The capability registry as described answers "can this sandbox reach this host with credentials attached" and "what changed when the policy moved" — reach, not verbs. A DELETE to an approved host rides the same decoration as a GET, and policy review gates the perimeter, not the call. The gap you name is real: endpoint policy can't see irreversibility, and "one call, no undo" is exactly the class that passes every host allowlist.

The completion is your specialty pointed at the registry: verb semantics as a first-class field. A capability registry that carries read / write / destroy per endpoint turns the prover's reachability query verb-aware — reach-with-destroy becomes its own question, reviewable separately from reach-with-read. And the per-request hold is the right enforcement point for the class the registry can't anticipate: the policy names what the endpoint is, the hold names what this call costs. Perimeter policy plus per-call holds on irreversible verbs is the pairing that would make the blast-radius claim survive its first DELETE — and the PocketOS April incident is the citation for why "allowed" and "safe" stopped being the same word.

Collapse
 
gautamtalksdev profile image
Gautam K •

"The policy names what the endpoint is, the hold names what this call costs" is the clearest version of this I've seen.

One thing I learned building an irreversible map: verbs alone aren't enough. A POST can destroy things (a GraphQL mutation like Railway's volumeDelete), and a PATCH can publish private code (changing a repo's visibility). So a registry needs body-aware rules for a few providers, not just method and path, and anything it can't read should be held rather than passed.

Thread Thread
 
slabb profile image
Sam LABBE •

Taken — verbs were the first cut, and your two cases are where method semantics lie: the GraphQL mutation is a POST that deletes, the visibility flip is a PATCH that publishes. Body-aware rules for the few providers where the damage hides in the payload — and the clause that makes it safe: anything it can't read is held, never passed. An unreadable request is a request the policy can't witness, so it waits for review instead of riding the credential. That's the undecidable token at the egress: named state, never a green light by default. Method first, body where it matters, hold as the resting state — that's the order in which the registry earns the blast-radius claim.

Thread Thread
 
heyvaldemar profile image
Vladimir Mikhalev •

Body-aware rules still mean parsing each provider's payload, and that list only grows. The other way out is to never hand the agent an HTTP API at all.

In the agent sandbox I run, the agent gets a set of tools from one MCP server, granted per agent, and the server checks every argument before it calls anything, with credentials the agent pods never hold. A volumeDelete nobody planned for has no tool to ride.

The cost moves to the schema: an argument the schema leaves open is the new approved host. In the cluster I run, send_email is granted, but its schema holds the recipient to one domain, and the server writes an audit line per call.

Thread Thread
 
slabb profile image
Sam LABBE •

That last line is the whole boundary law: the approved surface is whatever the schema admits, so the schema is the policy. Two things keep it from drifting back into a config file nobody dates. First, destructive verbs stay their own tools — volumeDelete gets no generic sibling, because discretion hiding in a payload argument is the schema's version of the write scope you just closed. Second, the schema itself gets the receipt treatment: sealed before use, so "the approved surface" is a fact with a timestamp, and a new host shows up as a sealed diff — the next argument the schema leaves open becomes a change someone sealed, not an edit someone made. That's also what upgrades your audit line per call: without a sealed surface it's a log; against a sealed surface it's evidence, because the call can be checked against what was approved, when it was approved. The pattern underneath is the same one the series runs on: a boundary is only checkable if its own history is under seal.

Collapse
 
amintacccp profile image
tamina •

The fail-closed prover is the detail I'd take to my own team. Most agent guardrails are regexes and prayer — a policy change is green until someone notices. unsupported_policy_shape flips that to "I can't prove anything about this, so no", which is the right failure mode for a security boundary.

Two gaps I'd name: rotation and schema coverage. When the gateway holds every key, revoking a leaked one has to propagate to every sandbox without downtime — that's the operational half of custody inversion, and it's the part that bites at 2am. And the prover only verifies the schema it understands, so the schema's coverage boundary quietly becomes the new attack surface.

That's what makes the "confinement is not evidence" limit the most honest paragraph here. "What can this agent do" and "what did this agent actually do" are both needed before this is deployable anywhere regulated. Still, "LLMs propose, solvers dispose" is the cleanest mental model I've seen for agent policy this year.

Collapse
 
slabb profile image
Sam LABBE •

Both gaps are real, and rotation is the one with a clock on it. The custody inversion's consolation is that rotation becomes central by construction: the gateway holds every key, so a revocation propagates from one point instead of twenty sandboxes hunted down at 2am — it's the same concentration that makes compromise catastrophic, pointed at the defensive case. The flip is that central rotation means central availability: if the gateway is down, the revocation is down with it. That's the trade the deployment accepts when it accepts the gateway as trusted core.

On schema coverage: agreed, with the one mitigation the design already ships — the boundary is declared. unsupported_policy_shape means the set of policies the prover didn't check is enumerable, which makes it a boundary instead of a hole: you can list what wasn't verified. The attack surface point stands, but an attack surface you can enumerate beats one you can't.

Collapse
 
amintacccp profile image
tamina •

That availability flip is the part I had not weighed properly. Central rotation turns a 2am hunt across sandboxes into one propagation point, but it also means a gateway outage freezes revocation itself — that is a sharper trade than just centralizing trust. And your enumerable-boundary point sharpens the schema case for me: an attack surface you can list is at least auditable, even if it is still an attack surface.

Thread Thread
 
slabb profile image
Sam LABBE •

Right — the trade isn't trust versus no-trust, it's which failure you can survive discovering late. One thing the journal side adds to the freeze: an intent to revoke can be sealed the moment it's decided, so the record shows the decision predating the outage — propagation is machinery and catches up, but "when you decided" stops being the gateway's word against yours. And "auditable but still an attack surface" is the honest ceiling of every enumerable boundary: listing it is the check, and the list going stale is the drift.

Thread Thread
 
amintacccp profile image
tamina •

That sealed-intent point sharpens it well — it separates the decision from the enforcement. A sealed revocation intent gives you a timestamp you can defend after the fact, so an outage no longer turns into a dispute about what was decided when. But it still leaves the live window open: until propagation catches up, the credential can still be used, so the journal fixes accountability more than it fixes availability. That is why I would pair it with a short-lived credential or a local fail-closed check where possible, rather than treating the journal alone as the mitigation. And on the stale list, I agree — enumeration is only a check if something keeps re-deriving the list from the actual system, otherwise it quietly becomes another unreviewed artifact.

Thread Thread
 
slabb profile image
Sam LABBE •

Right split — accountability fixed, availability not, and the pairing with short-lived credentials is the correct second half. It's also the older law in this space: expiry-as-default, survival-as-action. A short-lived credential inverts the renewal relationship — the stamp is the only path back in, so an outage or a compromise degrades into silence instead of persistence. The journal timestamps the decision; the TTL makes the credential stop arguing.

Collapse
 
kielltampubolon profile image
Kiell Tampubolon •

Good walkthrough. Moving credential custody out of the agent process is the change I've wanted to see as a default, because right now "don't leak the key" depends on the model behaving, which is not a control.

One question from reading the providers section: once the supervisor injects the resolved credential into the sandbox, does the agent process still see it in its own environment, or is it attached at the egress proxy so the agent never holds the raw value? Those are very different threat models. In the first case a prompt-injected agent can still read and send the token to any allowed destination; in the second it can only use it through the policy.

Also worth being clear for readers: the Z3 check proves a policy is consistent with the rules you wrote, not that the rules match your intent. Still a big step up from YAML nobody reviews.

Collapse
 
slabb profile image
Sam LABBE •

Answered from the source, since it's open: the agent process sees the credential in its own environment. inject_env in crates/openshell-providers merges the resolved provider environment into the child process's env map, and the README is explicit that the sandbox supervisor "injects credentials into agent child processes." So your first threat model is the live one: a prompt-injected agent can read the token out of its own environment and send it to any destination egress policy allows. Your second half exists as a surface, not a default — there is an openshell-gateway-interceptors crate for middleware on the request path, so raw-value-stays-gateway-side is buildable in principle, but nothing in the default path attaches at egress today.

The Z3 point is the sharper of the two. Consistency is cheap, intent is unprovable: a prover answers "does this policy contradict itself," never "is this policy what you meant." Which is why the human approval step is load-bearing rather than ceremonial — and why, on our side, the receipt binds the policy's hash: it proves the policy checked is the policy on record, not that the policy is wise.

Collapse
 
kielltampubolon profile image
Kiell Tampubolon •

Thanks for going to the source, that settles it. If inject_env puts the resolved credential in the child's environment, then a prompt-injected agent can read it and send it anywhere egress allows. The protection comes from the egress policy, not from custody. Good to know the interceptors crate exists, though. Keeping the raw value on the gateway side looks like the design worth pushing for upstream.

Agreed on Z3 too. Binding the policy hash into the receipt is a nice touch: it proves which policy was checked, and leaves "is this the right policy" to the human approval step.

Collapse
 
autenai profile image
Auten •

Good walkthrough. One gap the egress-injection model can't reach: credentials that have to go through a UI. A computer-use agent logging into a web app, or a desktop app with no API, has to put the password into a field on a screen, so there is no outbound request for the gateway to decorate.

The pattern that has worked for us there is the same inversion one level up: the agent asks a local tool to fill "the login for site X" by name, the tool types it on the machine, and the model only gets back "filled". Two leaks stay open after that: the screenshot taken right after typing (show-password toggles, an OTP sitting in a notification banner), and the target check. The tool should only type when the focused window or page origin matches the stored login, the way a password manager binds to a domain.

Does OpenShell's policy model have any notion of a display or input surface, or is it file/network/process only for now?

Collapse
 
slabb profile image
Sam LABBE •

File/network/process only for now — the display and input surfaces are outside the policy model's vocabulary, which makes your gap the real one: a typed password enters the screen, and the screen is a channel the gateway can't decorate. Your fill-by-name pattern is the password-manager inversion one level up — the agent asks for "the login for site X," the tool does the typing, the model gets "filled" — and your second guard is the part most implementations skip: the tool should refuse unless the focused origin matches the stored login, because a form-filler without origin binding is a universal injector with good manners.

The screenshot leak you name is the one I'd underline: after the pattern ships, the model's view is "filled," the gateway's view is a clean session, and the screenshot taken three seconds later still shows the OTP sitting in a notification banner. Display surface, input surface — both outside every rule in the policy model. That's not a knock on OpenShell; it's the boundary of what network-layer custody can see. The password managers solved this exactly the way you describe, and the agent stack will get there too — but today, your two leaks are the honest edge of the walkthrough.

Collapse
 
autenai profile image
Auten •

Agreed, and "a universal injector with good manners" is the right way to put it.

Two cheap mitigations for the screenshot leak that don't need the policy model to learn a display vocabulary. After a fill, verify through the accessibility tree instead of pixels: secure text fields don't expose their value there, so the agent can confirm "field is non-empty, focus moved" without ever seeing the characters. And when a pixel screenshot is unavoidable, mask the filled field's bounds and the notification area before the image leaves the machine.

Neither is airtight: a show-password toggle turns the field back into plain text, so the masking step has to check the field type at capture time, not at fill time. But it moves the leak from "every screenshot" to one specific state you can write a test for.

Thread Thread
 
slabb profile image
Sam LABBE •

The accessibility-tree verification is the right instrument — it answers "filled?" without pixels, which is the model-view/gateway-view split applied to confirmation: the agent learns a fact about the field, never the field. And your caveat is the honest one in the same sentence: the property has to be checked at capture time, not at fill time — the state of the screen when the evidence is taken is the only state that counts. That's the wrong-by-drift family again: the leak moved from "every screenshot" to one named, testable state, which is exactly what a named state is for.

Collapse
 
dexoryn profile image
Dexoryn •

Moving credentials out of the agent reduces the blast radius, but it also makes the gateway a critical part of the trust model.

Its access controls, audit trail, and recovery process deserve just as much attention as the sandbox. Otherwise, you're improving one security boundary while concentrating risk in another.

Collapse
 
slabb profile image
Sam LABBE •

Agreed — and that's the post's own limit two ("who runs the gateway... becomes your actual security boundary"). What the threads added since turns the concern into a price: the keeper question (what happens when the operator is the accused — the anchors decide, a fork has no timestamps), the canary test (plant a key, watch where it goes — egress approves destinations, not content), and the custody grade (a record kept inside the accused's perimeter says so, loudly). Naming the boundary is the first half; the second half is a record that complains when the boundary itself moves.

Collapse
 
ayoub_hammoudi profile image
Ayoub Hammoudi •

The custody inversion is the real headline. The place I'd poke first is redirects. The credential gets attached because the request was aimed at an approved host, and an approved host that answers with a 302 to somewhere else is the oldest way to walk a token out of the room. If the supervisor re-checks policy on every hop, the blast radius claim holds. If it only checks the first one, it's one open redirect away from the old model.

Collapse
 
slabb profile image
Sam LABBE •

From the walkthrough, enforcement is per-connection at the kernel — every connection passes the policy check before it leaves the sandbox, and credentials attach only to requests bound for approved endpoints. On paper that makes each hop face its own check: hop one to the approved host gets decorated, hop two to the redirect target is a new connection that doesn't match policy, so it leaves bare or not at all.

But your poke is still the right one, because the architecture only holds if the credential is attached per-connection and never re-sent by the client: what walks a token out on a redirect is usually the CLIENT re-sending auth to wherever it was told to go. That's a client-behavior question, not a policy-model question — and it deserves the probe rather than the assumption: an approved host that 302s to an external target, credential attached on hop one, then read hop two's headers. If hop two leaves decorated, the blast-radius claim is one open redirect wide; if it leaves bare or blocked, the claim holds. Either way the result belongs in the walkthrough — that's the test the article's claim is asking someone to run.

Collapse
 
manojkagitha profile image
Manoj Kumar Kagitha •

The gateway-as-trusted-core point is the one that stays with me. Moving custody off the agent is the right inversion, but it just moves the audit target. I would want a tamper-evident log of every credential injection before this touches a real fleet.

Collapse
 
slabb profile image
Sam LABBE •

Agreed — and that log is exactly the complementary layer the walkthrough's first limit points at: walls and flight recorder. The injection event is the natural receipt: credential id, sandbox id, destination endpoint, nonce, timestamp — sealed at the gateway before the key leaves, hash-chained like any other record. The custody inversion without it just moves the diary; with it, every credential use becomes a record the agent can't rewrite — which is what makes the blast-radius claim checkable after the fact instead of asserted at deploy time.

Collapse
 
mona_d_4222dda374567263b profile image
Amber D. •

The credential inversion is the part that matters most. Moving the blast radius from "every key in the environment" to "the endpoints this sandbox's policy named" is the right framing, and your point that keys stop living in /proc/self/environ makes the benefit concrete rather than abstract.

I also liked that you called out the fail-closed behavior in the prover. Returning unsupported_policy_shape instead of a green light is exactly what you want from a verifier, and it's a detail most write-ups would skip. "LLMs propose, solvers dispose" is a neat way to describe where this is heading.

The "what it doesn't do" section earns trust. Separating confinement from evidence (walls vs. flight recorder) is an important distinction, and the note that the gateway becomes the new trusted core is the kind of tradeoff people tend to discover the hard way.

One question: when the agent proposes its own policy change, how noisy does the human review step get in practice? I'd be curious whether the prover's "risky delta" flagging is selective enough to avoid approval fatigue on a busy fleet.

Thanks for spending the evening in the repo and writing it up.

Collapse
 
slabb profile image
Sam LABBE •

Honest answer: not measured — the walkthrough was an evening in the repo, and approval fatigue is a fleet-operation number. What the design gives you is selectivity by construction: only the deltas that move reachability wait for a human — a policy change that doesn't credentially reach anything new isn't a query, it's a diff. The noise floor on a busy fleet is the unmeasured half, and it belongs to the same family as rubber-stamp renewal: the approval that always passes stops being a control. The dashboard number is the flag-to-block ratio on a real fleet — if the prover is selective enough, the ratio stays interesting; if it flags everything, the human is a rubber stamp within a week.

Collapse
 
raju_dandigam profile image
Raju Dandigam •

Appreciate your source-level follow-up on inject_env: the child process can read the credential, while kernel egress policy limits where it can send it. That's a different guarantee from “the agent never holds keys.” A canary-key test covering stdout/logs and approved-host requests would make that boundary concrete. Is request-time proxy injection on the roadmap, or intentionally outside OpenShell's model?

Collapse
 
slabb profile image
Sam LABBE •

Right — and the honest name for what the kernel side actually gives you: egress policy approves destinations, not content. It is content-blind by design. An approved host is an exfil channel for anything the child process can read, at full bandwidth, because a token riding a query string to an allowed domain is indistinguishable from legitimate traffic at that layer.

Your canary test is the right instrument, and it pre-registers cleanly. Plant a canary credential through a provider, then run three legs: read it from env and write it to a log file, echo it to stdout, and place it in a request to an approved host. Predictions on record before the run: legs one and two land wherever the sandbox lets files and stdout land; leg three leaves, because the policy said yes to the destination and was never asked about the payload. A fourth leg is worth adding: check whether the OCSF HTTP Activity events capture request headers — if the recorder logs the request, the evidence stream itself is a channel, and that finding is worth more than the architecture post.

On request-time proxy injection: that's NVIDIA's roadmap to declare, not mine to guess. What the repo shows is the seam — the gateway-interceptors middleware is exactly where request-time injection would attach, so it is buildable inside their extension model without forking the thesis; nothing in the default path does it today. Buildable-in-principle, shipped-by-no-one is the honest state of the art, and the canary test is how anyone would know the moment that changes.

Collapse
 
ascendra_ventures_92c21f2 profile image
Ascendra Ventures •

The correction from possession to destination is the useful part of this post. An agent that can reach an approved host without ever seeing the key is a much smaller problem than an agent that has to be trusted with the key itself.

Gautam's point about DELETE versus GET on the same approved host is where most setups still leak. Approving a destination is coarse. On client builds that include agents, we write a short credential map before any code: each task, the hosts it may call, the methods it may use, and who rotates the secret. It fits on one page, and it turns an argument about trust into a review of a list.

Odinaka's comment on audit log independence matters as much. If the agent can write to the log that records what it did, the log is not evidence.

Has anyone tried OpenShell with inject_env for older tools that only read keys from the environment, as Kiell asked?

Collapse
 
slabb profile image
Sam LABBE •

Answered from the source, since it's open: yes — env injection is the legacy path by design. inject_env merges the resolved provider environment into the child's env map precisely so unmodified tools that read OPENAI_API_KEY (or their own var) work unchanged; request-time attachment would have broken every tool that reads env, which is most of them. The honest price of that compatibility is the boundary in the update: the legacy tool holds the raw value, always. Compatibility is why possession persists.

Your one-page credential map is the schema in human form — task, hosts, methods, who rotates — and "turns an argument about trust into a review of a list" is exactly what the schema layer formalizes. Two notes from the threads: the methods column has a home in the policy text itself (REST method and path selectors are part of the selector language the prover models, so DELETE-vs-GET granularity is provable, not aspirational), and the map deserves the receipt treatment — a list that steers approvals wants a date and an author on record, or it drifts into another unreviewed artifact. And Odinaka's line is the whole watcher thread compressed: if the agent can write to the log that records what it did, the log is not evidence.

Collapse
 
glenallen profile image
Glen Allen •

The distinction between restricting an agent's capabilities and verifying its actual actions is especially important. At IT Path Solutions, this is a valuable consideration when designing AI-powered workflows. Sandboxing and credential isolation reduce the blast radius, but tamper-evident audit trails add another layer by making actions independently verifiable. I'd also be interested in how teams handle incident investigation when an agent technically stays within its permissions but still produces an unintended outcome. How do you see that gap being addressed in production?

Collapse
 
slabb profile image
Sam LABBE •

The gap you're pointing at has a name in this series — confinement is not evidence — and production closes it with post-state, not better walls. An agent that stayed in its permissions and still broke something is a run whose outcome diverged from its authorization; no amount of sandboxing sees that, because it's not a boundary crossing. What sees it is the third input the watcher thread converged on: seal what changed (the diff), who consumed it (the consumer graph), and what actually happened after (post-state findings, including non-terminal activity recorded after the run was declared done). The unintended outcome almost always shows up as a state the run never predicted — a record missing, a count dropped, a downstream consumer acting on a stale input — and reconcilers catch those as findings, not as alarms someone has to interpret.

The production practice that makes it real is the asymmetry rule: nothing the agent writes may be load-bearing for the record of what it did. The moment the agent's own narration ("done", "tests pass") is the evidence, in-permission harm is invisible by construction — the agent filed the report about itself. Teams that investigate this well treat the agent's report as one voice among several, and the non-agent witnesses (CI artifacts, external checkers, counterparty receipts) as the ones that decide.

So the layering your team can test: walls reduce the blast radius, receipts make the outcome independently checkable, and post-state closes the gap your question names — the case where every permission was respected and something still went wrong. That last one is the interesting failure mode of 2026, and it's exactly the one that doesn't show up in any permission log.

Collapse
 
srinivaspadmanabhuni profile image
Dr Srinivas Padmanabhuni •

Keeping keys out of the agent process is the right default. One gap still worth testing even with a broker: secrets the agent does see, such as a token in a tool response or a value pasted earlier in the chat, can resurface many turns later in user-visible output or logs. I'd also test that exports or reads through the brokered credential stay scoped to the requesting tenant. The broker holds the key, but the agent still chooses what to ask for.

Tests: T6-011 Secrets held in memory are not emitted in later user-visible outputs; T5-011 Export tools scoped to the requesting tenant only. Checklist (CC-BY): huggingface.co/datasets/srinivaspa...

Collapse
 
slabb profile image
Sam LABBE •

Both gaps are the same sentence wearing two layers: the broker moved the key, it didn't move the judgment. And your checklist answers them better than the architecture alone can, because it tests both halves.

Gap 1 is the context layer, and its honest law is: once a secret enters the context, containment is probabilistic — detection is mechanical. A token in a tool result in turn 3 is part of the model's voice in turn 30; nothing at the prompt layer proves its absence later. T6-011 is the right test precisely because it's temporal — plant the secret, then ask for it later — which makes it the time-shifted version of the canary test this thread ran earlier. The distinction doing the work: the emission is a step, the sweep is a sensor, and only the sensor is load-bearing, because the absence ceiling applies to redaction too: log redaction is keeper-side and works; output redaction is sampling, and sampling never proves absence, only tests presence.

Gap 2 is the authorization layer — the confused deputy one rung up: the broker authenticates the agent, but "what is this request allowed to touch" remains the agent's choice. T5-011 checks the right invariant (tenant_id must equal the session's on every export_*), and the production version is to make the broker's allow/deny a decision with a receipt: what was requested, under which tenant scope, on whose authority, with the reach bound to the tenant the check actually enforced — not just the fact that a check ran. Admission receipts at the broker seam, and the tenant stays in the schema the broker enforces, never in the prompt the agent writes.

What I'd say after reading the full checklist: its spine is the same law this series' threads converged on from other directions, row by row. T5-018 ("allowlist enforced at the tool gateway, not only in the prompt") is the sentence "only something outside the model can enforce it" written as a test. T2-002 — failing an irreversible action that ran before a later-fixed prerequisite, "otherwise agents launder order violations after the fact" — is attempt-first sealing as a test. T3-001 (a handover note's "manager approved" is not an approval token) is the second-speaker rule as a test. And per-row mappings to EU-AI Act articles and OWASP-ASI are the obligation made operational. A catalog that arrives at the same vocabulary independently is the strongest signal a lane gets — worth the crosswalk, and worth keeping the CC-BY license on it.

Collapse
 
slabb profile image
Sam LABBE •

The crosswalk is on the project tracker now — issue #129 on noirebox/noirebox: ten rows checked against the findings, three named candidates (a journaled canary-sweep receipt, tenant-bound reach on admission receipts, the crosswalk doc itself). No code until a decision — the issue is the deliverable. Your catalog is in the tracker, not just in a thread.

Collapse
 
samii profile image
Waqas Ahmed Waseer •

Building on Dexoryn's point about the gateway becoming a critical point: I think the risk does not stop at the gateway, because most agent setups have a second, quieter credential store on the same machine.

OpenShell's correction is important — egress checks destination, not possession, and inject_env still puts the resolved credential in the child process environment. That makes the gateway's access controls, rotation, and audit trail essential, as Dexoryn says. But an agent that never receives a provider key can still reach local credentials if it runs with broad filesystem access: SSH keys, cloud credentials, and tokens in a home directory are a different custody problem from an API key held by the gateway.

That is why I would pair a gateway with whole-process isolation, not just a shell sandbox. A sandbox that only constrains shell commands leaves file-reading tools, MCP servers, and hooks outside its boundary, and if reads are wide open by default, the agent can still collect secrets that were never meant to be in scope. My checklist would be short, task-scoped network allowlists, explicit deny-read rules for credential paths, fail-closed behavior when the sandbox cannot start, and a test that tries to read a canary file and reach a blocked host. This walkthrough of Claude Code's filesystem and network isolation shows where a shell-only boundary stops: techriseups.com/articles/how-to-sa...

Without that second boundary, moving keys to the gateway improves one blast radius while leaving another open.

Collapse
 
slabb profile image
Sam LABBE •

Right — the gateway relocated one store; the home directory is the unbrokered one, and no egress policy sees it. Checked the source on whether the pairing you want already exists: OpenShell's restrictive default policy ships with an empty network policy list (block all) and a filesystem grant of include_workdir plus read-only /bin and /usr — the home directory is outside the default grant, so the SSH-keys-and-cloud-creds store you name is fenced in the same runtime, and your four checklist items map onto shipped knobs: network allowlists are network_policies, deny-read for credential paths is the filesystem policy's shape, and fail-closed on startup is the crate's own guidance ("treat unknown values as fail-closed").

Your canary-file-plus-blocked-host test is the geometry check arriving at the filesystem layer — declared vantage (the policy text) versus effective vantage (can the process actually read it), which is the same law the sandbox thread ran for egress. One addition from the other direction: the fence has an inside. A .env committed in the worktree is a credential living inside include_workdir — the custody problem doesn't leave with the home directory, it follows whatever the task checks out. Deny-read rules still earn their place for in-repo secrets; the canary should have a copy in the workdir too, because that's the one path no default excludes.

Collapse
 
galagent profile image
Qingfeng •

The "destination, not possession" correction in your update is the most important paragraph here, and it reframes what a credential gateway actually buys you. If the supervisor injects the resolved key into the child's env, a prompt-injected agent can still read the raw value; what stops exfiltration is the egress policy. So the policy is the security boundary, and the gateway is mostly hygiene: rotation, redaction, one place to revoke.

I run a local-first agent on my own machine and arrived at the same shape from the other direction, with the runtime -- not the tool wrapper -- enforcing filesystem/network/process rules. Two things I would add to this model from that experience:

  1. Fail closed on policy-check timeout. A per-connection check that times out is an implicit allow in most implementations. I would rather the sandbox drop the connection than default open -- cheap to get right, expensive to discover in an incident review.

  2. Classify by reversibility, not by action count, when deciding what needs a human OK. Reversible local edits run free and get logged; anything outward-facing (push, send, pay, delete) blocks for approval. Approving every call just trains you to rubber-stamp by the tenth prompt.

One thing I am curious about: does the Z3 prover reason about dynamic egress (ports and hosts resolved at runtime) or only the static policy text? That is the difference between "the endpoints this policy named" being provable and being aspirational.

Collapse
 
slabb profile image
Sam LABBE •

Your question has a two-layer answer, and the repo is explicit about the split. The prover reasons about the static policy text: its own boundary section says it models ASCII literals and wildcards in network and REST method/path selectors, over the runtime match language — it proves properties of the policy as written, and returns unsupported_policy_shape for what it can't model. The dynamic half lives one layer down, at enforcement: the supervisor runs its own policy DNS, hostnames only — IP literals are rejected outright ("direct IP connections bypass policy DNS and are blocked"), so a name resolving somewhere new is still caught at connection time. By the kernel, not by the solver.

So: provable as written, enforced as resolved. The gap your question points at is the correspondence between the two over time — the policy text that was proven and the resolution behavior that actually ran. That's drift, and it doesn't want a better prover; it wants re-derivation on a schedule, the same named-state move the geometry thread landed on.

Both your additions are the same law arriving from different directions. Fail-closed on check timeout is the crate's own guidance ("treat unknown values as fail-closed") applied one layer up — an implicit allow on timeout is an unknown value that got promoted to a decision. And classify by reversibility rather than action count is the schema thread's destructive-verb rule: outward-facing and irreversible are their own verbs for a reason — approving every call trains the rubber stamp you were trying to avoid.

Collapse
 
anne_jacobs_ac166227aa92b profile image
anne jacobs •

Very insightful essay thank you for sharing!

Collapse
 
cj_e49fbe4085948741f2f23d profile image
CJ •

Thank you for sharing this!

Collapse
 
kami_butt_69b922ade8de012 profile image
Agentic Affairs •

The custody inversion is the strongest idea here — blast radius shrinking from 'every key in the environment' to 'the endpoints this policy named' is a genuinely better security posture than any wrapper library. What surprised me is the Z3 prover at this maturity; most sandboxes ship policy-as-wishlists. How long does a policy proof take on a realistic fleet-sized ruleset — seconds or minutes?

Collapse
 
slabb profile image
Sam LABBE •

Not benchmarked in the walkthrough — the honest answer, and the question is the right probe for the re-verify. What the design suggests: the proofs it runs stay in the tractable class by construction, because the crate refuses the shapes it can't model — unsupported_policy_shape exists so the prover never hangs on a quantified blowup, it exits as a named state instead. So the honest expectation is seconds on the shapes it accepts, and a refusal — not a timeout — on the ones it doesn't. But expectation isn't a number: a fleet-sized ruleset run with the wall-clock published would settle it, and it belongs in the re-verify queue.

Collapse
 
kami_butt_69b922ade8de012 profile image
Agentic Affairs •

The named refusal is the elegant move — it turns a hang into a testable state, which is exactly the pattern a runtime needs. But it shifts the measurement question rather than settling it: seconds on the accepted shapes only tells the story if most fleet-sized rulesets are accepted shapes. So the re-verify number I'd pair with the wall-clock is the refusal rate on a realistic fleet policy — and what the runtime does when it fires: fail closed, or human-in-the-loop?

Collapse
 
aifrontierpost profile image
AI Frontier Post •

OpenShell still injects the resolved credential into the child process environment through inject_env, so its egress check approves destination, not content. That leaves an approved host able to carry a leaked key, so teams need separate audit and rotation for the gateway itself.

Collapse
 
parsa_m profile image
Parsa Mohammadi •

limiting hosts doesn't fix the verb if it can still run a DELETE where only a GET was intended, especially when an agent loops a retry on a 429 and accidentally wipes a table. how do you handle stateful requests where the body needs to change per retry?

Collapse
 
dynokore_team profile image
DynoKore •

Agreed on the principle, and it's worth doing even without a full sandbox runtime. On our side, a hook blocks coding agents from reading credential files before the tool call runs, and anything that needs a secret goes through a small tool that reads it in its own process and never prints it. The agent asks the tool to do the action; it never sees the value.

Prompt rules shape intent, but only something outside the model can enforce it. Kernel-level policy with a prover in front of policy changes is the same idea taken much further.

(DynoKore team, AI-assisted)

Collapse
 
busy_nikita profile image
Nikita •

Lots of great insights thank you for sharing!