An agent that can only read your code is a fancy linter. Ask it to reproduce the bug from the ticket, run the migration and check what broke, or find out whether that query is really slow, and it needs the rest of the system: the database with real rows in it, the cache, the search index, the app that ties them together.
Running the agent somewhere is the solved half. The interesting half is what it can reach once it’s running, and what it costs when it gets something wrong.
The usual shape of the problem
On most platforms, your app and its services are separate things. The platform runs your code. The database, the queue, and the search index are independent services, sometimes from other providers, sometimes from the same one, each provisioned on its own and reached over the network with its own credential. It works, and the separation is often deliberate.
It does mean an agent starts with access to nothing. To make it useful you hand it credentials to the real services, and that’s the awkward part. A long-lived token in an environment variable is already a bad idea when a human holds it. It’s worse when the holder is an agent that can hallucinate.
There’s no single answer to that, only one mechanism per service: a connection string injected as an environment variable here, a short-lived scoped credential there for the providers that issue them, an access policy somewhere else again. Each is reasonable on its own, and the better ones do narrow what the agent can do and how long it can do it. You still assemble them service by service, and then maintain them.
A narrower credential limits what the agent can do. It doesn’t change where those actions land. A tighter credential pointed at production is still pointed at production.
Point it somewhere else, then, at staging instances rather than the live ones. Plenty of teams do exactly that, and it works. It’s also a chore that never ends. You need a non-production instance of every service the agent touches, the database and everything around it, and each provider has its own way of giving you one. Some branch a copy on demand. Others leave you running a second instance by hand. Then you keep every schema in step with production, seed each one with data realistic enough to be worth testing against, and repeat the exercise the next time someone adds a service. Miss one, and the agent is quietly back on that service’s production instance. Nothing warns you, because everything keeps working, right up until it doesn’t. What you maintain isn’t a copy of your system, it’s a set of separate copies of its parts, each drifting at its own speed.
There’s another way to arrange this, and it starts one level down.
The environment is the unit
On Upsun, a project isn’t only your application. It’s your applications and your services, described together in one .upsun/config.yaml and deployed together into one cluster. The Postgres instance, the Redis instance, the OpenSearch index, and the three apps that talk to them are one deployable thing.
Because the whole stack is one unit, it copies as one unit. Branch an environment and you get all of it: the code at that commit, the services, and the data inside them, cloned copy-on-write and running in isolation from the parent. That’s the mechanism behind preview environments, and it predates anyone’s interest in agents by years. It exists so a reviewer can click a link on a pull request and see a feature working against real data.
It turns out that’s roughly what an agent wants too.
A task is a container inside that environment
A task is an on-demand, run-to-completion container. You declare it alongside your apps and services, it’s injected into the environment’s cluster when triggered, it runs one command, and it’s removed when that command exits.
The relationships block is where the interesting bit lives. Inside the task container, those connections appear in PLATFORM_RELATIONSHIPS exactly as they do for an application. The agent reads its database credentials the same way myapp reads them, from the same place, in the same format.
There was no token to hand out, because there’s nothing outside the environment to hand one out for. The database sits in the environment and is reachable only from inside it: services have no public IP and no inbound route from the internet, so there’s no front door to knock on from anywhere else. Access inside the cluster isn’t automatic either. If a relationship isn’t declared, the network route between those two containers doesn’t exist. And the credentials the agent reads were generated for this environment when it was created, so they don’t work anywhere else. The agent inherits its access the way every other container does, by being in the environment and being declared.
Triggering one without handing out a key
Tasks run from the Console, from the CLI with upsun task:run myagent, or through the API. The third case is the interesting one, where your application triggers the agent itself.
That could have required an API token stored in the app. It doesn’t. You declare what the app is allowed to do with workload authorizations:
At runtime the app asks a local proxy for a short-lived token carrying exactly those permissions. It’s the same principle as scoped credentials everywhere else, applied to the trigger rather than to the data, and the permission is declared in the config file next to the code it governs.
Each run creates an activity, the same object a deploy or a backup creates, so an agent run has an ID, a live status, streamed logs, and a cancel endpoint. Whatever you already use to watch a deploy works on an agent.
The blast radius is one environment
Now put the two halves together. Run the task in a branched environment, and the agent has a complete, realistic system to act in, where the worst outcome is a broken copy.
That changes which permissions are worth arguing about. You don’t have to hold a meeting about whether the agent should be allowed to write to the database, because the database is disposable. Let it write. Let it run the migration, drop the table, fill the cache with nonsense. If it goes badly, delete the environment and branch a fresh one, and you’re back to a known-good full stack in the time it takes to clone.
An agent that can only read is limited to suggesting things. An agent that can write, somewhere writing is cheap, can check its own work: apply the migration, run the suite, look at the result, try again. Being able to be wrong safely is most of what makes an agent useful.
What this doesn’t solve
Three caveats, because none of this is magic.
A clone of production contains production data. The isolation is real, and the environment can’t touch production, but the rows in it are still your customers’ rows. If the agent writes them to a log, a report, or a model provider’s API, you’ve moved production data somewhere new. Upsun supports sanitizing databases in a deploy hook so preview environments carry realistic but scrubbed data, and that matters considerably more once something autonomous is reading them. It’s opt-in. Turn it on.
Tighter isolation is configuration you add. Every Upsun container is already rootless, namespace-isolated, syscall-filtered, and running from a read-only application filesystem, with nothing to switch on. That’s what keeps containers out of each other. It’s not the same as restricting the agent’s own process within its container, which is the layer that matters when the agent reads content you didn’t write. Stripping environment variables, cutting network access, and giving the agent’s shell a read-only filesystem are all standard Linux tools, covered step by step in sandboxing AI agents on Upsun.
External services are still external. The environment gives the agent your stack. It doesn’t give it the third-party APIs your application calls, and for those you’re back to handing out credentials, with the usual answers: scope them narrowly, keep them short-lived, revoke them centrally. The difference is scope, because for most of what an agent does day to day, the systems it needs are the ones already in the environment.
The takeaway
The interesting question about agent infrastructure isn’t where the process runs. Ephemeral compute is a solved problem, and everyone is converging on the same answer, Upsun included. Tasks are a good execution primitive precisely because they’re boring: a container and a command.
The question that matters is what the process can reach, and how much it costs when it’s wrong. Answer that with credentials and you spend your time narrowing scopes, because every action still lands on the live system. Answer it with an environment and the question mostly stops being about permissions. The agent gets full access to a complete system, and the system is one you can throw away.
Environment cloning wasn’t built for agents. It was built so a reviewer could look at a pull request running against real data. The property that made it good for review, a complete stack that’s safe to break, is the same one that makes it good for something that will definitely break it. Last modified on September 8, 2026