sambox: an agent serves only itself, over A2A, contracted statically - #361
Conversation
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
| 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.
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.