Skip to content

sambox: an agent serves only itself, over A2A, contracted statically - #361

Merged
aojea merged 3 commits into
google:mainfrom
aojea:samone
Sep 7, 2026
Merged

sambox: an agent serves only itself, over A2A, contracted statically#361
aojea merged 3 commits into
google:mainfrom
aojea:samone

Conversation

@aojea

@aojea aojea commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Runtime service declaration is gone from the sandbox boundary. The bundle now contracts at most one thing - the agent's own a2a service name and the sandbox port it must bind (like $PORT on a serverless runtime) - and the gateway routes it from startup, never talking to the node. The operator declares the a2a:// service in the node's configuration with the gateway's stable --ingress-listen address as its backend.

Everything that made runtime declaration feel necessary lives at its proper layer instead: capabilities and dynamic behaviour are the agent card and A2A traffic inside the route; readiness is the node's new card-fetch probe (an A2A agent is up exactly when it serves its card), so a declared-but-unbound sandbox stays out of discovery and a moved agent re-points via probe-gated advertisement rather than re-registration. Tools (mcp://) and models (inference://) remain operator node services, never agent ingress.

The CUJ now runs the stock a2a-go SDK on both ends: an unmodified agent server in the sandbox and an unmodified client bootstrapping from the mesh-regenerated card.

Runtime service declaration is gone from the sandbox boundary. The bundle
now contracts at most one thing - the agent's own a2a service name and
the sandbox port it must bind (like $PORT on a serverless runtime) - and
the gateway routes it from startup, never talking to the node. The
operator declares the a2a:// service in the node's configuration with the
gateway's stable --ingress-listen address as its backend.

Everything that made runtime declaration feel necessary lives at its
proper layer instead: capabilities and dynamic behaviour are the agent
card and A2A traffic inside the route; readiness is the node's new
card-fetch probe (an A2A agent is up exactly when it serves its card), so
a declared-but-unbound sandbox stays out of discovery and a moved agent
re-points via probe-gated advertisement rather than re-registration.
Tools (mcp://) and models (inference://) remain operator node services,
never agent ingress.

The CUJ now runs the stock a2a-go SDK on both ends: an unmodified agent
server in the sandbox and an unmodified client bootstrapping from the
mesh-regenerated card.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the agent ingress architecture by shifting from dynamic service registration via an agent-facing /ingress endpoint to a static, contract-based model. The AgentBundle now explicitly contracts a single A2A service name and port under the Serves field, and the IngressManager starts serving this ingress at startup. Additionally, a Probe method has been introduced to A2AService to verify backend readiness by fetching its agent card. Feedback on the changes suggests avoiding the use of http.DefaultClient in the probe logic to prevent potential resource leaks from missing timeouts, recommending a custom client with a configured timeout instead.

if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using http.DefaultClient is generally discouraged in production because it has no default timeout. If the passed ctx does not have a deadline, the request could block indefinitely, leading to resource leaks. Consider using a custom http.Client with a configured timeout (e.g., 10 seconds) as a safeguard.

Suggested change
resp, err := http.DefaultClient.Do(req)
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Do(req)

…time

Remove the /sam/service/register and /sam/service/unregister endpoints:
anything a caller can mutate at runtime is an interface it can abuse, and
every legitimate use was a declaration in disguise. Services now only
exist by declaration at startup — the node's --config services block, or
the FFI start configuration on mobile — and the probe gates advertisement,
so readiness is observed rather than asserted.

The former endpoint paths fall through to the egress proxy like any other
/sam/ path, with no special handling; the sandbox boundary still refuses
them before the node is ever reached, which the boundary tests keep
pinning. Validation moves into SamNode.RegisterService, the one funnel the
config and FFI paths share.

Tests, harnesses, the mobile app, the Cloud Run example and the
architecture doc's ingress section all migrate to declaring backends
before their node starts. Runtime-written config files are chmod 644: the
node container runs as a non-root user and a restrictive umask would make
the mounted config unreadable inside it.
@aojea
aojea merged commit 09e6e9e into google:main Sep 7, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant