aw-gateway is a configuration, orchestration, and access layer for
disposable or reusable container workspaces. It wraps Podman, Docker, Colima,
or Apple container with validated target definitions, lifecycle hooks,
readiness checks, in-container service supervision, generated SSH client
config, and an optional JSON HTTP API.
Users connect with familiar tools such as OpenSSH, SCP, SFTP, VS Code, the host CLI, or HTTP automation. The gateway starts or reuses the configured container, routes access to container-local services, and keeps host filesystem access behind explicit gateway paths and policy hooks.
- Why It Exists
- Features
- Install
- Quickstart
- Core Concepts
- Binaries
- Build
- Deployment Guides
- Configuration
- SSH Workflows
- Local Workstation Mode
- Container Agent Services
- Gateway Config Shape
- Launches (Named Command Templates)
- CLI
- HTTP API
- Assets
- Runtime Environment Contracts
- Template Variables
- Logging
- Lifecycle Diagrams
- Development
- Release
- Project Structure
Container runtimes already provide the isolation and process model. The purpose
of aw-gateway is to make those containers easier to configure, prepare,
access, and reuse as workspaces. It wraps Podman, Docker, Colima, or Apple
container with a validated config model, lifecycle hooks, readiness checks,
service supervision, generated SSH client config, and optional HTTP automation.
The main value is operational convenience and consistent access. Operators can describe targets, launches, identity, mounts, cleanup, and policy once in TOML instead of stitching together runtime commands, shell scripts, SSH config, and ad hoc status checks. Users get familiar access paths through OpenSSH, SCP, SFTP, VS Code, Codex, Claude Code, the host CLI, or the JSON HTTP API while the gateway handles container startup and routing.
This is especially useful for agent and build workspaces because the tools can run with normal local freedom inside a container while host access remains mediated by the configured gateway paths. Site policy can be expressed through configured lifecycle hooks, mounted bootstrap assets, supervised services, and host or container network controls without hard-coding those policies into the gateway binary.
- Host-side CLI for target lifecycle, status, launches, run commands, and client configuration.
- SSH-compatible attach for interactive shells, direct commands, SCP, SFTP, and gateway management actions.
- Optional JSON HTTP API for status, targets, readiness, launch, and run automation.
- Container lifecycle management for fixed and ephemeral targets.
- Runtime support for Podman, Docker, Colima, and Apple container.
- Config-driven lifecycle steps before container start and host steps after start, including health checks.
- Named launches for validated, discoverable command templates that prepare a ready target and then run a final in-container command.
- In-container service supervision with dependency ordering, restart policy, service health checks, and graceful shutdown.
- Built-in Unix-socket bridge from short runtime socket directories to container SSH.
- Generated SSH client configuration for SSH, SCP, and SFTP clients.
- Per-user default target selection.
- First-run identity-token generation and controlled forwarding to selected services.
- Optional idle cleanup that can stop a container or reap non-preserved processes after the last gateway session exits.
- Protocol-safe logging for proxy modes and rotating JSON logs for diagnostics.
Download the latest archive for your platform from GitHub Releases:
https://github.com/kcosr/aw-gateway/releases
Supported release platforms are currently:
linux-x86_64linux-arm64macos-arm64
Each archive contains binaries for one platform only. Extract the archive that
matches the host that will run aw-gateway; if the managed container or VM
runs a different Linux platform, also extract the Linux archive that matches
that container or VM. For example, a macOS arm64 host with a native Apple
Silicon Colima profile uses the macOS arm64 archive for bin/aw-gateway and
the Linux arm64 archive for the container-side runtime binaries. Install those
files using the deployment guide for your runtime:
For unsupported platforms or local development, build from source in the Build section.
For a working end-to-end deployment, pick the runtime guide that matches your host and follow it before using the README as a reference:
- Download and extract the latest release archive for your host platform and, when needed, the Linux archive for your container or VM platform.
- Pick a deployment guide: Podman, Docker, Colima, or Apple Container.
- Install the host and container-side runtime files using that guide's layout.
- Copy or adapt the guide's gateway config and validate it.
- Start a target with
aw-gateway --config <path> up <target> --json. - Generate client config and connect with OpenSSH, SCP, SFTP, or VS Code.
- A target is a named container configuration. Fixed targets reuse one container; ephemeral targets create one container per session id.
- A session is one gateway connection or invocation tracked for lifecycle, idle cleanup, and optional per-session workspace cleanup.
- A workspace is the host directory mounted or used as the session working
area. Ephemeral targets may use a workspace path that includes
{session_id}. lifecycle_stepsare host hooks tied to start/stop phases.host_stepsrun on the host after agent readiness.container_bootstrap_stepsrun inside the container before the agent starts.- OpenSSH
ForceCommandmakes host SSHD run the gateway instead of the user's shell. OpenSSHProxyCommandlets a local SSH client tunnel through the authenticated host connection into container SSH.
This repository builds four binaries:
aw-gateway: host-side CLI, SSH dispatcher, optional HTTP API daemon, runtime lifecycle manager, and client-config generator.aw-container-bootstrap: optional in-container bootstrap entrypoint that prepares identity/state and then execs the agent.aw-container-agent: container-side supervisor, control socket, service manager, idle-cleanup agent, and SSH socket bridge.aw-ssh-command-filter: container-side SSHDForceCommandhelper used to enforce configurable legacy SCP policy without breaking shell command exec.
Component layout:
flowchart LR
subgraph workstation["Workstation"]
client["SSH / SCP / SFTP / VS Code or HTTP client"]
end
subgraph host["Managed host or local workstation"]
hostssh["Host sshd (managed deployments)"]
gw["aw-gateway: CLI, SSH dispatch, HTTP listener, client-config generation"]
runtime["Podman / Docker / Colima / Apple container"]
wsdir["Host workspace and state directory"]
end
subgraph container["Managed container"]
boot["aw-container-bootstrap (optional entrypoint)"]
agent["aw-container-agent: service supervisor, control socket, SSH bridge"]
sshd["container sshd and aw-ssh-command-filter"]
svc["configured services"]
end
client -- "ssh user@host" --> hostssh
hostssh -- "ForceCommand" --> gw
client -- "local listener or HTTP" --> gw
gw -- "exec / inspect / remove" --> runtime
runtime -- "bind mounts: binaries, configs, workspace" --> container
runtime -- "manages" --> wsdir
gw -- "control socket" --> agent
gw -- "SSH bridge" --> sshd
boot -- "execs into" --> agent
agent -- "supervises" --> sshd
agent -- "supervises" --> svc
Use source builds for local development or unsupported release platforms. Run build commands from the cloned repository root. Building requires a Rust toolchain with edition 2024 support, which means rustc 1.85 or newer. Container-side binaries must be built for the Linux architecture used by the target container.
cargo build --releaseThe binaries are:
target/release/aw-gateway
target/release/aw-container-bootstrap
target/release/aw-container-agent
target/release/aw-ssh-command-filter
For managed deployments, aw-gateway is installed on the host.
Container-side binaries can either be installed in the target image or mounted
read-only through the bootstrap-mount mode.
See Podman, Docker,
Colima, or
Apple Container for the host and container
install layout for your deployment. macOS readers should use a Linux release
archive or source build whose architecture matches the Linux architecture used
inside the VM or Apple container guest.
Pick a runtime before following a guide. Podman is rootless-friendly and the
default fit for managed Linux hosts. Docker uses a daemon and works well on
shared Linux workstations. Colima wraps Docker inside a Linux VM for macOS.
Apple Container uses Apple's native container runtime on Apple silicon macOS
26 or newer.
- Podman: generic local workstation and remote SSH deployment patterns with a minimal Ubuntu image and copyable example configs.
- Docker: native Linux Docker local and remote SSH deployment patterns.
- Colima: macOS local Colima deployment pattern using Docker through a Colima profile.
- Apple Container: macOS local Apple
containerdeployment pattern using published-port SSH. - Firewall Policy: optional host, container, or VM firewall hooks for egress control.
- Proxy And CA Policy: optional proxy service, CA trust, session environment, and firewall redirect patterns.
- Smoke Test Harness: opt-in live tests for remote Docker, rootless Podman, and Colima hosts.
Gateway config lookup uses this precedence:
--config PATHAW_GATEWAY_CONFIG- User config file, when present:
{AW_GATEWAY_CONFIG_HOME|XDG_CONFIG_HOME|~/.config}/aw-gateway/gateway.toml - System config file, when present:
/etc/aw-gateway/gateway.toml
The container-agent and bootstrap configs remain explicit/system-managed:
/etc/aw-gateway/container-agent.toml
/etc/aw-gateway/container-bootstrap.toml
schema_version pins the config schema. The current value is "1". Gateway
and container-agent configs with a different value fail validation, so set it
at the top of every config file.
Create starter configs before validating them. For gateway configs, either copy the minimal syntax sample or start from a deployment guide's example. For container-agent configs, generate a starter file:
cp aw-gateway.sample.toml ./gateway.toml
aw-container-agent config init ./container-agent.tomlOnce installed on PATH, validate the configs:
aw-gateway --config ./gateway.toml config validate
aw-container-agent --config ./container-agent.toml config validateOn success, gateway config validation exits 0 without output. Container-agent
validation prints ok and exits 0. Validation errors print a diagnostic and
exit nonzero.
Show resolved gateway config paths:
aw-gateway config paths
aw-gateway config paths --jsonConfig path and log level can be overridden with flags or environment variables:
AW_GATEWAY_CONFIG
AW_GATEWAY_CONFIG_HOME
AW_GATEWAY_STATE_HOME
AW_GATEWAY_LOG_LEVEL
AW_CONTAINER_AGENT_CONFIG
AW_CONTAINER_AGENT_LOG_LEVEL
AW_CONTAINER_BOOTSTRAP_CONFIG
AW_GATEWAY_CONFIG selects an explicit host gateway config. AW_GATEWAY_CONFIG_HOME
and AW_GATEWAY_STATE_HOME override the user config and state roots.
AW_CONTAINER_AGENT_CONFIG selects the in-container supervisor config, and
AW_CONTAINER_BOOTSTRAP_CONFIG selects the rendered bootstrap config consumed
by aw-container-bootstrap.
The minimal gateway syntax sample and canonical agent sample are:
aw-gateway.sample.toml
container-agent.sample.toml
For working platform deployments, start from the guide and example config for Podman, Docker, Colima, or Apple Container instead of copying the minimal gateway sample.
Ingress modes converge on the same gateway operation layer. Managed SSH is the standard remote-user path; local-listen mode is useful for workstation profiles; the JSON HTTP API is for non-interactive automation.
flowchart TD
user["User or tool"]
user --> ssh["Managed SSH through host SSHD ForceCommand"]
user --> local["Local listener loopback SSH"]
user --> http["JSON HTTP API"]
ssh --> op["Gateway operation layer"]
local --> op
http --> op
op --> runtime["Container runtime and agent control"]
On a managed host, OpenSSH authenticates the user and invokes aw-gateway with
ForceCommand. The user's normal login shell should remain a standard shell
such as /bin/bash; the gateway handles command dispatch.
ForceCommand makes host SSHD run the gateway instead of the user's shell for
matched accounts. SSHD passes the requested command in SSH_ORIGINAL_COMMAND,
which the gateway parses as a management action or container command.
Generated client config uses ProxyCommand so workstation SSH tools can
tunnel through the authenticated host connection to container SSH.
Example SSHD match block:
Match Group aw-gateway-users
ForceCommand /opt/aw-gateway/bin/aw-gateway --config /etc/aw-gateway/gateway.toml
PermitTTY yes
AllowTcpForwarding no
AllowStreamLocalForwarding no
PermitTunnel no
X11Forwarding no
AllowAgentForwarding noManaged SSH deployments should pass an explicit system config path when
per-user overrides are not intended. Unrestricted users running aw-gateway
directly can use their own default config at
~/.config/aw-gateway/gateway.toml.
Typical user actions through host SSH:
ssh user@host
ssh user@host status
ssh user@host set-default fedora-dev
ssh user@host show-default
ssh user@host reset-default
ssh user@host stop
ssh user@host remove internal-ubuntu-dev
ssh user@host 'git status' # container passthrough commandCommands that match ssh_dispatch.enabled_actions run as gateway management
actions. Other commands, such as git status, are passed through to the
container when allow_container_commands = true.
There are two direct-container client output modes. client-config prints SSH
config that uses key material you manage locally; client-bundle creates a
gateway-managed inner private key for operators who want the gateway to control
that material. For direct SSH/SCP/SFTP/VS Code access to the container SSH
daemon from your workstation, add your public key to the container and generate
client config:
cat ~/.ssh/id_rsa.pub | ssh user@host 'add-container-key ubuntu-dev --public-key -'
ssh user@host 'client-config ubuntu-dev' > ~/.ssh/config.d/aw-gateway
# Ensure your normal ~/.ssh/config includes: Include ~/.ssh/config.d/*
ssh aw-ubuntu-devThe first command appends the workstation public key to the container
authorized-key file under workspace state. The second writes SSH config for the
container route. If you do not use Include ~/.ssh/config.d/* in your main
~/.ssh/config, either add it or pass the generated file explicitly:
ssh -F ~/.ssh/config.d/aw-gateway aw-ubuntu-devThe generated config intentionally omits User and IdentityFile; keep those
in your normal local SSH config when needed. client-config only prints config
and does not create key material or modify authorized keys.
If an operator explicitly wants gateway-managed inner key material, generate a managed key bundle:
ssh user@host 'client-bundle ubuntu-dev'Managed-server client config uses ProxyCommand to route the workstation's SSH
client through the authenticated host connection and into the container SSH
bridge.
Local profiles can use the same gateway binary without host SSHD. For local workflows that do not need OpenSSH-compatible tools, configure no-SSH runtime execution:
[targets.default.access]
method = "runtime_exec"Runtime-exec targets do not expose container SSH, do not generate SSH client
config, and do not support SCP, SFTP, VS Code Remote-SSH, connect,
add-key, add-container-key, client-config, or client-bundle.
add-host-key remains available because it only mutates the host user's SSH
configuration. Runtime-exec targets do support up, run, shell, launch,
launches, status, status --all, targets, stop, and remove through
the host container runtime. They cannot be used when the effective
container_agent configuration consumes a deployment identity bearer because
container runtimes retain launch environments in metadata and runtime-exec
sessions can inherit them, bypassing the agent's read-and-clear boundary. Use
SSH access for targets with bearer-authenticated Access Flow presentation or
approved service identity inheritance.
Start a no-SSH target and open an interactive shell with the configured
identity.session_shell:
aw-gateway --config ./gateway.local.toml up default --json
aw-gateway --config ./gateway.local.toml shell defaultSSH-compatible local profiles remain available. A target can enable a loopback-only listener:
[targets.default.local_ssh]
mode = "listen"
backend = "socket"
readiness = "agent_control"
host = "127.0.0.1"Docker, Colima, and Apple Container profiles can use a published loopback port as the gateway's backend for container SSH:
[targets.default.local_ssh]
mode = "listen"
backend = "published_port"
readiness = "ssh_only"
host = "127.0.0.1"
[target_defaults.container_agent]
enabled = true
control_socket = falseUse control_socket = false when the host gateway only needs the published
SSH port. This lets aw-container-agent supervise services without creating an
unused Unix socket on a Docker/Colima bind mount or Apple container guest bind
mount. Docker and Colima ask the runtime for the mapped port after startup;
Apple Container uses an explicit preallocated loopback port. Apple Container
targets can alternatively use access.method = "runtime_exec" to avoid
publishing an SSH port entirely.
Local workstations that want SSH/SCP/SFTP/VS Code to connect directly to the runtime-published container SSH port can use direct mode:
[targets.default]
mode = "fixed"
stop_when_idle = false
[targets.default.local_ssh]
mode = "direct"
backend = "published_port"
readiness = "ssh_only"
host = "127.0.0.1"
# Optional. If omitted, aw-gateway allocates and persists an explicit
# loopback host port when the container is created.
# port = 40222
[targets.default.idle_cleanup]
owner = "none"
action = "none"Direct mode starts or reuses the fixed target with up, waits for SSH
readiness, records the loopback endpoint, and exits. client-config run on
the gateway host emits SSH config with HostName 127.0.0.1 and the direct
published port, without a ProxyCommand. SSH-dispatched client-config and
client-bundle reject direct mode because 127.0.0.1 would refer to the
client workstation, not the gateway host. stop preserves the endpoint state
for the fixed container; remove deletes it.
Direct SSH sessions bypass the gateway listener and the agent SSH bridge, so
agent-owned idle cleanup cannot observe active SSH/SCP/SFTP sessions. Direct
mode rejects stop_when_idle = true and agent-owned idle cleanup. If a base
config or target default enables agent cleanup, override it with
owner = "none" and action = "none" for the direct target.
The container SSH server must listen on the container network interface for
runtime port publishing to work. The Docker and Podman examples keep sshd bound
to container loopback by default for bridge-only deployments; direct published
port configs that use start-container-sshd can opt in by adding this env value
to the existing container-sshd service entry:
[target_defaults.container_agent.services.env.AW_SSHD_LISTEN_ADDRESS]
value = "0.0.0.0"On macOS, non-interactive SSH sessions may not include user-local package
manager paths. If the Docker CLI used for Colima is not on the SSH session
PATH, set [runtime].program to an absolute path.
Common local_ssh options:
| Field | Values | Purpose |
|---|---|---|
mode |
proxy_command, listen, direct |
Generate ProxyCommand client config, bind a loopback gateway listener, or expose direct loopback SSH config for a runtime-published container port. |
backend |
socket, published_port |
Connect to the container agent SSH bridge socket or to a runtime-published SSH port. |
readiness |
agent_control, ssh_only |
Wait for the agent control socket or only for SSH reachability. |
host |
IP address | Listener or direct client address. Listen mode allows loopback addresses; direct mode requires 127.0.0.1. |
port |
TCP port | Listener port in listen mode, or direct published SSH host port in direct mode. |
In this mode the SSH client still talks to the gateway listener. Docker's published port is an internal backend hop:
ssh client -> aw-gateway local listener -> Docker published port -> container:22
In direct mode there is no gateway listener in the data path:
ssh client -> runtime-published loopback port -> container:22
Start the target and emit connection details:
aw-gateway --config ./gateway.local.toml up default --jsonThe generated SSH config is written under the workspace state directory and can be used by SSH-compatible tools.
When target_defaults.container_agent.enabled = true or an effective target
enables the agent, the gateway renders the effective container-agent policy
into the container state directory. By default it
starts aw-container-agent as the container entrypoint. If
target_defaults.container_bootstrap.enabled = true or an effective target
enables bootstrap, it starts aw-container-bootstrap
instead; bootstrap prepares passwd/group/home/state, runs configured bootstrap
steps, and then execs aw-container-agent so the agent becomes PID 1. When the
agent is disabled, the gateway can still manage container lifecycle and
run/status/stop, but SSH proxying and supervised services are unavailable
unless the target uses a separate configured mechanism.
The container agent supervises configured services, exposes the SSH bridge, and
answers gateway control requests over a private Unix-domain control socket.
Disabling the control socket is useful for published-port SSH backends, but it
also removes agent-control readiness and mutating control requests through that
socket. Mutating control requests require AW_CONTAINER_CONTROL_TOKEN; if the
control socket is enabled without that token, status remains available but
shutdown, reap, and session-hold requests fail as unauthorized.
Gateway-managed control and SSH bridge sockets live under target control socket
config, not under durable workspace state. Before starting a container, the gateway
checks the resolved host and in-container Unix socket paths and fails fast if
any path exceeds the platform socket path limit.
Service example:
[[target_defaults.container_agent.services]]
name = "container-sshd"
required = true
user = "root"
command = ["/usr/local/bin/start-container-sshd"]
restart = "always"
depends_on = ["acl-proxy"]
[target_defaults.container_agent.services.health_check]
type = "tcp"
host = "127.0.0.1"
port = 22
interval = "2s"
timeout = "1s"Supported service health checks include process, TCP, and HTTP checks. HTTP checks can require status codes and top-level JSON field matches.
The optional Access Flow relay runs inside aw-container-agent; it is not a
supervised child process. Every route listens on IPv4 loopback, recovers the
original redirected destination, publishes an AW Access Flow preface, and
connects through one configured Access Flow transport. Presentation is a strict tagged choice of
disabled, anonymous, or bearer_environment:
[target_defaults.container_agent]
access_flow_execution_context = "external"
[target_defaults.container_agent.access_flow_relay]
setup_timeout = "2s"
drain_timeout = "10s"
max_connections = 1024
copy_buffer_bytes_per_direction = 16384
start_after_services = ["transparent-firewall"]
[target_defaults.container_agent.access_flow_relay.presentation]
kind = "bearer_environment"
variable = "AW_IDENTITY_TOKEN"
[[target_defaults.container_agent.access_flow_relay.routes]]
name = "http"
listen = "127.0.0.1:3128"
allowed_destination_ports = [80]
[target_defaults.container_agent.access_flow_relay.routes.transport]
kind = "unix"
path = "/run/acl-proxy/transparent-http.sock"access_flow_execution_context is an optional operator-authored label shared by
every route in the relay plan. Values must match [a-z][a-z0-9_-]{0,63} and are
literal: they are not rendered from target templates, runtime context, or the
container environment. A present value requires an Access Flow relay; an absent
value remains valid. Updated container agents always write AW Access Flow v2,
using a zero-length context when the field is absent. Server-side v1 acceptance
supports older agent binaries and does not synthesize a context.
A route may instead use TLS/TCP. system uses the container process's platform
store and needs no CA mount. For custom or system_plus_custom, provision the
explicit trust bundle through a read-only mount. Mount the containing directory
rather than the individual file so an atomic host-side replacement remains
visible to a later SIGHUP:
[[target_defaults.container_mounts]]
source = "/opt/aw-gateway/trust/acl-proxy"
target = "/etc/aw-gateway/acl-proxy-trust"
mode = "ro"The dedicated
examples/docker/gateway-access-flow-tls.toml
profile shows the mount and relay configuration together. It is a configuration
shape, not a complete transparent-network policy: the deployment must also
redirect workload TCP ports 80 and 443 to the loopback relay listeners and
permit only the relay's narrowly scoped network path to the configured remote
proxy addresses and ports. See
Remote Access Flow TLS/TCP
for the required firewall boundary.
The dedicated TLS examples assume one relay source per Proxy and cap the relay
at 64 active flows, matching each corresponding Proxy listener's global
max_connections. The shipped one-relay Proxy profile leaves its optional
per-source connection and handshake policy disabled, so relay traffic is
bounded by the global listener and handshake limits without an additional
source-address throttle. Deployments with multiple independent relay source
addresses may enable and size the Proxy's per-source policy explicitly.
Before starting the profile's root-run container agent, provision the host source as root-owned, single-link material:
sudo install -d -o root -g root -m 0755 /opt/aw-gateway/trust/acl-proxy
sudo install -o root -g root -m 0644 ./acl-proxy-roots.pem \
/opt/aw-gateway/trust/acl-proxy/roots.pemThe secure loader evaluates metadata in the container's filesystem view.
/, every directory component, and the PEM leaf must be owned by root or the
agent's effective UID and must not be group- or world-writable. No component
may be a symlink. The leaf must be a nonempty regular file with exactly one
hard link, at most 2,113,536 bytes of PEM, at most 128 certificates, and at
most 1 MiB of aggregate DER. For the shipped root-run profile, use root
ownership as shown above. A read-only mount prevents container writes but does
not make unsafe source ownership, modes, links, or host-side mutation trusted.
The corresponding TLS route is:
[[target_defaults.container_agent.access_flow_relay.routes]]
name = "http"
listen = "127.0.0.1:3128"
allowed_destination_ports = [80]
[target_defaults.container_agent.access_flow_relay.routes.transport]
kind = "tls_tcp"
address = "proxy.example.com:7443"
server_name = "proxy.example.com"
trust = "custom"
ca_certificate = "/etc/aw-gateway/acl-proxy-trust/roots.pem"TLS routes require bearer_environment; disabled and anonymous presentation
remain available only to Unix-only relays. Unix and TLS routes may coexist in
one bearer-authenticated relay. Every TLS route requires an explicit trust
mode: system, custom, system_plus_custom, or insecure. The
ca_certificate field is required only for custom and
system_plus_custom. TLS uses version 1.3, verifies the independently
configured name in every verified mode, requires the exact Access
Flow ALPN, and opens one outer connection for each workload connection. It
does not use client certificates, ambient proxy settings, cleartext fallback,
endpoint fallback, pooling, or multiplexing. Address, server name, and trust
source are literal and are not rendered from target variables.
Every verified trust mode grants its complete authority set permission to
authenticate a server that can receive the reusable AWAF bearer. For custom
trust, use a dedicated, minimally scoped CA and name rather than a broad
organizational bundle. system deliberately grants the complete platform
store that authority. The server private key remains only on the remote ACL
Proxy host; do not mount it into the container, install it in AW Gateway, or
expose it to workloads. Access Flow TLS performs no online OCSP or CRL
retrieval. Revocation therefore requires short-lived server certificates and
explicit trust-generation replacement.
The agent prepares system roots, stable custom PEM sources, or their union
through the shared Access Runtime trust component before reporting relay
readiness. insecure still performs encrypted TLS and exact ALPN negotiation
but deliberately skips certificate and server-name authentication. No remote
reachability probe is required. SIGHUP atomically reloads the complete trust
generation without reloading route configuration or the bearer. Existing flows
retain their established generation and drain. Candidate construction leaves
the current valid generation ready. Material, custody, or internal failures
then close new admission; transient system-store failures, cancellation, and
generation-budget rejection preserve the ready generation. SIGTERM and
Ctrl-C retain their ordered shutdown behavior whether or not the agent control
socket is enabled. Foreground aw-gateway signal behavior is unchanged.
Agent relay logs expose only fixed event kinds (Prepared, Ready,
ConnectionOpened, ConnectionRejected, AdmissionClosed,
ConnectionClosed, Drained, Forced, and Failed), a validated route name
when present, the active-flow count, and a fixed close category when applicable.
Close categories are Complete, Saturated, OriginalDestination,
DestinationPort, Channel, Dns, TcpConnect, TlsHandshake,
Authentication, Alpn, Preface, ResourceExhausted, SetupDeadline,
Cancelled, Copy, and Truncation. Remote transport observations never
contain the bearer, raw source addresses, DNS answers, certificate subjects,
SANs, DER bytes or fingerprints, trust or key paths, key material, arbitrary
TLS errors, or untrusted bytes. TLS key logging is disabled.
Server identity and relay trust are independent atomic generations. When the issuing CA changes, rotate them in this order:
- Issue a currently valid server-authentication certificate whose SAN matches
the configured
server_name. Keep its private key on the Proxy host. - Atomically replace the relay PEM bundle with one bounded bundle containing
both the old and new trust anchors. Send
SIGHUPtoaw-container-agent, not the foregroundaw-gateway, and verify aggregate agent readiness. - Atomically replace the Proxy certificate chain and matching private key,
send
SIGHUPto ACL Proxy, and verify Proxy readiness plus a real HTTP and HTTPS Access Flow through every relay population. - After every relay has loaded the overlap bundle and old flows have drained,
atomically replace the relay bundle with the new anchor only, send
SIGHUPto each agent, and repeat readiness and flow verification.
If the server certificate remains under the same trust anchor, skip the overlap steps and rotate only the complete Proxy identity generation. Never publish a partial chain/key pair or mutate a mounted PEM file in place.
A failed agent trust reload keeps established flows alive but makes the relay
unready and closes all new Access Flow admission in that agent, including Unix
routes. Restore the last valid complete PEM file and send another SIGHUP; do
not add a fallback endpoint or weaken verification. A failed Proxy
certificate/key reload has the corresponding fail-closed effect on Proxy
Access Flow admission. Restore a complete valid identity at the same configured
paths and reload again.
Bearer rotation is deliberately separate and disruptive. Stop admitting work, drain and stop the agent, update the sensitive bearer source and the Proxy resolver as one deployment transaction, restart the agent, verify readiness and authenticated flows, and only then reopen workload admission. Trust reload does not reload the bearer, route configuration, address, or server name.
Unix and TLS/TCP are strict transport alternatives, not availability fallbacks. To move an existing route:
- Start and validate the remote Proxy TLS listener, server identity, workload bearer resolver, and deny-by-default policy before changing the relay.
- Install the dedicated relay trust bundle and the exact firewall rules for the remote Proxy address and listener ports. Verify that direct web egress remains blocked.
- Build and validate one complete agent configuration that replaces each
selected route's
unixtransport withtls_tcp. Preserve its loopback listener and allowed destination ports; do not author alternate endpoints. - Drain and stop the target agent, deploy the complete configuration and sensitive bearer together, restart, then test transparent HTTP, nested HTTPS, policy denial, and fail-closed Proxy loss.
Route shape is restart-required. Rollback is the same explicit transaction in reverse: drain and stop the agent, restore the complete prior Unix configuration and its required socket exposure, restore the matching firewall, and restart. The relay never attempts Unix after TLS failure or TLS after Unix failure. Remote TLS still requires a constrained network path from the container to the Proxy; it does not provide a network-disabled or no-NAT container mode.
bearer_environment makes the gateway provision its existing host identity
token under the configured agent variable. Bootstrap disables core/dump
capture before any work and runs bootstrap steps with cleared environments.
The agent reapplies that hardening, consumes and removes the variable before
starting runtime threads, and does not expose the source or bearer through
status, logs, managed services, or workload sessions. A missing or invalid
bearer fails agent startup. The source name must match
[A-Za-z_][A-Za-z0-9_]* and be at most 256 bytes. AW Gateway accepts only
AW_IDENTITY_TOKEN or a nonempty application-owned name beginning
AW_ACCESS_FLOW_; all other names are rejected.
Services may depend on the reserved @access-flow-relay node. Relay activation
waits until every required service named by start_after_services is healthy.
The relay joins the same cycle validation, aggregate readiness, status, idle
activity, fatal shutdown, and reverse dependency ordering as managed services.
On shutdown the agent closes relay admission, stops relay-dependent services,
drains established flows for the configured bound, then stops prerequisites.
Services do not inherit sensitive gateway environment by default. A service
receives AW_IDENTITY_TOKEN only when explicitly configured in the service
env table, such as an ACL Proxy using typed HTTP parent presentation:
[target_defaults.container_agent.services.env]
AW_IDENTITY_TOKEN = { inherit = "AW_IDENTITY_TOKEN" }
STATIC_VALUE = { value = "example" }
FROM_FILE = { file = "/run/secrets/example", required = false }Service env entries can use literal value, inherit from the agent
environment, or read a file. Template interpolation applies to literal value
strings and file paths when interpolate = true; values loaded from files or
inherited environment variables are passed through literally.
AW_IDENTITY_TOKEN inheritance is the narrow exception: it must use the exact
canonical key and source shown above. The agent consumes and removes that
source before creating runtime threads, retains it in clearing sensitive
storage, and materializes it only for each start or restart of an approved
service.
When either a relay or service requests the host identity token, target
container_env, session_env, and session_env_inherit cannot author or
expose AW_IDENTITY_TOKEN. The gateway's typed relay/service injection is the
only container boundary for that deployment token.
The gateway config defines the runtime, workspace paths, SSH dispatch behavior, client config generation, container targets, host lifecycle steps, and embedded container-agent policy.
Minimal runtime selection:
[runtime]
type = "podman" # podman, docker, colima, or apple_containerDocker can use a specific Docker socket:
[runtime]
type = "docker"
docker_host = "unix:///var/run/docker.sock"Colima is implemented through Docker and derives DOCKER_HOST from the Colima
profile:
[runtime]
type = "colima"
profile = "default"Apple container support is for Apple silicon macOS 26 or newer with Apple
container CLI 1.0.0 or newer. Before Apple runtime operations, the gateway
checks container system version --format json and
container system status --format json; if the system is stopped, run
container system start. Apple container targets can use no-SSH
access.method = "runtime_exec" for local runtime execution, or
local_ssh.backend = "published_port" when SSH-compatible clients are needed.
Set runtime.program to use a specific runtime executable instead of looking
up the default podman, docker, colima, or container binary on PATH:
[runtime]
type = "podman"
program = "/usr/local/bin/podman"Target example:
[targets.ubuntu-dev]
image = "ubuntu/dev"
mode = "fixed"
name = "{image_slug}"
stop_when_idle = true
remove_on_stop = falseReplace ubuntu/dev with a real container image. Image references must not
contain whitespace or control characters, must not start with -, and must use
normal slash-separated repository syntax with optional :tag or @digest
suffixes. The deployment guides build and use working runtime-specific images
from the included example Containerfiles.
Fixed targets reuse one named container across connections. Ephemeral targets
create a per-session container and require mode = "ephemeral",
ephemeral_name with {session_id}, and stop_when_idle = true so idle
cleanup can remove each session container.
Delegated runtime context can further partition runtime identity for trusted control-plane callers. Declare allowed keys at the gateway root:
[context_vars.tenant]
required = true
format = "slug"
[context_vars.workspace]
required = true
format = "slug"Callers supply context with global flags before the subcommand:
aw-gateway --context tenant=acme --context workspace=web launch repo-shell -- --agent-arg
aw-gateway --context-file /run/aw-gateway/context.json status --all --json
aw-gateway --context tenant=acme --context workspace=web remove repo --session-id 018f...Context files are JSON objects with string values. Duplicate keys across files
and flags are rejected; missing required keys, unknown keys, malformed
key=value, invalid slug values, unreadable or oversized files, and non-string
JSON values fail before runtime operations. Context values are not secrets:
they are persisted in AW Gateway session metadata and runtime labels, and may
appear in status output for authorized callers.
Context is runtime identity metadata, not launch input. AW Gateway does not
copy context into launch variables, command argv, process environment,
container_env, session_env, or passthrough args. If a context-scoped
container or session exists, an invocation with no context does not act as a
wildcard; list, status, attach/connect, resume, stop, remove, and cleanup paths
fail closed or exclude the scoped container/session.
Fixed targets with required context must include every required key in
target.name, for example:
[targets.repo]
image = "ubuntu/dev"
mode = "fixed"
name = "{image_slug}-{context.tenant}-{context.workspace}"Ephemeral targets can also opt into target workspace cleanup:
[targets.worker]
image = "ubuntu/dev"
mode = "ephemeral"
ephemeral_name = "worker-{session_id}"
stop_when_idle = true
[targets.worker.workspace]
path = "{home}/.cache/aw-gateway/workspaces/{target}-{session_id}"
cleanup = "always"
[targets.worker.idle_cleanup]
# owner = "gateway" is required for workspace.cleanup.
owner = "gateway"
action = "exit_container"workspace.cleanup accepts never (the default), success, or always.
Cleanup is supported only for ephemeral targets with a target-specific
workspace.path under an aw-gateway path component that references
{session_id}. Cleanup also requires gateway-owned exit_container idle
cleanup with no preserve_processes, so the session workspace is not deleted
while the container is intentionally still alive. The gateway deletes only the
resolved workspace for that session after the session is done and after
container cleanup has completed or been attempted. Missing workspaces are
treated as success; cleanup failures are warnings and do not replace the
command or launch exit status. Unsafe deletion roots are refused before the
session runs (empty paths, /, the user home directory, paths missing the
current session id) and again before deletion (symlink roots). Control socket
runtime directories are managed by [target_defaults.control_sockets] or
[targets.<name>.control_sockets], not by workspace
cleanup. Non-listen up remains a warm-up operation and does not trigger
workspace cleanup.
Podman managed-host targets default to the authenticated host user and home.
Docker and Colima targets default to root and /root; set
container_user and container_home when the image provides a different
account.
Target identity controls the user and numeric identity prepared inside the container:
[targets.ubuntu-dev.identity]
bootstrap_user = "root"
session_user = "{user}"
session_uid = "{uid}"
session_gid = "{gid}"
session_home = "/home/{user}"
session_shell = "/bin/bash"Gateway configs commonly include:
-
Config identifiers such as target, launch, template, service, launch-var, and runtime-profile names must start with an ASCII letter, number, or
_, and may then contain only ASCII letters, numbers,.,-, and_. -
[target_defaults]: partial target-shaped defaults inherited by every target. -
[target_templates.<name>]: reusable partial target-shaped templates that targets opt into with ordereduse = [...]. -
[target_defaults.workspace]: default host workspace path, optionalcontainer_pathmount target, state directory, and cleanup policy. -
[target_defaults.control_sockets]: short runtime directories for gateway-managed Unix sockets. Durable config, logs, state, and session metadata remain under the workspace state directory. -
[ssh_dispatch]: which host SSH commands are handled by the gateway and whether interactive shell and container command passthrough are enabled. -
[http]: optional JSON HTTP daemon listener, auth mode, and HTTP action allow list. -
[context_vars.<name>]: allowed non-secret runtime context keys. Key names andformat = "slug"values use lowercase ASCII letters, numbers, and hyphens only, with no leading, trailing, or consecutive hyphens. -
[client_config]: generated SSH alias templates, host name, gateway path, and default identity directory. -
[targets.<name>]: container image, naming mode, access method, container user/home, idle and workspace cleanup behavior, optional runtime args, environment, and local-listen settings. -
[targets.<name>.access]: target transport contract.method = "ssh"is the default and allows SSH-compatible operations when an SSH endpoint is configured.method = "runtime_exec"disables AW Gateway-managed container SSH and uses the runtime exec path for lifecycle, shell, run, and launch operations. -
[targets.<name>.identity]: container bootstrap and session identity fields. -
[[target_defaults.lifecycle_steps]]: phase-keyed host hooks forpre_start,post_start_host,pre_stop, andpost_stop, with per-step command timeouts. -
[[target_defaults.host_steps]]: post-start host hooks that run after agent readiness, such as firewall setup, with per-step command timeouts and optional command health checks. -
[launches.<name>]: named command templates that select a target, validate caller variables and optional passthrough args, optionally run post-ready setup steps, and execute a final command inside the ready container. -
[launch_templates.<name>]: reusable partial launch-shaped templates that launches opt into with ordereduse = [...]. -
includes: strict include globs for splitting target templates, launch templates, targets, and launches into separate TOML files. Strict includes must match at least one file, are lexicographically ordered, and reject unknown fields, duplicate definitions, cycles, and partial object overrides. -
extends: root config inheritance for layering a selected config over a managed base config. Extends chains are limited to 64 root config files. -
[[target_defaults.container_mounts]]and[[targets.<name>.container_mounts]]: extra host-to-container bind mounts, typically read-only bootstrap binaries/configs/certs. Each mount usessource,target, andmode("ro"or"rw"). Rendered mount sources and targets must not contain:or,, targets must be absolute paths, and read-write mount sources must not resolve to world-writable paths. The generated workspace and control-socket mounts use the same separator checks. -
[target_defaults.host_socket_exposures.<name>]and the corresponding target and template maps expose one existing host Unix socket at one exact container path. Socket sources are not accepted throughcontainer_mounts. -
[target_defaults.container_bootstrap]: optional bootstrap entrypoint configuration and pre-agent container bootstrap steps. Targets may overlay[targets.<name>.container_bootstrap]field-by-field. -
[[target_defaults.container_bootstrap_steps]]: optional container-side setup commands that run after identity preparation and before the agent starts. Targets may replace, remove, append, or order steps with[[targets.<name>.container_bootstrap_steps]]. -
[target_defaults.container_ssh.transfer]: explicit container SSH file-transfer policy. Setsftp = "deny"to block SFTP and modern OpenSSH SCP. Setlegacy_scp = "deny","inbound", or"outbound"to control legacyscp -t/scp -fserver mode through the container-side command filter. A target may overlay individual transfer fields with[targets.<name>.container_ssh.transfer]. -
[target_defaults.container_agent]: optional in-container supervision and SSH bridge support. -
target_defaults.container_agent.access_flow_execution_context: optional literal execution context attached to every flow from the effective relay. It overlays as an independent scalar without replacing inherited relay routes or limits and is invalid when no relay is configured. -
[target_defaults.container_agent.access_flow_relay]: optional embedded transparent TCP-to-Access-Flow relay with strict Unix or server-authenticated TLS/TCP routes. The relay table replaces as one object in target overlays; an omitted table inherits, while a present incomplete table is invalid and never borrows omitted fields. -
[[target_defaults.container_agent.services]]: in-container services supervised byaw-container-agent. Usedepends_onand dependency health checks to order managed services. Requiredcontainer_bootstrap_stepsperform privileged preparation before the agent starts. -
[target_defaults.container_agent.ssh_bridge]: Unix socket bridge to container SSH. In gateway config, the socket path is generated from target control sockets.
By default, the host workspace is mounted at target.container_home.
workspace.container_path changes that mount target without changing the
container user's home directory. The configured workspace.state_dir is
resolved under both the host workspace and its container mount, so generated
agent configuration and managed SSH keys remain reachable at the corresponding
in-container path. It must be a contained relative path: absolute paths,
home-relative paths, and .. components are rejected. Existing containers
record the resolved workspace layout in labels and are not reused after that
layout changes. For the conventional container-sshd service, AW Gateway
injects that managed key path as AW_SSHD_AUTHORIZED_KEYS_FILE; images using
start-container-sshd apply it automatically.
By default, gateway-managed sockets use a short per-runtime host directory and a stable in-container mount point:
[target_defaults.control_sockets]
host_dir = "/run/user/{uid}/aw-gateway/{runtime_id}"
container_dir = "/run/aw-gateway"Fixed targets use the target id as {runtime_id}. Ephemeral targets use the
session id. Target-specific overrides are available for unusual runtimes:
[targets.code-review.control_sockets]
container_dir = "/tmp/aw-gateway"The gateway creates the rendered host directory with private permissions before
container startup, bind-mounts it into the container, and removes the leaf
runtime directory during stop/remove cleanup. If the default /run/user/{uid}
directory is unavailable or not writable, configure
target_defaults.control_sockets.host_dir to another short absolute path under
[target_defaults.control_sockets] or
[targets.<name>.control_sockets].
For macOS/Colima, use a user-owned path because macOS does not normally provide
/run/user/{uid}:
[target_defaults.control_sockets]
host_dir = "/Users/alice/.cache/aw-gateway/sockets/{runtime_id}"
container_dir = "/run/aw-gateway"Use the typed map when an existing listener on the runtime host must be reachable from the container:
[target_defaults.host_socket_exposures.transparent_http]
host_path = "/Users/alice/Library/Application Support/AW Gateway/runtime/transparent-http.sock"
container_path = "/run/acl-proxy/transparent-http.sock"
selinux_relabel = "none"AW Gateway validates the final source component without following symlinks, requires an existing Unix socket, rejects collisions with broader managed mounts, and checks the guest endpoint as the configured container user before reporting the target ready. Apple Container 1.1 or newer realizes the declaration as a path-reconnecting UDS-over-vsock relay. Native local Linux Docker and Podman bind the current socket inode; replacing the host listener pathname requires container recreation unless the listener inode is preserved. Colima, VM-backed runtimes, and remote daemons are rejected.
selinux_relabel is mandatory and accepts none, shared, or private on
Linux. Apple accepts only none. Optional user selects the identity used only
for the bounded post-create socket type and access readiness probes and defaults
to root; it does not alter the container, socket, or runtime realization. A
later defaults, template, target, or root extends layer atomically replaces an
exposure with the same key. Status JSON reports each exposure's
path_reconnect or pinned_inode realization and a sanitized failure category.
Target-specific runtime and environment knobs are explicit:
[targets.default]
session_env_inherit = ["GIT_ASKPASS", "GIT_TERMINAL_PROMPT"]
[targets.default.runtime]
extra_run_args = ["--cap-add", "SYS_ADMIN"]
[targets.default.container_env]
CODEX_HOME = "/var/lib/codex"
[targets.default.session_env]
CODEX_HOME = "/var/lib/codex"container_env is passed when the long-lived container is created.
session_env is used for gateway-managed command execution and is rendered
into the generated SSHD session environment snippet consumed by the example
container SSH helper.
session_env_inherit is an allow-list of environment variable names copied
from the aw-gateway process into gateway-managed command execution when the
named variable is present. Inherited values are not passed when the long-lived
container is created and are not written into the generated SSHD session
environment snippet. Effective command env is layered as built-in SHELL and
PATH, then present inherited values, then explicit session_env, then launch
env, and finally per-step launch env for that step. Repeating the same
session_env_inherit key through target defaults, templates, and the concrete
target is a validation error; unlike container_env and session_env, inherited
env names do not override by key.
Inherited names are read from the running gateway process at command execution
time. This is useful when a caller starts a fresh aw-gateway process with
per-launch non-secret routing or helper values. A long-running shared HTTP
gateway has one process environment, so inherited values are static for that
daemon rather than per principal. Avoid inheriting SHELL or PATH unless you
intentionally want host-process values to override the container-oriented
defaults. Inherited values must be valid UTF-8; values that are present but not
valid UTF-8 are skipped with a warning.
Runtime exec env values are normally supplied through the spawned container
runtime process environment while only the env names appear on runtime argv.
Env names that can alter the host-side runtime client itself, such as
PATH, LD_PRELOAD, DOCKER_HOST, and related loader/runtime configuration
keys, are passed as explicit KEY=value runtime arguments instead of being set
on the client process environment. Avoid using those host-sensitive names for
high-value secrets because their values can be visible in host process listings.
Container SSH transfer policy is independent for SFTP and legacy SCP:
[target_defaults.container_ssh.transfer]
sftp = "allow" # allow | deny
legacy_scp = "allow" # allow | deny | inbound | outboundTarget transfer policy overlays the default transfer table field-by-field, so set only the fields that differ for that target:
[targets.internal.container_ssh.transfer]
sftp = "deny"
legacy_scp = "outbound"When sftp = "deny", start-container-sshd removes the SFTP subsystem from
the runtime SSHD config. Modern OpenSSH SCP uses SFTP, so that blocks both. The
helper also adds a container-side ForceCommand that runs
aw-ssh-command-filter whenever either SFTP or legacy SCP policy is
restrictive, so exec-form sftp-server and denied legacy SCP server commands
are checked by the same policy boundary. Legacy SCP inbound means upload into
the container (scp -t); outbound means download from the container
(scp -f). SFTP has only allow/deny because the SFTP subsystem is a
bidirectional protocol channel rather than separate upload/download server
commands.
Container-side aw-ssh-command-filter implements the SFTP exec-form and legacy
SCP checks. Direct denied transfer-server commands are rejected. Shell
composition remains available, but a best-effort lexical scan also rejects
composed commands containing a recognizable scp -t, scp -f,
internal-sftp, or sftp-server invocation that the configured policy denies.
The scan covers common chaining, substitution, wrapper, and shell re-entry forms
without attempting to implement a complete shell parser. Install or mount the
binary alongside the agent whenever transfer policy may deny or direction-limit
file transfer.
This filter is a best-effort policy control to discourage casual scp/sftp
use, not a security boundary. The lexical scan is deliberately
non-exhaustive, and any user permitted to run commands over the container SSHD
can still move data by other means — for example streaming a file over an
ordinary command's stdin or stdout (cat > file, cat file). Use
transfer policy to enforce a stated "do not transfer files" convention; do not
rely on it to contain a motivated or compromised user. Removing arbitrary
command execution (a transfer-only session) is the only way to make the
direction limits an actual boundary.
target_defaults.container_ssh.transfer only applies to traffic that
traverses the container SSHD. Gateway actions such as run execute through the
host container runtime, so they do not pass through the container SSHD
ForceCommand and are not controlled by SFTP/SCP transfer policy.
Host-gateway SSH dispatch checks the default transfer table before dispatch;
per-target transfer overrides do not relax that host-side gate and only affect
direct container-SSHD access. If a deployment intends to expose management-only
SSH commands without arbitrary container exec, omit run from
ssh_dispatch.enabled_actions; omit launch if users should not start
configured launch workflows; omit launch-show if users should not inspect a
single configured launch's implementation details; omit launches if users
should not discover configured launches and their inputs; also omit connect
if users should not receive a full container SSH session.
allow_interactive_shell = false blocks SSH-dispatched interactive shells,
while allow_container_commands = false blocks non-gateway passthrough
commands. Both default to true, and neither option disables explicitly enabled
gateway actions.
Default lifecycle, host, and bootstrap step lists are inherited by every target.
Target entries use the same key as the default list (phase + name for
lifecycle_steps, name for host_steps and container_bootstrap_steps).
A same-key target entry replaces the inherited entry in place, while
enabled = false removes an inherited entry. New target-only entries append by
default and can specify one of before = "name" or after = "name"; lifecycle
ordering references are resolved only within the same phase.
As a convenience, a target lifecycle or host step entry that sets only
timeout inherits the missing fields from the matching default entry; any other
partial override must include the full replacement payload.
Use lifecycle_steps for host hooks tied to a lifecycle phase, including stop
and teardown phases. Use host_steps for post-start checks and setup that
should run after the container agent is ready and can report readiness.
Use container_bootstrap_steps for in-container setup after identity
preparation and before the agent starts.
| Step kind | Where | When |
|---|---|---|
lifecycle_steps |
Host | pre_start, post_start_host, pre_stop, post_stop |
host_steps |
Host | After container agent readiness |
container_bootstrap_steps |
Container | After identity prep, before agent start |
Lifecycle and host hook commands use timeout = "60s" by default. Set a larger
per-step timeout when a hook legitimately needs more time. The timeout uses
the same explicit units as other durations: ms, s, m, or h. Timed-out
required hooks fail the operation after the child process is killed and reaped;
timed-out optional hooks warn and continue. host_steps.health_check timeouts
are separate from the host step command timeout and default to 5s for command,
TCP, and HTTP health checks.
[[target_defaults.lifecycle_steps]]
phase = "pre_start"
name = "ensure-workspace"
required = true
timeout = "60s"
command = ["/usr/bin/mkdir", "-p", "{workspace}"]
[[target_defaults.host_steps]]
name = "network-policy"
required = true
timeout = "30s"
command = ["/opt/site-policy/bin/network-policy", "add", "{container_pid}"]
[target_defaults.host_steps.health_check]
type = "command"
command = ["/opt/site-policy/bin/network-policy", "check", "{container_pid}"]
timeout = "5s"Common target behavior can also be factored into named
[target_templates.<name>] sections. Templates use the same partial target
shape as [target_defaults] and [targets.<name>]. A target opts in with
ordered use = [...]; effective target order is [target_defaults], then each
named template in order, then the concrete target. Templates may use other
target templates, and cycles or unknown template names fail config validation.
[target_templates.rocky-runtime]
image = "rocky8/base"
container_user = "worker"
container_home = "/home/worker"
[target_templates.review-ephemeral]
mode = "ephemeral"
ephemeral_name = "review-{session_id}"
stop_when_idle = true
[target_templates.review-ephemeral.idle_cleanup]
owner = "gateway"
action = "exit_container"
[target_templates.rocky-review]
use = ["rocky-runtime", "review-ephemeral"]
image = "rocky8/review"
[targets.code-review-worker]
use = ["rocky-review"]
image = "rocky8/review-sip"
[targets.code-review-worker.workspace]
path = "{home}/.cache/aw-gateway/workspaces/{target}-{session_id}"
cleanup = "always"Target and launch definitions can be split into strict include files:
includes = ["/etc/aw-gateway/config.d/*.toml"]Include glob matches are sorted lexicographically before composition. Each
declared include pattern must match at least one file. Includes are resolved
relative to the file that declares them, may be nested, and reject cycles,
duplicate target/template or launch/template names, unknown fields, and partial
object merge or override behavior. Include files may define nested
includes, [target_templates.<name>], [launch_templates.<name>],
[targets.<name>], and [launches.<name>].
Gateway-wide policy and defaults remain root-owned. Include files must not
define schema_version, default_target, extends, [runtime], [logging],
[http], [ssh_dispatch], [client_config], [target_defaults], or
[launch_defaults].
Gateway config composition has two boundaries: include files split one root
config into smaller files, while extends layers complete root configs. Each
root config composes its own includes before it participates in root-to-root
inheritance.
flowchart TD
selected["Selected root config: flag, env, user, or system"]
selected --> selectedIncludes["Compose selected root includes"]
selectedIncludes --> hasExtends{"extends?"}
hasExtends -- no --> selectedReady["Selected root value"]
hasExtends -- yes --> parent["Load parent root config"]
parent --> parentIncludes["Compose parent includes"]
parentIncludes --> parentExtends{"parent extends?"}
parentExtends -- yes --> ancestor["Repeat for ancestor roots"]
ancestor --> mergeAncestors["Merge ancestors base-to-child"]
parentExtends -- no --> mergeParent["Parent root value"]
mergeAncestors --> mergeParent
mergeParent --> mergeRoot["Merge parent root into selected root"]
selectedReady --> validate["Deserialize and validate gateway schema"]
mergeRoot --> validate
validate --> effectiveTargets["Resolve effective targets"]
validate --> effectiveLaunches["Resolve effective launches"]
effectiveTargets --> runtime["Runtime, CLI, SSH, HTTP use effective config"]
effectiveLaunches --> runtime
After this raw root composition step, normal schema validation and typed target/launch defaults, template chains, and concrete-definition overlays produce the effective config used by runtime operations.
Root configs may inherit another root config with extends:
extends = "/etc/aw-gateway/gateway.toml"extends is honored only by the selected root config. Unlike include files, an
extended root may define root-owned policy, defaults, templates, targets, and
launches. Extends chains may have multiple levels, up to 64 root config files.
The loader composes each file's own includes relative to that file, strips
loader-only extends and includes, then merges deepest base-to-child before
normal validation.
Root inheritance uses these merge rules:
- Tables merge by key, except each service
env.<NAME>value replaces the inherited value as a whole. - Each same-key
host_socket_exposuresentry replaces the inherited entry as a whole rather than merging individual path or relabel fields. container_agent.access_flow_relayreplaces as a whole typed component when a later target layer supplies it. Its routes and lifecycle bounds never merge piecemeal.- Scalars and ordinary arrays replace the inherited value. This includes the
independent
container_agent.access_flow_execution_contextscalar,container_mounts, runtime argument arrays, command arrays, dependency arrays, allow-list arrays, and launch variable value arrays. - Named service arrays merge by
nameattarget_defaults.container_agent.services,target_templates.<name>.container_agent.services, andtargets.<name>.container_agent.services. - Named step arrays merge by
namefor target lifecycle, host, and container bootstrap steps, and for launchstepsin launch defaults, templates, and launches.
Named arrays preserve inherited order and append new child entries. Step patch
controls such as enabled = false, before, and after are not deletion or
reorder operations across extends; after root inheritance, the merged step must
still pass normal validation. Use target or launch template overlays when a
target or launch needs to remove or reorder inherited steps.
Launches are stateless, configured command templates for repeatable workflows.
They do not add a job history or background launch manager. A launch selects an
existing target, validates typed caller variables, starts or reuses the target
through the normal readiness path, runs optional post_ready steps, then
executes the final command inside the ready container.
Caller variables are referenced only as {var.<name>}. Built-ins such as
{workspace}, {container_home}, {session_id}, {target}, and
{container_name} remain unprefixed. Unknown variables fail config
validation, and unprefixed caller variables such as {repo} are rejected.
String launch variables supplied by callers, string defaults, and enum values
must not contain NUL, LF, or CR characters.
Treat {var.*} values as untrusted caller input when rendering host-side launch
step command, cwd, or env fields. Avoid mapping caller strings into
host-sensitive environment keys such as HOME, XDG_CONFIG_HOME,
XDG_DATA_HOME, PATH, LD_PRELOAD, runtime client settings such as
CONTAINER_CONNECTION, shell startup variables, or language loader paths
unless the variable is constrained to a small configured enum.
[launches.repo-shell]
target = "default"
description = "Clone a repository and open a shell."
cwd = "{container_home}/repo"
env = { REPO_URL = "{var.repo}" }
command = ["/bin/bash", "-lc", "exec /bin/bash"]
[launches.repo-shell.vars]
repo = { type = "string", required = true, description = "Git repository URL" }
branch = { type = "string", default = "main" }
mode = { type = "enum", values = ["fast", "safe"], default = "safe" }
debug = { type = "boolean", default = false }
limit = { type = "number", default = 1 }
[[launches.repo-shell.steps]]
phase = "post_ready"
location = "container"
name = "clone"
required = true
timeout = "5m"
cwd = "{container_home}"
command = ["git", "clone", "--branch", "{var.branch}", "--single-branch", "{var.repo}", "repo"]Common launch behavior can be factored into [launch_defaults]. Defaults use
the same partial launch shape as concrete launches: scalar fields such as
target, cwd, description, and command are replaced by a concrete
launch, env and vars merge by key, and steps merge by name.
[launch_defaults]
target = "default"
cwd = "{container_home}"
env = { CODEX_HOME = "{container_home}/.codex" }
[launch_defaults.vars]
repo = { type = "string", required = true, description = "Git repository URL" }
[[launch_defaults.steps]]
phase = "post_ready"
location = "container"
name = "prepare"
command = ["mkdir", "-p", "{container_home}/repo"]
[launches.repo-shell]
description = "Clone a repository and open a shell."
cwd = "{container_home}/repo"
command = ["/bin/bash", "-lc", "exec /bin/bash"]Named launch templates provide additional reusable partial launch layers.
Launches opt in with ordered use = [...]; effective launch order is
[launch_defaults], then each named launch template in order, then the
concrete launch. Launch templates may use other launch templates, and cycles or
unknown template names fail config validation. A launch command always
replaces the earlier command; command fragments are not composed.
[launch_templates.repo-review]
target = "default"
cwd = "{container_home}/repo"
[launch_templates.repo-review.vars]
repo = { type = "string", required = true, description = "Git repository URL" }
[launch_templates.codex-review]
use = ["repo-review"]
env = { CODEX_HOME = "{container_home}/.codex" }
command = ["codex", "exec", "{var.repo}"]
[launches.code-review]
use = ["codex-review"]
description = "Run a Codex review."
command = ["codex", "exec", "review", "{var.repo}"]Supported variable types are string, enum, boolean, and number.
Boolean CLI values must be true or false; number values must parse as
finite numbers; enum values must exactly match the configured values.
Optional variables referenced by templates must define a default.
Launches can also opt into caller-supplied argv passthrough with
allow_args = true. The configured command owns the executable and places one
whole-argv {args} sentinel where caller args should be spliced. {args} is
not a template variable, cannot be embedded inside a larger string, and cannot
be argv[0].
[launches.agent-pack-review]
target = "default"
allow_args = true
command = [
"agent-pack",
"run",
"--manifest", "{var.manifest}",
"--json",
"{args}",
]
[launches.agent-pack-review.vars]
manifest = { type = "string", required = true }Callers pass launch args after --. Empty passthrough args are equivalent to
no args; non-empty args are rejected unless the effective launch has
allow_args = true.
When a launch is reachable through SSH dispatch, allow_args = true exposes the
configured program's CLI surface to authorized launch callers. Omit launch
from ssh_dispatch.enabled_actions if callers should not be able to supply
program arguments.
Launch commands:
aw-gateway launches
aw-gateway launches --json
aw-gateway launch show repo-shell
aw-gateway launch show repo-shell --json
aw-gateway launch repo-shell --var repo=https://example.invalid/YOUR-REPO.git --var branch=main
aw-gateway launch repo-shell --session-id abc123def456 --var repo=https://example.invalid/YOUR-REPO.git
aw-gateway launch agent-pack-review --var manifest=/opt/agent-pack/review.yaml -- --skill engineering/fresh-eyes "Review this branch."When launches, launch-show, and launch are present in
ssh_dispatch.enabled_actions, the same commands can be invoked
through the host SSH gateway:
ssh host launches
ssh host 'launch show repo-shell --json'
ssh host 'launch repo-shell --var repo=https://example.invalid/YOUR-REPO.git --var branch=main'
ssh host 'launch repo-shell --session-id=abc123def456 --var=repo=https://example.invalid/YOUR-REPO.git'
ssh host 'launch agent-pack-review --var manifest=/opt/agent-pack/review.yaml -- --skill engineering/fresh-eyes "Review this branch."'Omit launch from ssh_dispatch.enabled_actions if SSH users should not start
configured launch workflows. Omit launch-show if SSH users should not inspect
one configured launch's implementation details. Omit launches if SSH users
should not list configured launches and their inputs.
launches --json emits a bare array of launch summaries. Each summary includes
name, target, allow_args, optional description, and a vars object
keyed by variable name with type, required, optional default, optional
enum values, and optional description. launch show --json emits one
detail object with the same variable metadata plus allow_args, resolved
target mode, fixed-target container name, steps, optional final cwd,
optional final env, and final command.
Ephemeral launch detail omits the concrete container name because it is not
known until a session id is selected.
Launch execution order is:
- Load, include, and validate config.
- Resolve the named launch.
- Validate supplied
--var key=valuevalues, passthrough args, and apply defaults. - Resolve and prepare the configured target.
- Run the existing target lifecycle, readiness checks, and target
host_steps. - Run launch
post_readysteps in TOML order. - Execute the final command inside the ready container.
- Drop the session marker and run normal gateway-owned cleanup.
Container launch step environment is target session env, then rendered launch env, then rendered step env, with later values overriding earlier. The final command receives target session env plus rendered launch env. Host launch step env is exactly the rendered step env.
Launch provenance is intentionally minimal. Session markers, status JSON, and
newly created ephemeral session container labels store only the launch name as
launch; resolved variables, argv, env, repository URLs, and branch names are
not persisted. Fixed/reused containers do not persist launch labels because the
container can outlive any one launch session.
aw-gateway status <target> --json and aw-gateway status --all --json
include nullable launch fields. Text status <target> prints
launch: <name> only when present, and status --all includes a compact
LAUNCH column.
aw-gateway [--config PATH] [--log-level LEVEL] <command>
Global options:
--config PATH: gateway config path. Also available asAW_GATEWAY_CONFIG.--log-level LEVEL: override configured gateway log level. Also available asAW_GATEWAY_LOG_LEVEL.-h, --help: print help.-V, --version: print version.
Gateway commands:
config validate
config paths [--json]
connect [--session-id ID] [target]
up [target] [--json] [--session-id ID]
run [--session-id ID] [target] [--cwd DIR] -- <command> [args...]
shell [--session-id ID] [target] [--cwd DIR] [-- <shell-args>...]
launches [--json]
launch show <name> [--json]
launch <name> [--session-id ID] [--var key=value]... [-- <args...>]
stop [target] [--session-id ID]
remove [target] [--session-id ID]
status [target] [--json] [--session-id ID]
status --all [--json]
targets [--json]
http
set-default <target-or-image> [--reset]
show-default
reset-default
add-key [target] [--public-key PATH|-]
add-host-key [--public-key PATH|-]
add-container-key [target] [--public-key PATH|-]
help
client-config [target] [--identity-file PATH]
client-bundle [target] [--identity-file PATH] [--rotate-key]
A session is one gateway connection or invocation tracked for lifecycle and
idle cleanup decisions. Ephemeral targets generate a fresh 12-character
lowercase hexadecimal session ID unless --session-id ID is supplied. Use an
explicit session ID when another tool needs deterministic naming for local
connect, run, launch, up, status, stop, or remove commands
against the same per-session container. SSH dispatch accepts --session-id for
connect, run, launch, stop, and remove. Fixed targets reject
--session-id.
Gateway command behavior:
config validate: load and validate the gateway config.config paths [--json]: show the effective user, user config/state directories, checked gateway config files, and selected config source.connect [--session-id ID] [target]: start or reuse a target and proxy the current SSH stream to the container SSH bridge.up [target] [--json] [--session-id ID]: start or reuse a target and report readiness. Local-listen targets keep the listener alive until interrupted; direct published-port targets return after the container SSH endpoint is ready.run [--session-id ID] [target] [--cwd DIR] -- <command> [args...]: start or reuse a target and run one command inside the container. A command is required; useupto start or hold a target without running a command. Foreground runs interrupted bySIGINT,SIGTERM, orSIGHUPare canceled with in-container process cleanup and then routed through normal session cleanup.shell [--session-id ID] [target] [--cwd DIR] [-- <shell-args>...]: start or reuse a target and run the target's configuredidentity.session_shellthrough the runtime exec path. This is local CLI/API behavior, not an OpenSSH endpoint.launches [--json]: list configured launches.launch show <name> [--json]: show one configured launch's variables, steps, and final command.launch <name> [--session-id ID] [--var key=value]... [-- <args...>]: start or reuse the launch target, run any post-ready steps, and execute the launch command. Args after--are accepted only for launches withallow_args = trueand are spliced at the configured{args}argv element. Foreground launches interrupted bySIGINT,SIGTERM, orSIGHUPare canceled with in-container process cleanup and then routed through normal session cleanup.stop [target] [--session-id ID]: stop a target, or a specific ephemeral session target.remove [target] [--session-id ID]: stop a fixed target if needed, then remove its existing container so the next start recreates it from the current config, or remove one specific ephemeral session target. Explicit remove also attempts to clean the resolved session workspace when workspace cleanup is notnever; if the container is already absent, context-scoped cleanup requires a matching session marker context. Workspace cleanup failures or skipped context checks are logged, and the command result still reports the container removal outcome.status [target] [--json] [--session-id ID]: report one configured/default target's container state.status --all [--json]: list existingaw-gateway-managed containers for the current user from runtime labels. This omits configured targets that have never created a container and omits unrelated or unlabeled containers.--allcannot be combined with[target]or--session-id.targets [--json]: list configured targets without starting or inspecting containers.http: start the JSON HTTP listener configured by[http]. Fails withhttp listener is disabled in configwhen[http].enabled = false.set-default <target-or-image> [--reset]: set the user's default target. If the argument is not a configured target name, the gateway tries to resolve it as a known image name.--resetis equivalent toreset-default.show-default: show the user's effective default target.reset-default: clear the user's default and fall back to the configured default.add-key [target]: add one SSH public key to both the user's host~/.ssh/authorized_keysfile and the target container authorized-key file.add-host-key: add one SSH public key to the user's host~/.ssh/authorized_keysfile.add-container-key [target]: add one SSH public key to the target container authorized-key file.help: print the restricted SSH command summary. This is separate from CLI--helpso it can be safely exposed through SSH dispatch.client-config [target]: generate SSH client configuration for direct container SSH/SCP/SFTP/VS Code access.client-bundle [target]: generate a gateway-managed inner private key and a self-contained SSH config bundle.
When invoked by OpenSSH ForceCommand, the gateway parses
SSH_ORIGINAL_COMMAND and exposes the restricted SSH command set. That set
uses the same command names as the host CLI for supported actions.
CLI and SSH management commands share the same operation handling for target discovery, status, launch discovery, launch execution, lifecycle actions, default selection, and client config rendering. That keeps text and JSON output aligned between those transports. The HTTP API uses the same operation layer for its narrower action set, but it does not expose SSH-only actions, streaming, or background job retrieval.
add-key, add-host-key, and add-container-key options:
--public-key PATH: read exactly one SSH public key from a file.--public-key -: read exactly one SSH public key from stdin.- Omitting
--public-keyprompts for one SSH public key on stdin.
client-config options:
--identity-file PATH: use an explicit private key path in generated config.
client-bundle options:
--rotate-key: rotate the managed inner SSH key before writing the bundle.--identity-file PATH: use an explicit private key path in the generated bundle config.
Container agent commands:
aw-container-agent [--config PATH] [--log-level LEVEL] config validate
aw-container-agent [--config PATH] [--log-level LEVEL] config init [path] [--force]
aw-container-agent [--config PATH] [--log-level LEVEL] run
Container agent options:
--config PATH: container-agent config path. Also available asAW_CONTAINER_AGENT_CONFIG.--log-level LEVEL: override configured container-agent log level. Also available asAW_CONTAINER_AGENT_LOG_LEVEL.-h, --help: print help.-V, --version: print version.
Container agent command behavior:
config validate: load and validate the container-agent config.config init [path] [--force]: write the embedded sample container-agent config.run: start supervised services, control socket, SSH bridge, and cleanup loop according to config.
Container bootstrap invocation:
aw-container-bootstrap [--config PATH] [--bootstrap-config PATH] [--log-level LEVEL]
Container bootstrap options:
--config PATH: container-agent config path passed through to the agent. Also available asAW_CONTAINER_AGENT_CONFIG.--bootstrap-config PATH: rendered bootstrap config path. Also available asAW_CONTAINER_BOOTSTRAP_CONFIG.--log-level LEVEL: override configured container-agent log level. Also available asAW_CONTAINER_AGENT_LOG_LEVEL.
aw-container-bootstrap is intended as a container entrypoint. It prepares the
container identity and configured bootstrap steps, then execs
aw-container-agent.
aw-gateway http starts a JSON HTTP listener from the gateway config. The
daemon starts only when [http].enabled = true; otherwise it exits nonzero
with http listener is disabled in config. The listener address is a single
socket string such as 127.0.0.1:8080 or [::1]:8080.
[http]
enabled = true
listen = "127.0.0.1:8080"
enabled_actions = ["status", "targets", "launches", "launch-show", "launch", "up", "run", "stop", "remove"]
[http.auth]
type = "none"When auth.type = "none", no Authorization header is required and
http.listen must be loopback. Non-loopback HTTP listeners require bearer auth.
Bearer auth reads the configured token and requires
Authorization: Bearer <token> on every /api/v1/* route. The HTTP listener
does not terminate TLS; use loopback or a TLS-terminating reverse proxy for
bearer auth.
[http.auth]
type = "bearer"
token = "change-me"http.enabled_actions is an HTTP-specific allow list. Supported values are
exactly status, targets, launches, launch-show, launch, up, run,
stop, and remove. Other gateway actions such as connect, key management,
client-config/bundle, proxy/tunnel helpers, and default-target management are
not HTTP API actions.
Every success response is JSON. Metadata endpoints return:
{"ok": true, "data": {}}Runtime operations accept the same delegated context as the CLI. Body-based
requests use a strict context object:
{
"target": "repo",
"session_id": "018f...",
"context": {
"tenant": "acme",
"workspace": "web"
}
}Status endpoints carry context as flattened query parameters:
GET /api/v1/status?target=repo&session_id=018f...&context.tenant=acme&context.workspace=web
GET /api/v1/status/all?context.tenant=acme&context.workspace=web
Wait-mode command and launch responses return HTTP 200 even when the command exit code is nonzero:
{"ok": true, "mode": "wait", "exit_code": 0, "stdout": "...", "stderr": "..."}Wait-mode callers can request JSON decoding for captured streams with
output_format. A successfully decoded stream is returned as stdout_json or
stderr_json instead of the text field for that stream:
{
"ok": true,
"mode": "wait",
"exit_code": 0,
"stdout_json": {"status": "ok"}
}If a stream requested as JSON is valid UTF-8 but not valid JSON, the response
still reports the completed command and returns the raw text stream with an
output_errors entry:
{
"ok": true,
"mode": "wait",
"exit_code": 1,
"stdout": "not-json",
"output_errors": {
"stdout": {
"format": "json",
"code": "invalid_json",
"message": "captured stdout is not valid JSON"
}
}
}If a selected stream is not valid UTF-8, the response still reports the
completed command and omits only that stream, with an invalid_utf8 entry in
output_errors.
Wait mode captures at most 4 MiB per selected stream. If stdout or stderr
exceeds that cap, the stream is truncated to the cap and the response includes
an output_truncated flag for that stream:
{
"ok": true,
"mode": "wait",
"exit_code": 0,
"stdout": "...",
"output_truncated": {"stdout": true}
}Detach-mode command and launch responses return HTTP 202:
{"ok": true, "mode": "detach", "status": "accepted", "operation_id": "abc123"}There is no query-later result endpoint for detached operations. Detached operations run in the background through the same gateway operation layer and are observable only through existing lifecycle/status side effects.
PTY-mode command and launch requests prepare an interactive session and return HTTP 201 with a short-lived single-use attach lease:
{
"ok": true,
"mode": "pty",
"status": "prepared",
"pty_id": "pty_abc123",
"attach_token": "awpt_secret",
"session_id": "abc123def456",
"attach_url": "/api/v1/pty/pty_abc123"
}For fixed targets, session_id is null.
The POST is the authorized run or launch action and may block while the
target becomes ready and launch steps run. The client then opens the WebSocket
attach URL and sends the first text frame as {"type":"auth","token":"..."}.
The attach token is a one-time lease-scoped capability; bearer headers are not
used on the WebSocket route so browser-native clients can attach. After auth,
binary WebSocket frames carry terminal bytes, text resize frames update the
PTY size, and closing the WebSocket cancels the foreground PTY exec while normal
idle/workspace cleanup policy decides whether the container remains available.
Gateway shutdown also cancels active PTY execs and expires prepared leases.
Errors use a stable envelope:
{"ok": false, "error": {"code": "invalid_request", "message": "human-readable message"}}Validation and authorization errors include specific client-facing messages. Internal operation failures use a generic message and log the detailed source server-side.
| Method | Path | Action | Operation |
|---|---|---|---|
GET |
/api/v1/status?target=default&session_id=abc |
status |
GatewayOperation::Status |
GET |
/api/v1/status/all |
status |
GatewayOperation::StatusAll |
GET |
/api/v1/targets |
targets |
GatewayOperation::Targets |
POST |
/api/v1/up |
up |
GatewayOperation::Up |
POST |
/api/v1/stop |
stop |
GatewayOperation::Stop |
POST |
/api/v1/remove |
remove |
GatewayOperation::Remove |
GET |
/api/v1/launches |
launches |
GatewayOperation::Launches |
GET |
/api/v1/launches/{name} |
launch-show |
GatewayOperation::LaunchShow |
POST |
/api/v1/launches/{name}/run |
launch |
GatewayOperation::Launch |
POST |
/api/v1/run |
run |
GatewayOperation::Run |
GET |
/api/v1/pty/{pty_id} |
lease token | WebSocket PTY attach |
Lifecycle POST bodies accept optional target and session_id fields. Fixed
targets reject session_id; ephemeral targets require it for stop and
remove.
Command-like POST bodies also accept optional mode, output, and
output_format fields.
mode defaults to wait and can be wait, detach, or pty; HTTP does not
expose inherited-stdio stream mode. output defaults to
["stdout", "stderr"], accepts only stdout and stderr, and applies only to
wait responses. output_format accepts text or json for selected streams
and defaults to text. output and output_format are rejected for detach and
PTY requests.
In wait mode, aw-gateway owns the operation until the response completes. If a direct HTTP client connection closes or resets before completion, the gateway cancels the operation, runs the same session cleanup path used by interrupted foreground commands, and attempts bounded in-container process-tree cleanup for the final command. Silently dead peers that do not deliver FIN/RST are limited by TCP/proxy behavior; use detach mode for operations that must survive client network loss.
{
"target": "default",
"session_id": "optional",
"cwd": "~/workspace",
"command": ["bash", "-lc", "echo hello"],
"mode": "wait",
"output": ["stdout", "stderr"],
"output_format": {
"stdout": "text",
"stderr": "json"
}
}Launch run requests accept typed JSON variables. Strings, booleans, integers,
and finite numbers are passed to launch validation; nulls, arrays, objects,
duplicate keys, unknown vars, missing required vars, enum/range/type failures,
and non-finite numbers are rejected as invalid_launch_var. Launch run
requests also accept optional args, an array of non-empty strings. Non-empty
args require the effective launch to set allow_args = true; malformed or
disallowed args are rejected as invalid_launch_args.
{
"session_id": "optional",
"vars": {
"repo": "https://example.invalid/YOUR-REPO.git",
"debug": true,
"count": 3,
"mode": "safe"
},
"args": ["--skill", "engineering/fresh-eyes", "Review this branch."],
"mode": "wait",
"output": ["stdout", "stderr"]
}PTY requests must include an initial terminal size. Pixel dimensions are optional:
{
"command": ["bash", "-lc", "exec bash"],
"mode": "pty",
"terminal": {
"cols": 120,
"rows": 34,
"cell_width_px": 9,
"cell_height_px": 18
}
}PTY attach leases are single-use. A PTY session cannot be replayed or attached by multiple clients.
The assets/ directory contains deployable helpers and image files:
assets/acl-proxy.example.toml: starteracl-proxyallowlist for common coding-agent egress. Review and adapt before deploying.assets/aw-iptables: applies, checks, and reports namespace-local proxy firewall rules for a running container PID.assets/aw-transparent-uds-firewall: installs, validates, repairs, and watches the optional host-proxy profile's generation-based, fail-closed transparent firewall inside the container.assets/ensure-storage-conf: creates a rootless Podman storage config for shared image storage on managed hosts. It takes explicit--template,--shared-store, and optional--storage-confarguments.assets/copy-skel: copies top-level deployed skel files into a workspace without overwriting existing files. It takes explicit--skel-dirand--workspacearguments.assets/copy-workspace-template: copies a workspace template into an empty gateway-owned workspace path. It takes explicit--template,--dest, and optional repeated--excludearguments.assets/sshd_config_agent: container-local SSHD policy intended for gateway targets.assets/start-container-sshd: prepares/run/sshd, generates missing SSH host keys, merges SSHD session environment into a runtime SSHD config, renders transfer policy, validates the result, and execs containersshd.
The gateway and container agent coordinate through environment variables and private state files:
Access Flow execution context is not an environment variable. The gateway serializes its optional literal value into the private generated container-agent configuration, and the agent compiles it into the immutable relay plan.
AW_IDENTITY_TOKEN: generated or inherited by the gateway. It is provisioned only when a bearer Access Flow relay or an explicit service inheritance requires it; the relay may map it to its configured agent variable.AW_CONTAINER_CONTROL_TOKEN: generated per container and passed only to the container agent for mutating control-socket requests. Mutating requests fail closed when this token is absent.AW_AUTHENTICATED_UIDandAW_AUTHENTICATED_GID: authenticated host user identity used by the container agent for peer validation and service-user handling. If either variable is present, both must be present and numeric.AW_CONTAINER_STATE_DIR: in-container durable state path for generated agent config, logs, SSH policy snippets, and session data.AW_CONTAINER_AGENT_ALLOW_PROCESS_REAP=1: enables actual process reaping; without it, reaping reports remain dry-run.AW_SSHD_POLICY_CONFIG: generated SSH transfer-policy file consumed by container SSHD helper scripts.AW_SSHD_AUTHORIZED_KEYS_FILE: absolute in-container path to the gateway-managed authorized-keys file. AW Gateway injects the computed value into the conventionalcontainer-sshdservice, andstart-container-sshdreplaces the globalAuthorizedKeysFiledirective with it.AW_SSHD_SETENV_CONFIG: generated SSHDSetEnvsnippet for configured session environment variables. When set, the helper requires this file to be readable and merges its values with basesshd_config_agentSetEnvdefaults into one globalSetEnvdirective; generated values win by key.
The SSHD helper also supports test/override hooks:
AW_SSHD_BASE_CONFIG, AW_SSHD_RUNTIME_CONFIG, AW_SSHD_RUN_DIR,
AW_SSHD_DRY_RUN_CONFIG, and AW_SSH_COMMAND_FILTER.
Template variables are scoped to the phase that renders a field. Loader paths and config identifiers remain literal so includes, inheritance, validation, merging, and references are deterministic.
| Field group | Render phase | Supported variables |
|---|---|---|
target.identity.* |
Gateway identity resolution | {user}, {uid}, {gid}, {home} |
target.container_home |
Gateway identity resolution | {user}, {uid}, {gid}, {home} |
target.name, target.ephemeral_name |
Gateway container identity | {image_slug}, {session_id} for ephemeral names, and declared {context.<name>} keys |
target.workspace.path |
Gateway target resolution | {user}, {uid}, {gid}, {home}, {target}, {image}, {image_slug}, {session_id}, and declared {context.<name>} keys |
target.workspace.container_path |
Gateway workspace runtime resolution | {user}, {uid}, {gid}, {container_user}, {container_home}, {target}, {image}, {image_slug}, {container_name}, {session_id}, and declared {context.<name>} keys |
target.workspace.state_dir |
Gateway workspace runtime resolution | {user}, {uid}, {gid}, {target}, {image}, {image_slug}, {container_name}, {session_id}, and declared {context.<name>} keys |
target.container_env, target.session_env, target.container_mounts.*, target.runtime.extra_run_args, target.container_bootstrap.*, target.container_bootstrap_steps.* |
Gateway runtime resolution | Gateway vars except {container_pid} |
target.session_env_inherit |
Gateway runtime resolution | Env key names only; no template interpolation |
target.lifecycle_steps[].command |
Gateway lifecycle execution | Pre-start supports gateway vars except {container_pid}; later phases support all gateway vars |
target.host_steps[].command and HTTP health-check URLs |
Gateway host-step execution | All gateway vars, including {container_pid} |
container_agent.services[].user in gateway config |
Gateway-managed agent config render | {container_user} |
container_agent.services[].command, cwd, literal env value, env file paths, and health-check URL |
Container-agent service execution | {container_state_dir} |
container_agent.control_socket and ssh_bridge.socket in standalone agent config |
Container-agent startup | {container_state_dir} |
launch.cwd, launch.command, launch.env, and launch.steps[] command/cwd/env |
Launch execution | Launch built-ins plus {var.<name>} |
client_config.inner_alias_template, container_host_template, default_identity_dir |
Client config generation | {user}, {uid}, {gid}, {home}, {container_user}, {container_home}, {workspace}, {state}, {state_dir}, {target}, {image}, {image_slug}, {container_name}, {container_state_dir}, {container_state_dir_in_container}, {session_id}, {host} |
target.control_sockets.host_dir, target.control_sockets.container_dir |
Gateway runtime socket resolution | {user}, {uid}, {gid}, {home}, {target}, {image}, {image_slug}, {container_name}, {session_id}, {runtime_id}, and declared {context.<name>} keys |
logging.directory |
Gateway logging startup | {user}, {uid}, {gid}, {home}, {workspace}, {state}, {state_dir}, and declared {context.<name>} keys |
Container-agent logging.directory |
Container-agent logging startup | {container_state_dir} |
runtime.docker_host |
Runtime initialization | {user}, {home} |
Gateway vars are {user}, {uid}, {gid}, {home}, {container_user},
{container_home}, {workspace}, {state}, {state_dir}, {target},
{image}, {image_slug}, {container_name}, {container_state_dir},
{container_state_dir_in_container}, {session_id}, and, after the container
starts, {container_pid}. Launch built-ins are the gateway vars available at
launch execution time; caller variables use {var.<name>}.
target.container_home must render to an absolute path. Literal absolute
templates such as /home/{user} are valid, and {home} may also be used as
the absolute leading path segment.
{session_id} is available only when an ephemeral session is active or an
explicit --session-id was supplied. Rendering a template that uses
{session_id} for a fixed target without a session id fails.
Gateway and agent logs are configured in TOML:
[logging]
level = "info"
directory = "{state}/logs/gateway"
max_bytes = 104857600
max_files = 5
console = falsemax_bytes accepts a raw byte integer; 104857600 is 100 MB. max_files
must not exceed 1024. File logging creates log directories with mode 0700
and active log files with mode 0600.
console = true writes structured logs to stderr. console = false disables
that console writer, so diagnostics go only to configured file logging and
explicit stderr messages.
Protocol and proxy paths keep stdout quiet. Diagnostics go to stderr or the
configured log files. The minimal gateway sample uses console logging; managed
deployment examples usually keep gateway file logs under target workspace
state. Gateway log directories can interpolate {user}, {uid}, {gid},
{home}, {workspace}, {state}, and {state_dir}. Container-agent log
directories can interpolate {container_state_dir}. Container service
stdout/stderr is captured under the container state log directory with the
configured rotation limits.
For managed deployments, resolve the target state path and follow the gateway
log with a command such as tail -F <state>/logs/gateway/gateway.log; exact
paths and rotated filenames depend on the rendered logging config.
Control socket directories can interpolate {user}, {uid}, {gid},
{home}, {target}, {image}, {image_slug}, {container_name},
{session_id}, and {runtime_id}.
The user-facing ingress paths differ, but they converge on the same target resolution, readiness, operation execution, and cleanup machinery.
In managed-server mode, host SSH authenticates the user and then invokes the gateway. The user's SSH client is ultimately connected to container-local SSH, not to the host shell or host filesystem.
sequenceDiagram
autonumber
participant C as SSH client
participant H as Host sshd
participant G as aw-gateway
participant R as Container runtime
participant A as aw-container-agent
participant S as Container sshd
C->>H: SSH authenticate as host user
H->>G: ForceCommand / ProxyCommand stream
G->>G: Resolve target and acquire lifecycle lock
G->>R: Inspect target container
alt Container missing or stopped
G->>G: Run configured pre_start steps
G->>R: Start container
G->>G: Run configured post_start host steps
G->>A: Wait for agent/control readiness
else Container already running
G->>A: Validate configured readiness
end
A->>S: Supervise container sshd service
A-->>G: Expose SSH bridge socket
G-->>C: Proxy bytes between client and container SSH
The HTTP API is a non-interactive JSON ingress path into the same gateway operation layer used by CLI and SSH management actions.
sequenceDiagram
autonumber
participant C as HTTP client
participant G as aw-gateway http
participant O as GatewayOperation
participant R as Container runtime
participant A as aw-container-agent
C->>G: POST /api/v1/run or /launches/{name}/run
G->>G: Authenticate request and check http.enabled_actions
G->>O: Build operation with wait or detach mode
O->>R: Resolve target and ensure container readiness
R->>A: Wait for configured services/control readiness
alt wait mode
G->>G: Spawn operation task and hold disconnect cancel guard
O->>R: Execute cancelable command and capture stdout/stderr
G-->>C: 200 JSON with exit_code/stdout/stderr
else detach mode
O->>R: Start background operation with session guard
G-->>C: 202 JSON with operation_id
end
Custom deployment hooks are represented as configured lifecycle/host steps. They can prepare storage, create workspaces, install firewall rules, or perform site-specific setup, but the native gateway lifecycle remains the same.
flowchart TD
A[Load and validate gateway config] --> B[Resolve user, target, workspace, state]
B --> C[Acquire target lifecycle lock]
C --> D{Managed container exists?}
D -- no --> E[Run configured pre_start steps]
D -- yes, stopped --> E
D -- yes, running --> I[Validate labels]
E --> F[Create identity/control tokens when needed]
F --> G[Render container-agent config when enabled]
G --> H[Run container through configured runtime]
H --> I
I --> J[Run configured post_start host steps]
J --> K{Container agent enabled?}
K -- yes --> L[Wait for services and control socket readiness]
K -- no --> M[Container lifecycle ready]
L --> N{SSH endpoint configured?}
N -- socket bridge --> O[Validate bridge socket]
N -- published port --> P[Wait for loopback SSH port]
N -- no --> M
O --> M
P --> M
Cleanup is target policy. A target can keep running, stop after the last
session, ask the container agent to reap non-preserved processes, and for
ephemeral target-specific workspaces optionally remove the resolved session
workspace. Preserve processes such as tmux or screen can keep a container
alive when configured.
stateDiagram-v2
[*] --> SessionActive
SessionActive --> LastSessionExited: gateway stream or run command exits
LastSessionExited --> KeepRunning: stop_when_idle = false
LastSessionExited --> GracePeriod: stop_when_idle = true
GracePeriod --> KeepRunning: another session starts
GracePeriod --> PreserveCheck: grace timer expires
PreserveCheck --> KeepRunning: preserved process found
PreserveCheck --> StopContainer: action = exit_container
PreserveCheck --> ReapProcesses: action = reap_processes
PreserveCheck --> KeepRunning: action = none
ReapProcesses --> KeepRunning: agent stops non-preserved process trees
StopContainer --> [*]: stop or remove per target config
KeepRunning --> [*]
Local mode does not require host SSHD. The gateway can start a target and bind a loopback-only listener for local SSH-compatible tools. Docker, Colima, and Apple Container can use a published loopback container SSH port instead of a host-visible Unix socket.
sequenceDiagram
autonumber
participant U as User
participant G as aw-gateway up
participant R as Container runtime
participant A as aw-container-agent
participant T as Local SSH tool
U->>G: aw-gateway up <target> --json
G->>R: Start or reuse target container
G->>A: Wait for configured readiness
alt socket backend
A-->>G: SSH bridge socket ready
else published_port backend
R-->>G: Loopback SSH port ready
end
G-->>U: Print readiness JSON and generated SSH config path
T->>G: Connect to loopback listener
G-->>T: Proxy to container SSH endpoint
U->>G: Ctrl-C or process exit
G->>G: Apply configured idle cleanup policy
cargo fmt --check
cargo test
cargo clippy --all-targets --all-features
cargo build --releaseReleases are driven from Cargo.toml, Cargo.lock, and CHANGELOG.md.
Use current when Cargo.toml already has the intended release version, or
use patch, minor, major, or an explicit semantic version:
node scripts/release.mjs current
node scripts/release.mjs patch
node scripts/release.mjs minor
node scripts/release.mjs major
node scripts/release.mjs 0.2.3The script stamps the changelog, commits Release vX.Y.Z, creates and pushes a
matching git tag, creates a GitHub release with notes from the changelog,
then commits a fresh Unreleased section for the next cycle.
If GitHub release creation fails after the commit and tag are pushed, recover by creating the release manually for the existing tag instead of rerunning the script.
Release binaries are packaged separately after the platform binaries have been provided or built by the release operator. Supported release platforms currently use archive names like:
aw-gateway-VERSION-linux-x86_64.tar.gz
aw-gateway-VERSION-linux-arm64.tar.gz
aw-gateway-VERSION-macos-arm64.tar.gz
Each archive should contain one top-level directory named
aw-gateway-VERSION-PLATFORM with:
bin/aw-gateway- gateway executable for that platform.bin/aw-container-bootstrap- container bootstrap executable for that platform.bin/aw-container-agent- container supervisor executable for that platform.bin/aw-ssh-command-filter- SSH command filter executable for that platform.examples/- runtime-specific deployment configs, including the SSHD startup helper and SSHD config used by each guide.README.mdLICENSECHANGELOG.mdaw-gateway.sample.tomlcontainer-agent.sample.tomldocs/assets/
Archives are single-platform build outputs. Do not put Linux runtime binaries
inside a macOS archive. Mixed host/container deployments are assembled by
installing files from multiple archives. For example, a macOS arm64 host with a
native Apple Silicon Colima profile uses bin/aw-gateway from the macOS arm64
archive and the container-side binaries from the Linux arm64 archive.
Example packaging flow for one platform:
VERSION=0.6.0
PLATFORM=linux-x86_64
BIN_DIR=/path/to/platform-binaries
STAGE="$(mktemp -d)"
ROOT="aw-gateway-${VERSION}-${PLATFORM}"
mkdir -p "$STAGE/$ROOT/bin"
install -m 755 "$BIN_DIR/aw-gateway" "$STAGE/$ROOT/bin/aw-gateway"
install -m 755 "$BIN_DIR/aw-container-bootstrap" \
"$STAGE/$ROOT/bin/aw-container-bootstrap"
install -m 755 "$BIN_DIR/aw-container-agent" \
"$STAGE/$ROOT/bin/aw-container-agent"
install -m 755 "$BIN_DIR/aw-ssh-command-filter" \
"$STAGE/$ROOT/bin/aw-ssh-command-filter"
cp README.md LICENSE CHANGELOG.md aw-gateway.sample.toml \
container-agent.sample.toml "$STAGE/$ROOT/"
cp -R docs examples assets "$STAGE/$ROOT/"
tar -C "$STAGE" -czf "${ROOT}.tar.gz" "$ROOT"
rm -rf "$STAGE"Repeat that staging step for each platform, for example linux-x86_64 and
macos-arm64, using binaries built for that archive platform. After the
GitHub Release exists, upload the archives:
RELEASE_TAG="v${VERSION}"
gh release upload "$RELEASE_TAG" \
"aw-gateway-${VERSION}-linux-x86_64.tar.gz" \
"aw-gateway-${VERSION}-linux-arm64.tar.gz" \
"aw-gateway-${VERSION}-macos-arm64.tar.gz"src/bin/- binary entrypoints.src/config.rs- TOML schema, defaults, validation, and sample config.src/gateway.rsandsrc/gateway/- host-side CLI behavior, runtime orchestration, sessions, listeners, identity, and client config.src/agent.rsandsrc/agent/- in-container agent entrypoint, service supervision, control socket dispatch, SSH bridge, idle cleanup/reaper, process helpers, shared state, socket helpers, and status projection.src/config/- focused config support modules for targets, launches, includes, root inheritance, steps, agent config, validation, and template resolution.src/runtime.rsandsrc/runtime/- Podman, Docker, Colima, and Apple container command construction and runtime support.src/ssh_dispatch.rs-SSH_ORIGINAL_COMMANDparsing and restricted SSH dispatch.src/logging.rs- tracing setup and rotating log files.assets/- deployable host helpers and image integration files.tests/- deterministic tests for config, CLI, runtime rendering, assets, SSH dispatch, and control socket behavior.