While you run agents locally, credentials rarely come to the surface. An API key sits in your machine's environment variables and the agent just reads it. You know where the key is, and closing the laptop stops the run.
The first day I offloaded a task to the cloud via the Managed Agents API, I realized that comfort lived only on my desk. I went to pour a production deploy token into an environment variable for a cloud-run agent, the same way I always had, and my hands stopped.
That token would leave my machine, be used in a place I couldn't see, at a time I wasn't watching. If something leaked mid-flight, I couldn't quickly stop how far the damage reached or how long it lasted.
The essence of offloading an agent to the cloud is that execution leaves your hands. If so, the credentials have to be remade into a form that is safe once it leaves your hands too.
The fear of a long-lived token leaving your desk
Most credentials we use day to day are built to live a long time. Issue one, and it's valid until you explicitly revoke it. On the assumption that only you use it locally, that's fine.
The problem is that this property is a poor fit for cloud execution.
A long-lived token has three weaknesses. First, a leak doesn't stop: it can be reused until revoked, so the later you notice, the wider the wound. Second, the privilege is too broad: it's tempting to hand over a strong key "so it just works," and it reaches resources the agent never needed to touch. Third, you lose the trail: reuse the same token across runs and you can no longer separate which run did what.
Locally, all three stay latent. The blast radius is confined to your machine, broad privilege is an extension of your own actions, and your memory fills in the trail. But in the cloud, all three become operational risk as-is.
So the design stance is simple. Keep the long-lived strong key on your desk, and hand the cloud a different credential that lives briefly and reaches narrowly. That's the whole of it.
Issue per run, throw away when done
The first pillar is matching the token's lifetime to the run's lifetime.
Before starting an agent, issue a token meant only for that run. When the task ends, success or failure, revoke it. The next run receives a fresh token. With this in place, even if a token leaks mid-run, it is valid only for the short window until that run finishes.
It takes the shape of inserting a broker. Only the broker layer holds the strong key on your desk; the agent receives only the short-lived token the broker issued.
# token_broker.py — a broker that issues and revokes short-lived tokens per run
import time
import secrets
from dataclasses import dataclass
@dataclass
class LeasedToken:
value: str
scope: tuple[str, ...] # only the actions this run is allowed
expires_at: float
run_id: str
class TokenBroker:
"""Hide the strong key inside; lend out only short-lived, narrow tokens."""
def __init__(self, master_key: str, default_ttl: int = 600):
self._master_key = master_key # never leaves the desk
self._default_ttl = default_ttl
self._active: dict[str, LeasedToken] = {}
def issue(self, run_id: str, scope: tuple[str, ...], ttl: int | None = None) -> LeasedToken:
ttl = ttl or self._default_ttl
token = LeasedToken(
value=f"agt_{secrets.token_urlsafe(24)}",
scope=scope,
expires_at=time.time() + ttl,
run_id=run_id,
)
self._active[token.value] = token
return token
def authorize(self, token_value: str, action: str) -> bool:
token = self._active.get(token_value)
if token is None:
return False
if time.time() > token.expires_at:
self.revoke(token_value) # expired means immediately invalid
return False
return action in token.scope # reject anything outside the granted scope
def revoke(self, token_value: str) -> None:
self._active.pop(token_value, None)
# Usage
broker = TokenBroker(master_key="hide the strong production key here")
def run_agent_task(run_id: str):
# pass only the actions this run needs
token = broker.issue(run_id, scope=("read:articles", "write:draft"), ttl=300)
try:
dispatch_to_cloud_agent(run_id, token.value) # the agent gets only the short-lived token
finally:
broker.revoke(token.value) # always revoke, success or failureThe key point is that revoke always runs in finally. Whether it crashes on an exception or finishes cleanly, the token dies at the end of the run. The short ttl is a second layer of insurance: even if the revocation itself never runs, the token stops working on its own once time runs out.
The agent-side code doesn't even need to be aware that its token is short-lived. It uses it as an ordinary token, and it naturally goes invalid when the run ends. The complexity is sealed inside the broker.