pub struct ContainerRuntime { /* private fields */ }Expand description
An AgentRuntime that runs each agent in its own container via a
docker/podman CLI (shelled out through tokio::process, so no Docker
API dependency — the engine binary is the only requirement).
One container per agent, named a2a-agent-<id>. The engine is the source of
truth for liveness (inspect); the in-memory map only remembers each agent’s
published port so health can probe its card.
Images: an agent runs from the base image
(with_base_image) unless its config names its own
with [runtime] image, which is how an agent whose handler no TOML can
express gets deployed like any other. Either way the container gets the same
contract — the config bind-mounted at /etc/agent.toml and named by
A2A_CONFIG, HOST=0.0.0.0, the configured port published, and the
allowlisted variables passed through — but only the base image is given a
command (a2a run --config …). An agent’s own image boots by its own
ENTRYPOINT.
Binding: the in-container agent must bind 0.0.0.0 to be reachable
through the published port. The base image sets HOST=0.0.0.0 and this
adapter passes -e HOST=0.0.0.0; since the config’s default_host reads
HOST, a config that omits host binds all interfaces. A config that
hard-codes host = "127.0.0.1" will not be reachable.
Secrets: keep them out of the TOML as ${VAR} refs. Each referenced
variable — provided it is on the EnvAllowlist — is injected into the
container as a value-less -e VAR pass-through, resolved from this
process’s environment by the engine CLI. So the bind-mounted TOML and the
docker create argv never carry secret values, and the in-container
a2a run expands the refs at startup. The deploying process (e.g.
a2a control-plane) must therefore hold the secrets in its own env;
provisioning fails fast if a ref has no value and no ${VAR:-default}.
The allowlist is deny-by-default (with_allowed_env):
without it, any accepted config could name any variable this process holds
and have it expanded into the agent’s card for anyone to read back.
Hardening: every agent is created with capabilities dropped, privilege
escalation blocked, a process cap, and — where its storage permits — a
read-only root filesystem. See ContainerHardening for what is on by
default and why resource ceilings are not. Two consequences worth knowing:
an agent cannot bind a port below 1024 (the image runs as a non-root user
anyway), and a handler that writes outside /tmp needs either sqlx
storage or a relaxed policy.
Platform: the config is bind-mounted (-v host:container), so host config
paths must be expressible as a Docker volume source — works on Linux/macOS;
Windows host paths need conversion (out of scope here).
Implementations§
Source§impl ContainerRuntime
impl ContainerRuntime
Sourcepub fn with_engine(engine: impl Into<String>) -> Self
pub fn with_engine(engine: impl Into<String>) -> Self
Use a specific engine binary ("docker" or "podman").
Sourcepub fn with_hardening(self, hardening: ContainerHardening) -> Self
pub fn with_hardening(self, hardening: ContainerHardening) -> Self
Replace the hardening policy applied to every agent this runtime
provisions (default: ContainerHardening::default).
Sourcepub fn with_base_image(self, image: impl Into<String>) -> Self
pub fn with_base_image(self, image: impl Into<String>) -> Self
Override the base image (default [DEFAULT_IMAGE]).
This is the image for agents that do not name one. An agent whose config
sets [runtime] image runs from that instead, whatever this is set to.
Sourcepub fn with_allowed_env(self, allowed: EnvAllowlist) -> Self
pub fn with_allowed_env(self, allowed: EnvAllowlist) -> Self
Permit deployed configs to reference these host environment variables.
Anything not listed is rejected at provision.
Trait Implementations§
Source§impl AgentRuntime for ContainerRuntime
impl AgentRuntime for ContainerRuntime
Source§fn recover<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Recovered<AgentId>, RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn recover<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Recovered<AgentId>, RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
Rebuild the id → port map from the engine, which kept running while this process was not.
ps -a on purpose: a stopped-but-existing container is still an agent
this runtime manages, and adopting it is what makes stop/undeploy
work on it after a restart instead of returning NotFound.
Source§fn logs<'life0, 'life1, 'async_trait>(
&'life0 self,
id: &'life1 AgentId,
tail: Option<usize>,
) -> Pin<Box<dyn Future<Output = Result<Vec<String>, RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn logs<'life0, 'life1, 'async_trait>(
&'life0 self,
id: &'life1 AgentId,
tail: Option<usize>,
) -> Pin<Box<dyn Future<Output = Result<Vec<String>, RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Replay the container’s output via the engine, which retains it for the container’s whole life — including after it exits, which is exactly when the log matters most.
Source§fn provision<'life0, 'async_trait>(
&'life0 self,
spec: AgentSpec,
) -> Pin<Box<dyn Future<Output = Result<AgentId, RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn provision<'life0, 'async_trait>(
&'life0 self,
spec: AgentSpec,
) -> Pin<Box<dyn Future<Output = Result<AgentId, RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
AgentId.Source§fn start<'life0, 'life1, 'async_trait>(
&'life0 self,
id: &'life1 AgentId,
) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn start<'life0, 'life1, 'async_trait>(
&'life0 self,
id: &'life1 AgentId,
) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
RuntimeError::AlreadyRunning if it is
already live, RuntimeError::NotFound if it was never provisioned.Source§fn stop<'life0, 'life1, 'async_trait>(
&'life0 self,
id: &'life1 AgentId,
) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn stop<'life0, 'life1, 'async_trait>(
&'life0 self,
id: &'life1 AgentId,
) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Ok. RuntimeError::NotFound if it was never provisioned.Source§fn health<'life0, 'life1, 'async_trait>(
&'life0 self,
id: &'life1 AgentId,
) -> Pin<Box<dyn Future<Output = Result<RuntimeHealth, RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn health<'life0, 'life1, 'async_trait>(
&'life0 self,
id: &'life1 AgentId,
) -> Pin<Box<dyn Future<Output = Result<RuntimeHealth, RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
RuntimeHealth.Source§impl Clone for ContainerRuntime
impl Clone for ContainerRuntime
Auto Trait Implementations§
impl !RefUnwindSafe for ContainerRuntime
impl !UnwindSafe for ContainerRuntime
impl Freeze for ContainerRuntime
impl Send for ContainerRuntime
impl Sync for ContainerRuntime
impl Unpin for ContainerRuntime
impl UnsafeUnpin for ContainerRuntime
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more