Skip to content

fix(a2a): agents reach their own staging over loopback - #98

Merged
earakely-scale merged 2 commits into
mainfrom
edgararakelyan/agent-own-staging
Oct 7, 2026
Merged

earakely-scale merged 2 commits into
mainfrom
edgararakelyan/agent-own-staging

Conversation

@earakely-scale

@earakely-scale earakely-scale commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

When the object store's grants can't reach an agent, agent-env stages the agent's objects on the agent's own server and hands it grants that name paths on the agent's public URL. A sandbox can't always call its own public URL. On one provider, the in-sandbox route to the sandbox's public hostname serves a certificate that doesn't verify there, so every staged transfer failed with transfer_unavailable, including the solver's and the judge's trajectories.

The change

  • Core (a2a_agent/staging.py): each staged grant (read, write and upload policy) also carries an AgentEnv-Staging-Path header. Its value is the path at which the agent's own server serves the URL (/ext/staging/<id>/<n>), taken from the card's staging endpoint.
  • Protocol (transfers.py, a2a_agent/framework.py): upload, download and NamespaceUploader send such a request to http://127.0.0.1:{port}<path> first, and use the grant's URL only when nothing listens there.
    • The port is the one the SDK's server received the request on; it marks each request with transfers.serving_on.
    • Failing that, the port is A2A_PORT. agent-env sets it on agents it deploys from an image, and it also covers a separate process, such as a hook.
    • transfers.loopback_url(url, headers) returns the same URL for agents with transfer code of their own.

Loopback reaches the agent's server, not its staging directory. So it works from any process in the sandbox, and the server remains the only owner of its staging: byte counts, If-Match and atomic commits are unchanged.

Compatibility

There are no wire-model or card changes.

core agent a staged transfer goes
new new over loopback, or to the public URL when nothing listens there
new old to the public URL, as today (the header is sent and ignored)
old new to the public URL, as today (no header)

Not covered

  • A file part staged for a prompt is a bare URL with no header, and the agent reads it with whatever tool it uses. It still goes through the public URL.
  • An agent with its own transfer code uses the public URL until it adopts loopback_url.

Verification

check result
unit tiers (tst/unit, protocol tests) 6448 passed, 13 skipped
staging_test.py: every staging flow (skill bundle, trajectory, snapshot save and load, changelog upload, drain and apply), run a second time in a sandbox that refuses the agent's calls to its own public URL All pass. Transfers made while serving a request go to loopback on the port the request came in on; uploads the tests make outside a request, as a hook would, go to A2A_PORT. An agent that doesn't use loopback fails the same calls with the new hint.
protocol tests Loopback for PUT, GET and namespace POST. When loopback refuses the connection, the helpers fall back to the grant's URL and send the same bytes. A loopback answer (404) is final. The header is ignored when no port is known or when it isn't the URL's path suffix. An SDK server reaches its staging on the port it received the request on, even when A2A_PORT names another.
real sockets (uvicorn servers, no in-process transport) 8/8. Every grant names an unresolvable public host, so any success came over loopback. A server started without A2A_PORT stages a 64 MiB in-request upload. A separate process reaches it through A2A_PORT for namespace POSTs and a download. A closed loopback port falls back to the grant URL and fails cleanly in 2 s. 1 GiB moves at about 250 MiB/s with 92 MiB client RSS. 32 concurrent uploads and 32 downloads all arrive intact. A full staging (413) is final, with no fallback. A server SIGKILLed mid-upload fails the transfer 2 s later instead of hanging, and a restarted server is reached again.
live, on the provider that refuses the self-call: a five-step eval task (multi env, data load, agent, prompt, rubric judge) run from bundles with a local object store, on agent images built with this protocol Every run passed with score 1.0, and the solver's and the judge's trajectories all moved through staging into the local store. That covers both commits (two runs), a variant with local env, data and agent, and one with a local solver and a registry judge; three of these ran concurrently. In the agent's own container log, the trajectory upload goes to http://127.0.0.1:8000 and the server records the request from 127.0.0.1, while agent-env's calls arrive through the ingress. Run alongside them, an agent on the released protocol still fails at the solver's trajectory, now with the new hint.

🤖 Generated with Claude Code

RetriggerConfidence Score: 5/5

The PR appears safe to merge; the previously reported installed-agent failure is fixed.

What we checked:

  • Installed agents missing a port: Yes. The SDK wraps each HTTP request with its serving port, and transfers use that port before checking A2A_PORT.

Summary

SDK agents can now reach their own staged objects over loopback, rather than relying on calls to their public hostname. The grant URL remains a fallback when no server accepts the loopback connection.

  • Agents reach their own staged objects over loopback.
Diagram
sequenceDiagram
    participant Env as AgentEnv
    participant Agent as Agent SDK
    participant Stage as Local staging
    Env->>Agent: Send grant with staging path
    Agent->>Agent: Read current request port
    Agent->>Stage: Transfer over loopback
    Stage-->>Agent: Return staged object
Loading

Reviews (2) · Last reviewed commit: "fix(a2a): the SDK's server reaches its s..." · Reviewed by Greptile

A staged grant names a path on the agent's own public URL, and a sandbox can't always
call that URL: on one provider the in-sandbox route to the sandbox's public hostname
serves a certificate that doesn't verify there, so every staged transfer failed.

Core's staged grants now also carry an AgentEnv-Staging-Path header, the path the
agent's own server serves the URL at. The protocol's transfer helpers send such a
request to http://127.0.0.1:$A2A_PORT<path> first, and to the grant's URL only when
nothing listens there; transfers.loopback_url gives the same URL to agents with
transfer code of their own. Agents and cores on either side of the change still work
together: a grant without the header, or a helper that ignores it, uses the grant's URL.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@earakely-scale
earakely-scale requested a review from a team as a code owner October 7, 2026 16:35
Comment thread packages/agentenv-protocol/src/agentenv_protocol/transfers.py Outdated
…e request on

A2A_PORT reaches an agent's server only when whatever starts it sets it: agent-env
does for an agent it deploys from an image, but an installed agent's own install
commands start it. The SDK's server now serves each request serving_on the port its
socket took it on, and loopback_url prefers that port, so an SDK agent reaches its
staging over loopback however it was started. A2A_PORT stays the port for transfers
made outside a request, as from a hook process.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@earakely-scale
earakely-scale merged commit 1c11e2f into main Oct 7, 2026
16 checks passed
@earakely-scale
earakely-scale deleted the edgararakelyan/agent-own-staging branch October 7, 2026 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant