Repository navigation
feat(protocol): SDK agents serve a staging extension agent-env can move objects through - #39
Conversation
…ve objects through When an object store's grants cannot reach an agent, as with a local store and an agent on a remote sandbox, agent-env needs another way to hand the agent its objects and collect what it writes. Every SDK agent now serves urn:agentenv:staging/v1 at /ext/staging: a small object store on the agent's own server that agent-env pushes into before a call and pulls from after it. The grants it sends stay ordinary HTTPS grants naming staged paths on the agent's own URL, so extension handlers and the transfer helpers are unchanged. - PUT/GET/DELETE by path, multipart POST under a prefix (the upload-policy shape), and GET/DELETE of a prefix to list or clear it. - Writes land in a staging file and are renamed into place; each write gets a fresh ETag, and DELETE with If-Match spares an object rewritten since it was read. - A path's first segment must be a long, unguessable id; everything staged counts against AGENTENV_STAGING_MAX_BYTES (16 GiB; 0 turns staging off). - python-multipart joins the agent extra for the upload forms. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…move objects through grant URLs themselves Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| used = await self._usage() | ||
| try: | ||
| replaced = target.stat().st_size if target.is_file() else 0 | ||
| target.parent.mkdir(parents=True, exist_ok=True) |
There was a problem hiding this comment.
If the agent runs on a multiuser host with a typical umask, these directories let another local user list the IDs meant to keep staged files private. That user can then use an ID with the public GET route to read a file. Create the staging directories so other users cannot list them.
How this was verified: Default directory creation does not restrict access, while the public read route checks only the path’s ID length.
Knowledge Base Used: A2A agent runtime
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/agentenv-protocol/src/agentenv_protocol/a2a_agent/staging.py
Line: 223
Comment:
**Staging IDs become visible**
If the agent runs on a multiuser host with a typical umask, these directories let another local user list the IDs meant to keep staged files private. That user can then use an ID with the public `GET` route to read a file. Create the staging directories so other users cannot list them.
**How this was verified:** Default directory creation does not restrict access, while the public read route checks only the path’s ID length.
**Knowledge Base Used:** [A2A agent runtime](https://app.greptile.com/scale-ai/-/custom-context/knowledge-base/scaleapi/agentenv-framework/-/docs/a2a-agent-runtime.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Fixed in ff1d3d4: the directories staging makes (objects/, incoming/, and the per-process default root from mkdtemp) are 0700. A directory given with AGENTENV_STAGING_DIR is used as it is, since chmodding a shared path like /tmp would be worse.
| self._objects = self.root / "objects" | ||
| self._incoming = self.root / "incoming" | ||
| self._lock = asyncio.Lock() | ||
| self._used: int | None = None # stored plus reserved bytes, counted on first use | ||
| self._last_stamp = 0 |
There was a problem hiding this comment.
Shared stores lose coordination
If two app instances or workers use the same staging directory, each has its own lock and byte count. Neither sees the other’s pending writes, so together they can exceed AGENTENV_STAGING_MAX_BYTES. Another instance can also rewrite a file between an If-Match check and deletion, causing the newer file to be deleted. Coordinate stores that share a directory, or give each one its own directory.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/agentenv-protocol/src/agentenv_protocol/a2a_agent/staging.py
Line: 104-108
Comment:
**Shared stores lose coordination**
If two app instances or workers use the same staging directory, each has its own lock and byte count. Neither sees the other’s pending writes, so together they can exceed `AGENTENV_STAGING_MAX_BYTES`. Another instance can also rewrite a file between an `If-Match` check and deletion, causing the newer file to be deleted. Coordinate stores that share a directory, or give each one its own directory.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Documented in ff1d3d4 rather than coordinated: one server process owns a staging directory. An A2A agent is single-process already (the A2A SDK's task store is in memory), and without AGENTENV_STAGING_DIR each server now gets a directory of its own, so two never share one by default.
…its own, and leaves agents its paths when off - The directories holding staged objects are made 0700, so another user on the host can't list the ids that keep them private. A directory given with AGENTENV_STAGING_DIR is used as it is; only the ones staging makes change. - Without AGENTENV_STAGING_DIR each server stages in a directory of its own, made on first use, so a restarted server never counts what an earlier one left against its limit. One server process owns a staging directory: the byte count and the If-Match check are its own. - With staging off, an agent's own route below /ext/staging is no conflict. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| if self._root is None: | ||
| self._root = Path(tempfile.mkdtemp(prefix="agentenv-staging-")) |
There was a problem hiding this comment.
When AGENTENV_STAGING_DIR is unset, each server run creates a new temporary staging directory, but nothing removes it when the server stops. Staged files pile up across restarts, using disk space and keeping data longer than the documentation promises. Remove the directory when the server shuts down.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/agentenv-protocol/src/agentenv_protocol/a2a_agent/staging.py
Line: 114-115
Comment:
**Staged files survive restarts**
When `AGENTENV_STAGING_DIR` is unset, each server run creates a new temporary staging directory, but nothing removes it when the server stops. Staged files pile up across restarts, using disk space and keeping data longer than the documentation promises. Remove the directory when the server shuts down.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Fixed in 4caa425: the directory a server makes for itself is removed at exit (atexit); a directory given with AGENTENV_STAGING_DIR is left alone. A killed process can still leave one behind in the system temp dir, which the OS cleans. Test: test_a_servers_own_staging_directory_goes_when_it_exits.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…re left open Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| directory.mkdir(parents=True, exist_ok=True) | ||
| if directory.stat().st_mode & 0o077: | ||
| directory.chmod(0o700) |
There was a problem hiding this comment.
Shared directory becomes private
If AGENTENV_STAGING_DIR/objects or incoming is a symlink to a shared directory, _private follows it and changes that directory to 0700. A staging write can then lock other users out of files outside staging. Reject symlinked staging directories before changing their permissions.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/agentenv-protocol/src/agentenv_protocol/a2a_agent/staging.py
Line: 440-442
Comment:
**Shared directory becomes private**
If `AGENTENV_STAGING_DIR/objects` or `incoming` is a symlink to a shared directory, `_private` follows it and changes that directory to `0700`. A staging write can then lock other users out of files outside staging. Reject symlinked staging directories before changing their permissions.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
What and why
The local object store's grants reach only agents on the local machine. An agent on a remote sandbox (Modal, E2B, a plugin provider) can't reach them. For that agent, skill bundles, snapshots and changelogs were refused, and trajectories came back inline. The core change that fixes this needs a way to hand such an agent its objects without any connection back to the user's machine. This PR is the agent side: every SDK agent now serves a staging extension,
urn:agentenv:staging/v1. It is a small object store on the agent's own server at/ext/staging. Before a call, agent-env pushes into it what the agent will read. After the call, agent-env pulls out what the agent wrote.The grants agent-env sends stay ordinary
http-get/http-put/http-post-policygrants, with URLs that name staged paths on the agent's own URL. So no request or response model changes, and extension handlers and theupload/download/NamespaceUploaderhelpers are untouched. An agent that doesn't use the SDK can serve the same routes and add one card entry.PUT {path}GET {path}ETagPOST {prefix}fileat{prefix}/{key}, as an upload-policy grant is usedGET {prefix}/{objects: [{path, size_bytes, etag}]}DELETE {path}If-Match, only while it still has that tag, so an object rewritten since it was read survivesDELETE {prefix}/AGENTENV_STAGING_MAX_BYTES: 16 GiB by default, and0turns staging off and drops the card entry.AGENTENV_STAGING_DIR(default: a directory under the system temp dir).python-multipartjoins theagentextra, for the upload forms.transfers.redacting_request_urlspublic. agent-env now requests secret-bearing URLs itself (its pushes and pulls) and keeps them out of httpx's logs the same way the helpers do.This merges independently of the local-grant stack. Core starts using it in the follow-up PR, which is stacked on this one.
How it was tested
pytest packages/agentenv-protocol/tests: 301 passed.test_a2a_staging.pyis new. It covers:If-Match;upload,downloadandNamespaceUploaderround-tripping through the routes.pytest tst/unit: 5473 passed (core is unchanged by this PR).uv lock --checkis clean. The lock resolves from PyPI only.🤖 Generated with Claude Code
The PR appears safe to merge, though symlinked staging directories could change permissions on shared files.
Fix with agent prompt
Summary
SDK agents now host a staging store that lets AgentEnv move objects through the agent when object-store grants cannot reach a sandbox. The PR also makes the URL-redaction helper public for other grant callers.
redacting_request_urlsavailable to callers.Diagram
%%{init: {'theme': 'neutral'}}%% flowchart LR Core[agent-env] -->|push objects| Stage[Agent staging routes] Stage -->|read grants| Agent[Agent handler] Agent -->|write grants| Stage Stage -->|pull objects| CoreReviews (4) · Last reviewed commit: "fix(protocol): staging tightens its own ..."