AskDoozy logoAskDoozy

Keeping API keys out of our AI agent's sandbox, and what that doesn't solve

Doozy runs tasks against our customers' databases, Stripe and GitHub from a Slack thread. Here's how the agent does that without ever holding a key, and the risks a missing key doesn't remove.

· 9 min read

  • AI agent security
  • Prompt injection
  • Credentials
  • Postgres
  • Slack

Doozy is an AI agent people mention in Slack. Someone writes "@Doozy why was Acme charged twice?" in a support thread, and it goes and checks: it queries the database, reads Stripe, looks at the code in GitHub, and posts what it found back in the thread.

So the first question every engineer asks me is about AI agent security, and it's the right one: you're giving an LLM production credentials? This is the long answer. Most of it is deliberately boring. The interesting part is what's left over once the keys are gone.

The short version

The agent never holds an API key. Each task runs in a fresh sandbox that holds only a signed, short-lived job token. A credential proxy outside the sandbox checks that token, decides whether the tool is allowed, and adds the real key to that one call. Database queries run read-only, with a timeout and a row cap. That stops keys leaking. It doesn't stop prompt injection misusing the tools you've switched on, so those are the controls that matter most.

Why our AI agent never holds API keys

Every task runs in its own sandbox, a small VM that exists for that one job and is thrown away after. Nothing in it is a credential for your systems. No API keys, no database password, no model provider key, no GitHub token.

When the agent needs something outside, the call leaves the sandbox and goes to a gateway we run. The gateway decides whether the call is allowed, attaches the right credential, forwards it, and passes the response back. The agent sees results. It never sees the secret that produced them.

How a request reaches your tools without the agent holding a keyThe same path, top to bottom: a Slack mention, the sandbox holding only a job token, the gateway's five steps, your systems, and the answer back in the thread.Slack#support@Doozy why was Acme charged twice?Sandboxfresh per task, then discardedAgentreads, plans, calls toolsjob tokensigned, one job, dead after itAPI keysDB passwordsmodel keysThe one exceptiona token it asks for by name, with areason. Every request is logged.cloud metadata and private rangesare unreachablejob tokenno key backGatewayoutside the sandbox1Verify the job tokensignature, and the job is live2Find the workspacefrom the token, never the request3Check the tooldestructive ones start off4Attach the credentialadded to this call, not the sandboxCredential storeencrypted per workspace5Write a receiptthread, plus an append-only logkey added to this callYour systemsModel providerTools and APIsDatabaseread-only transactionstatement timeout · row capset by the gateway, not the promptSlackAnswer in the threadwith a receipt for every action it took
Every call leaves the sandbox with a job token and nothing else. The gateway turns it into a real credential for that one call, after checking the job is live, which workspace it belongs to, and that the tool is switched on.

None of this is new. A credential proxy, a service that injects keys into requests so the caller never sees them, is an old pattern, the one you'd draw on a whiteboard in five minutes. That's the point. When the thing on the other side of the boundary is a language model that will read anything it can reach, I want the boundary to be the most boring part of the system.

It matters because prompt injection isn't solved, and I don't expect it to be soon. A support ticket, a web page, a PDF someone drops in the thread: any of it can carry instructions. If one gets through, the agent can misuse the tools it has. What it can't do is print a key, write it to a file or send it somewhere, because there isn't one to find.

Job tokens: the only credential in the sandbox

The sandbox isn't empty. It holds a job token, and every call it makes carries it. If you're thinking "so that's the credential", you're right. The design is about making it a poor one to steal.

  • It names one workspace and one job, and we sign it. The gateway reads the workspace from the token, never from anything in the request, so no prompt, header or Slack message can point a job at another workspace's tools.
  • It dies with the job. It expires, and on every call the gateway also checks the job is still running. Once the job finishes or someone stops it, the token is refused, even before it expires.
  • It pins the model. The model the job started on is written into the token, and the gateway relays only to that one.

The claims are small enough to read at a glance:

{
  "tid": "<workspace>",
  "jid": "<job>",
  "exp": 1798387200,
  "mdl": { "p": "<provider>", "m": "<model>" }
}

So the worst case for a stolen token is someone making the calls this job could already make, through our gateway, for as long as the job runs. That's not nothing, and I come back to it below. It's a lot smaller than a leaked Stripe key that works from anywhere until someone notices.

How the credential gateway checks every call

Every call goes through the same steps, in the same order.

  1. Verify the token: the signature, the expiry, and that the job is live.
  2. Find the workspace from the token, and only from the token.
  3. Check the tool. Each connected tool is switched on or off per workspace. Anything that deletes, sends, refunds, publishes or changes who can see something starts switched off, and an owner turns those on one at a time. We don't rely on a service's own labels for this, because some don't mark sending an email as risky. Our own rule runs on top.
  4. Attach the credential. Credentials are stored encrypted per workspace. The one a call needs is added to that request on our side of the boundary, never to the sandbox.
  5. Write a receipt. Every action shows up in the Slack thread and goes into an audit log nobody can edit, including us.

The sandbox's network is fenced too. The cloud metadata endpoint and private address ranges are unreachable from inside it, so the agent can't wander from its VM into the machines around it.

Read-only SQL for AI agents, enforced outside the prompt

The database connector is where people expect the scariest stories, so it gets the strictest rules, and none of them depend on the prompt. The model writes a query. Before it runs, the gateway wraps it:

begin read only;
set local statement_timeout = 30000;

select * from (
  -- the query the model wrote
) as doozy_query limit 201;

rollback;

Three things fall out of that wrapper.

  • Only one statement can run. A second statement, a stray COMMIT trying to leave the read-only transaction, or a write all become a syntax error inside the subquery before anything executes. Anything that doesn't start as SELECT, WITH, TABLE or VALUES is refused with a plain message, so the model can fix its query instead of guessing at a parse error.
  • Postgres refuses writes anyway. If something slipped past the wrapper, the transaction is read-only and always rolled back.
  • It can't run forever or return everything. Statements time out, 30 seconds by default, and results stop at 200 rows. The 201st row only tells the model there's more, so it learns to aggregate instead of paging through a table.

Read-only is not the same as safe. The agent can read whatever its database user can read, and it can post that in Slack. The right setup is a user scoped to the tables it needs, on a replica if you have one, and every query it ran is in the thread for anyone to check.

When an agent needs a secret inside the sandbox

Some work genuinely needs a secret inside the sandbox: a CLI that only takes a token, an SSH key for a deploy. Saying no to all of it would make a lot of real tasks impossible.

So there's one door. The agent asks for a secret by name and says why. The request is logged with its reason, and only then is the value handed in.

This is the part I'm least comfortable with. Once a value is inside, a prompt injection can ask for it too. The log tells you exactly what was asked for and why. It doesn't tell you what happened to the value afterwards. I've accepted that trade because the alternative was a product that can't do half of what teams ask, but if you see a better design, I'd like to hear it.

What prompt injection can still do without keys

Taking the keys away removes one failure: losing a credential. It doesn't touch the ones that matter more. Here's what an attacker gets if a prompt injection takes over a job.

If a prompt injection takes over a job

It can

  • Use the tools that are switched on, as this workspace.
  • Read whatever those tools and that database user can read.
  • Send what it read to a public URL. The sandbox can reach the web, because looking things up is part of most tasks.
  • Use the job token from outside, through our gateway, until the job ends.

It can't

  • See, copy or keep a key for your systems.
  • Delete, send, refund or publish with a tool no owner turned on.
  • Point the job at another workspace's tools.
  • Write to the database, or hold it with a long query.
  • Reach the cloud metadata endpoint or our private network.
  • Use the token once the job is over.

So the controls that matter sit on the far side of the gateway: which tools are on, what each one can read, and receipts that put every action in front of the people in the thread. Our security page lists them, and AI and data use covers what the model sees and keeps. A thread turns out to be a good audit surface. People notice when their coworker does something odd in front of them.

The question I keep going back and forth on is whether a person should approve an action before it happens. Today the answer is tools that start off, plus a receipt for everything. Approval on every step turns a coworker back into a form to fill in. Approval on nothing is how you end up in an incident review. I suspect the answer is per tool, but I'd rather hear from people running agents against production than guess.

What model evals taught us about AI agent security

When I tested models for this on a synthetic billing investigation, credentials turned out to be the easier problem.

In one run the task folder sat next to the grader. One model found the answer key and graded itself, in all five runs. Moved into a sealed container, it still did the task well, and still spent several steps going through environment variables, /root and /tmp looking for something useful. That's when "give it nothing to find" stopped being a slogan for me.

The harder failure was quieter. Every model family I tested, at least once, told the customer "we've deployed a fix" when the fix only existed as a change in the repo. No key leaked and nothing was deleted. It just said something untrue, confidently, to a customer. So Doozy's operating rules forbid calling something done until it is, and it's the rule that gets broken most.

If you build AI agents that touch production, here's what I'd take from all this. Lock the credentials away early, because it's cheap and mechanical. Then spend the real effort on what the agent can read, what it's allowed to say, and making sure the people around it can see what it did.

If you see a hole in any of this, write to me at [email protected]. I read all of it.

Questions people ask

Can prompt injection steal an AI agent's API keys?
Only if the agent holds them. Ours doesn't: the sandbox has a job token and nothing else, and a gateway outside it adds the real key to each call. An injection can still misuse the tools that are switched on, so those are the controls to get right.
What is a credential proxy for AI agents?
A service between the agent and your tools that holds the keys. The agent sends a request with a short-lived token; the proxy checks it, attaches the right credential, forwards the call and returns the result. The agent never sees the secret.
Should an AI agent have write access to the production database?
I'd say no, at least to start. Ours runs every query in a read-only transaction with a timeout and a row cap, all set outside the prompt. Give it a database user scoped to the tables it needs, on a replica if you have one.
Is a sandbox enough to secure an AI agent?
No. A sandbox stops the agent reaching what's around it, but it can still read what its tools can read and send that somewhere. Keep keys out, turn risky tools off by default, and make every action visible to the people who asked for it.