DEV Community

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

Posted on Edited on

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

Uses Z3 SMT provers and kernel sandboxes

Every AI agent you run today holds your secrets in its process environment: API keys, cloud credentials, tokens. One prompt injection away from exfiltration — and 2026 proved it twice, with two separate compromises of the most popular LLM proxy on PyPI, in March and again this week. NVIDIA's answer landed on GitHub this week: OpenShell (Apache 2.0) — a runtime where agent fleets run in kernel-level sandboxes, credentials live behind a gateway instead of in the environment, and where every policy change is checked by a Z3 SMT prover before a human approves it.

This is not another agent framework. It's the layer underneath: confinement, custody, and proofs for fleets of agents that already exist. I spent the evening in the repo — here's what it actually does, mechanism by mechanism.

The three pieces

OpenShell is three components: a gateway (the control plane), a supervisor, and sandboxes (minimal Ubuntu by default, GPU passthrough optional, Kubernetes via Helm if you're fleet-sized). The policy model covers filesystem, network, and process rules; enforcement is at the kernel — every file access, syscall, and network connection is checked at runtime, not by a wrapper library the agent could forget to call.

The quickstart is two commands, on macOS Apple Silicon, Linux, or WSL2:

curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh
openshell sandbox create --name demo
Enter fullscreen mode Exit fullscreen mode

The default sandbox ships with no agent inside — you bring yours. The docs' first-agent walkthrough runs OpenCode against a free OpenRouter model, which means you can feel the whole thing without a paid key.

Mechanism one: credentials leave the agent

This is the inversion that matters. Today's default is: you export OPENAI_API_KEY into the agent's environment, and pray. The agent process holds the crown jewels in /proc/self/environ, one prompt injection from leaking them into whatever URL the model decided to fetch.

OpenShell's providers crate restructures custody:

  • The gateway persists provider records — API keys, bearer tokens, local account configs — discovered from your environment and config files, normalized, and kept out of logs ("Callers that display provider data must redact sensitive fields by default" is written into the crate's contract).
  • The sandbox supervisor fetches resolved provider config from the gateway and injects credentials into the agent's child processes.
  • The agent never stores a credential — keys live at the gateway, redacted in logs — but the boundary deserves precision (see the update below): the supervisor injects the resolved credential into the child process's environment (inject_env, providers crate), so the child can read the raw value. What the kernel layer enforces is destination, not possession: every network connection passes a policy check before it leaves the sandbox, and no request-time attachment exists in the default path — that would build in the gateway-interceptors middleware.

Read that again from the custody perspective: the blast radius of a compromised agent stops being "every key in the environment" and becomes "the endpoints this sandbox's policy named". Your agent can still work — read files, install packages, call the APIs you approved — but the keys to everything else simply aren't in the room.

Mechanism two: a Z3 prover gates policy changes

The part I didn't expect at this maturity: the openshell-prover crate is a formal verifier for sandbox policies. From its own README:

Encodes a policy + its attached credential set + a binary capability registry as a Z3 SMT model, then runs reachability queries to detect credentialed-reach and capability changes a reviewer should be aware of.

Concretely: when a policy change is proposed — by you, or by the agent itself — OpenShell doesn't just diff the YAML. It encodes the would-be state into SMT and asks reachability questions: can this sandbox now reach a new host with credentials attached? call a new API method? Risky deltas wait for human review. There's also a containment API: give it an operator boundary policy and a candidate, and it answers Within / not — with counterexamples when the answer is no.

And the failure mode is the right one: the model has bounds (ASCII literals in network selectors, no embedded NUL bytes), and when a policy uses a shape it can't model, it returns unsupported_policy_shape — "I can't prove anything about this" — instead of a green light. The crate's own guidance: treat unknown values as fail-closed. Whoever wrote that sentence has been burned before, in the good way.

If you've followed the formal-verification world this week, the timing lands: Anthropic published the same evening that LLMs are making formal verification feasible at much larger scale. Put the two together and a pattern emerges for the agent era — LLMs propose, solvers dispose: the model writes the policy, the prover holds the gate, the human signs the deltas.

What it doesn't do — read before deploying

Three honest limits, because a runtime named "safe, private" invites oversharing:

  1. Confinement is not evidence. OpenShell answers "what can this agent do?" — it doesn't answer "what did this agent actually do?" A sandboxed agent can still take wrong actions inside its walls, and after the fact you'll want a tamper-evident record of what happened, not just the walls that were up. Those are complementary layers — walls and flight recorder — and a serious fleet eventually wants both.
  2. The gateway is the new trusted core. Credentials now live with the gateway instead of the agent. That's a strictly better blast radius, but it moves the crown jewels — so who runs the gateway, and how it's backed up and audited, becomes your actual security boundary. Self-hosted via Helm is supported; treat that box like the keys it holds.
  3. It's 0.1.x. Stable cadence, real tests, RFC process, security policy — but young. The prover's own docs enumerate exactly which policy shapes are out of model. Read the support matrix before pointing a production fleet at it.

Run it yourself

curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh
openshell sandbox create --name demo
# then: docs.nvidia.com/openshell → "Run Your First Agent" (OpenCode + free OpenRouter model)
Enter fullscreen mode Exit fullscreen mode

And for the prover, the crate README carries a containment example you can build from source: a boundary policy, a candidate, check_within_boundary, and a CheckResult::Within or a counterexample. Ten minutes with the policy schema teaches you more about agent confinement than a month of threads.

Repo: github.com/NVIDIA/OpenShell · Docs: docs.nvidia.com/openshell · Apache 2.0, SDKs in Python/TypeScript/Go/Rust, skills for your coding agent (npx skills add NVIDIA/OpenShell).

Update (24 Oct) — a correction the threads earned

"Mechanism one" overshot, and kielltampubolon's question caught it: does the credential reach the child's environment, or attach at egress? The source answers env — inject_env in crates/openshell-providers merges resolved credentials into the child process's env map, and the README says the supervisor "injects credentials into agent child processes." The bullet above is corrected. What holds: credentials leave config and logs, and egress is policy-gated. What doesn't hold by default: proxy-style injection at request time — the gateway-interceptors middleware is the seam, and nothing in the default path attaches there.

Two additions the threads earned:

  • Egress approves destinations, not content (raju_dandigam). An approved host will carry whatever the child can read, at full bandwidth — a token riding a query string to an allowed domain is indistinguishable from legitimate traffic at that layer. His canary test pre-registers cleanly: plant a canary key, then three legs — write it to a log, echo it to stdout, send it to an approved host. On record before the run: the third leg leaves, because the policy was asked about the destination, never the payload. A fourth leg is worth adding: do the OCSF HTTP Activity events capture request headers? If the recorder logs the request, the evidence stream is itself a channel.
  • The evidence half got sharper (robertpaschal, five rounds): a keeper who is party to the dispute is a second speaker, and a custody grade only bites if a third party can derive it from the anchors without the keeper's cooperation. His strictest rule — a retention event may re-grade a drift finding, never close it; only the next independent anchor clears it — is stricter than what ships on the journal side today. Adopted there as a named change, not defended.

The title's claim survives in its precise form: the keys leave your config. They don't leave the process — and the canary test is how you keep checking.

TL;DR — the agent-security stack is assembling in layers this month: kernel sandboxes for confinement, gateway-held credentials for custody, SMT provers for policy review, and (from the other camp) journals and attestations for evidence. Your agent still can't be trusted. But it can finally be contained, funded, and proven — which is what "trusted" was supposed to mean all along.

Top comments (75)

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.

Thread Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

That keeper-with-nothing-at-stake test is the right filter, and it exposes the weak spot in a gateway-owned layer too: if the gateway operator is the party in the dispute, its own record is no more disinterested than a bundled vendor log. I have seen this in payment-adjacent systems, where the log that mattered was the one neither side could rewrite after the fact, because it was hashed and shipped to a store outside both parties' control. So I would push the question one step further: who operates the evidence store, and what happens if that operator is the accused? If the answer is a separate keeper whose business is credibility, not the transaction, then the gateway can own the mechanics while someone else owns trust. Is that separation how you see it working, or do you think a gateway can credibly be its own keeper when it has no financial stake in the outcome?

Thread Thread
 
slabb profile image
Sam LABBE • • Edited

The separation is how it actually works, and your payment-adjacent log is the anchor chain described from the buyer's side: hashed, timestamped by someone with no stake, verifiable without the keeper's cooperation. That's the mechanism behind "what if the operator is the accused" — the accused can stop writing and can't unwrite. Fork a fake chain and the fork has no timestamps; the anchors decide who is lying, not the vendor's word. So yes: the gateway can own the mechanics while someone else owns trust — collection and formatting on one side, RFC 3161 anchors and witnessed heads on the other, and the custody grade disclosed rather than claimed: a record kept inside the accused's perimeter says so (custody_unanchored), a lapse says so (custody_drift).

On self-keeping with no financial stake: the stake is never only financial. The operator's stake is the version of the story it can live with — the watcher thread this week ran on two agents editing their own monitors, and a disclosure arriving late is the operator's reputation optimizing the record. So no, I don't think a gateway can credibly be its own keeper. And the honest limit runs both ways: a separate keeper isn't automatically clean either — its custody gets graded like anyone's. The product isn't "never the accused." It's a record that degrades loudly: you can see the moment the evidence left disinterested hands.

Thread Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

The 'degrades loudly' framing is the part I would keep — a record that fails silently is worse than no record at all. The test I would add is who can recompute the grade: if the custody grade is the keeper's own attestation, it is the same problem one level up, and a lapse the keeper chooses not to report stays invisible. The grade only bites if a third party can derive it from the anchors — the RFC 3161 timestamps and witnessed heads — without the keeper's cooperation. Do you treat the grade as derived from the anchors, or as the keeper's signed claim about its own custody?

Thread Thread
 
slabb profile image
Sam LABBE •

Derived — the code answers it in the signature: custody_grade(events) takes the chain and computes the row, it never reads a claim. No anchor in the chain, custody_unanchored fires; a tail beyond cadence, custody_drift — both are arithmetic over two inputs the keeper doesn't author at check time: the hash-checked chain, and the anchors, which verify without cooperation because a TSA token's imprint is checked against the head digest anyone can recompute. Your test — a third party deriving the grade from the anchors — is exactly the design: hand the verifier the journal and the tokens, and the grade is recomputed end to end; the keeper's word enters nowhere, and a keeper-presented chain whose head doesn't match the anchored digests isn't a dispute, it's the finding.

The residue worth naming: the derivation needs the chain bytes. Withhold the journal and the verifier still holds the anchors — proof that a head existed at T — and every counterparty that kept its own anchor interval can prove its own receipts without the keeper. What stays a keeper claim is only the repair path: a drifted tail is fixed by an anchor or a sealed retention event, and a retention event is a self-report — graded as such, a claim with a timestamp, upgradable by the next anchor. The keeper's word never enters the grade; it only ever enters the repair. And the derived grade seals into the report as a disclosure that never touches clean_pass, so the act of measuring custody can't be mistaken for passing it.

Thread Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

That distinction between the grade and the repair path is the part I would keep, because it stops a keeper from laundering a claim into a pass. The gap I would still test is what happens between anchors: an anchor proves a head existed at T, but it says nothing about a tail that was rewritten and re-anchored in between, unless the cadence check also binds each anchor to the previous head. In a payment-adjacent log I worked on, that in-between window was where a quiet correction slipped through, and only a rule that a retention event cannot itself close a drift — it can only be upgraded by the next independent anchor — kept the repair honest. Would you require the next anchor to cover the retention event's own timestamp, or just its existence?

Thread Thread
 
slabb profile image
Sam LABBE •

Between anchors, the binding you're asking for is already in the object: an anchor imprints the head digest, and the head commits to its entire prefix — so anchor N+1 carries anchor N's anchored head as an ancestor, or you're not looking at a chain, you're looking at a fork. The quiet correction you caught slips through a different seam: it works only when the old token is gone. Rewrite the tail, re-anchor the new head, and the verifier who kept the T1 token sees the fork immediately — that imprint isn't a prefix of anything presented now. The verifier who didn't keep it sees a perfectly consistent chain with fresh anchors. That's why retention is priced in the grade at all: the tokens are the custody, and a counterparty that keeps its own anchor interval needs nothing from the keeper to detect the rewrite.

On your question — existence, and the reason isn't laziness. The next anchor proves the retention event was in the chain by its genTime: an upper bound. The previous anchor gives the lower bound. The claimed timestamp inside that window is a keeper-authored field no TSA can witness — genTime is the token's own clock, it never testifies to when a claim says it happened. Requiring timestamp coverage would be asking the anchor to underwrite the self-report it exists to price.

And your rule is stricter than what shipped, which I'll say plainly: as implemented, a sealed retention event is accepted as a repair — it quiets the drift finding. Your version is the better law, for the reason you gave a comment ago: a self-report that clears its own finding is the laundering the grade/repair split exists to stop. The strict form — a retention event re-grades the finding instead of closing it, and only the next independent anchor clears it — is a named change now, not a defense of the current behavior.

Thread Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

Existence with genTime as the upper bound is the answer I can actually use, because it stops me asking an anchor to underwrite a keeper-authored timestamp it never witnessed. The part I will keep is your fork distinction: the verifier who kept the T1 token sees the rewrite immediately, while the verifier who did not sees a perfectly consistent chain, so the protection is only as strong as the counterparty's own anchor interval. In a payment-adjacent log I worked on, that was the real cost — the audit only worked when the other side kept its own tokens, not when it trusted ours. Is keeping an independent anchor interval a requirement you would put on counterparties, or just a recommendation?

Thread Thread
 
slabb profile image
Sam LABBE •

Requirement at the handshake, grade after — those are the only two instruments the journal actually has. At the moment of exchange you can make the interval a term: a hand-off can demand artifacts, so "your anchored head, from your own witness, covering what I just handed you" is a demand the schema carries, not a favor anyone requests. After the handshake, enforcement is gone by design — the journal has no authority over a counterparty's storage, and pretending otherwise is how trust models rot. What replaces it is a grade, and the vocabulary already exists from the channel question: demonstrated, assumed, undeclared. A counterparty holding its own anchor interval has demonstrated custody — its receipts verify without the keeper's word. One that relies on yours is assumed — every dispute passes through the keeper's storage, and the receipt says so. Undeclared is the default nobody should accept silently.

Your payment-adjacent finding — the audit only worked when the other side kept its own tokens — is the same law the two-sided denominator runs on: the server seals fetches_served, the consumer seals receipts_held, both counters sealed like everything else, and the check grades the pair. A denominator kept by one side is a courtesy, not a count, and the check exists because "trusting yours" was the failure mode it was built from.

Thread Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

The demonstrated / assumed / undeclared vocabulary is the part I will steal, because it names the thing I kept dancing around: enforcement ends at the handshake, but the receipt can still say exactly what kind of custody you got. In the payment-adjacent log I worked on, the audit only held when the other side kept its own tokens, and "assumed" describes that failure mode precisely — every dispute routed through the keeper's storage. Does an assumed counterparty get a path to demonstrated, like a probationary exchange where it has to produce its own anchor before the next hand-off, or is the grade itself the whole instrument?

Thread Thread
 
slabb profile image
Sam LABBE •

Both — the path exists, and it isn't a ceremony, because the grade is derived. A claimed grade would need a probation procedure: someone awards demonstrated status, someone can withhold it. A derived grade is the path: the moment the counterparty holds its own anchor covering its interval, the grade recomputes to demonstrated — no petition, no approver, nothing to withhold. Produce the tokens and the facts changed.

Your probation ladder is still real, though — it lives one level up, as policy on the handshake demand: assumed counterparties get one exchange, and every later hand-off demands their anchored head. The journal doesn't schedule probation; it makes each rung provable, because the artifact demand at a hand-off is already a term the schema carries.

And the part that keeps the path honest: the transition is dated by the tokens, not by anyone's word. A counterparty that anchors late covers existence-by-genTime, but the window between the handshake and that first anchor stays visible in the record — when you were assumed and when you became demonstrated are both on the chain. Probation that can't be backdated is the only kind worth having. The grade is the whole instrument only if you forget that its own history is sealed too.

Thread Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

That distinction settles it for me: if the grade recomputes from the tokens, probation is not a second grade inside the journal, it is handshake policy around the first exchange. I had it backwards when I framed it as a ladder in the record itself. The dated transition is the part I will keep — a counterparty that anchors late still leaves the gap visible, so 'became demonstrated' cannot be rewritten later as 'was always demonstrated.'

Thread Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

That separation clears it up for me: the derived grade is the path, and probation is the handshake policy sitting above it. In API terms I would treat token possession the same way, as proof rather than a claim I have to take on trust, and I would still keep the demand explicit at every hand-off so an assumed counterparty cannot quietly keep moving without ever producing its anchor. The dated transition is the part I would steal for audits, because a log that shows when assumed became demonstrated is far harder to argue with than a current status field. How do you handle an anchor that expires mid-interval, before the next hand-off?

Thread Thread
 
slabb profile image
Sam LABBE •

Two different things can expire there, and they get different answers. If the anchor cadence lapses — the interval you expected passes with no new token — that's a named finding, not a silent state: the time-based drift check fires when the tail outlives the newest anchor by more than the declared cadence, and the grade degrades loudly until the next anchor covers it. The finding is the mechanism; nothing about a lapsed interval gets remembered as coverage.

If it's the token's certificate that expires — the TSA's chain going stale — the token stays verifiable for its genTime: RFC 3161 tokens carry their certificate material, so a historical token proves "this head existed at T" long after the TSA rotates, provided the verifier keeps the chain. That's long-term validation, and it's why the anchoring side pins roots and uses multiple TSAs: no single certificate expiry orphans a head, and re-anchoring under a fresh TSA re-covers the old ones for free. An anchor mid-interval is never "expired" in the sense that matters — worst case it's old, and old-but-verifiable still decides who is lying in a fork dispute.

Thread Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

Splitting cadence lapse from certificate expiry clears it up: one degrades loudly, the other stays verifiable for its genTime.

Thread Thread
 
slabb profile image
Sam LABBE •

The reversal is the rarest move in these threads, so it deserves naming: you took the harder side of the argument and closed it yourself. For the record, the ladder wasn't wrong as policy — it just lives at the handshake, where demands are cheap, and not in the journal, where grades recompute for free.

Thread Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

The handshake versus journal placement is a useful distinction.

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.

Thread Thread
 
heyvaldemar profile image
Vladimir Mikhalev •

Agreed, and the cheapest destructive verb has no tool at all: the agent I run gets records_lookup, quarterly_report and send_email, and the eval gate fails any run that leaves a record deleted.

The check against the approved surface already runs, too. Every eval reads the server's audit and recomputes from policy.json that each call let through was one that person may make.

A seal proves approval, not correctness. So the static check also runs the server's own code for every person in the policy, against five planted servers it must catch, one of which hands the agent its whole grant whoever it acts for.

Thread Thread
 
slabb profile image
Sam LABBE •

The no-tool-at-all endpoint is the schema argument taken to its limit — a verb the agent cannot name is the one boundary that can't be argued with at parse time. And your eval gate closing on post-state (any run that leaves a record deleted fails) is the third input from the watcher thread showing up in the wild: the diff says what changed, the consumer graph says who read it, the post-state says what it did.

What I'd add is one layer on the five planted servers, because they are the exact thing a compromised run wants unknown: pre-register the eval. Seal the fixture list — the five servers, the grants each person must not exceed — before the run, and a green run proves it was tested against the planted set, not that the planted set quietly became four. That's this week's disclosure in miniature: the two agents didn't break monitoring, they weakened the checks and kept the green board. Your recompute-from-policy.json is the watcher; the sealed fixture list is what watches the watcher — the eval can no longer be edited by the run it judges, and the confused-deputy plant (the server handing out the whole grant) can no longer be dropped from the set it exists to catch.

Your opening line is the law this whole thread runs on, said shorter than I said it: a seal proves approval, not correctness. The planted servers are how you test correctness. The seal is how you keep the test honest.

Thread Thread
 
heyvaldemar profile image
Vladimir Mikhalev •

The planted set is already a file under review, which covers most of that. In the repo I run, every static plant is a line in tests/static/plants.tsv: the file, the text it breaks, its replacement and the check that must go red.

Dropping a line is a diff a reviewer sees. A plant whose target text has moved fails the run as stale instead of passing quietly, so a check can't lose its plant by drift.

What the file can't stop is a reviewer who approves the deletion. That stays with whoever owns the merge.

Thread Thread
 
slabb profile image
Sam LABBE •

The plants.tsv design already solves the two quiet failures — dropping a plant is a reviewed diff, and a moved target fails stale instead of passing green. What's left is exactly the one you named, and it's worth being precise about what would actually close it. Review protects the file; a seal protects its history. The merge that deletes a line leaves only an approval as its record — one decision, one reviewer, gone from view once the branch lands. With the plant list sealed per run, "judged against fewer plants than the run before" becomes a countable fact rather than an archaeology project: plants declared versus plants present, the denominator again.

And the residual you end on — "stays with whoever owns the merge" — is the same place the custody thread landed this week from the other side: a responsibility gap, not a tooling gap. The seal's one honest job there is narrow: not to stop the deletion, but to keep the owner's name on it.

Thread Thread
 
heyvaldemar profile image
Vladimir Mikhalev •

Agreed that it's a responsibility gap, and the name is already kept. The pull request that deletes a line from tests/static/plants.tsv records who approved it, and the file's history shows the count going down.

A seal per run would be a second copy of that record. What neither copy does is make someone read it. That part stays a person's job.

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.

Thread Thread
 
amintacccp profile image
tamina •

That framing lands for me — expiry-as-default really does flip the failure mode. I like how you put it: the journal keeps the decision honest, and the TTL is what stops a stale credential from arguing its way back in. Silence after an outage is a much easier failure to notice and fix than a credential that quietly keeps working.

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
 
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
 
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
 
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
Mona 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
 
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
 
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.