Skip to content

Repository files navigation

Agent Workspaces Gateway

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.

Contents

Why It Exists

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.

Features

  • 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.

Install

Download the latest archive for your platform from GitHub Releases:

https://github.com/kcosr/aw-gateway/releases

Supported release platforms are currently:

  • linux-x86_64
  • linux-arm64
  • macos-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.

Quickstart

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:

  1. Download and extract the latest release archive for your host platform and, when needed, the Linux archive for your container or VM platform.
  2. Pick a deployment guide: Podman, Docker, Colima, or Apple Container.
  3. Install the host and container-side runtime files using that guide's layout.
  4. Copy or adapt the guide's gateway config and validate it.
  5. Start a target with aw-gateway --config <path> up <target> --json.
  6. Generate client config and connect with OpenSSH, SCP, SFTP, or VS Code.

Core Concepts

  • 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_steps are host hooks tied to start/stop phases. host_steps run on the host after agent readiness. container_bootstrap_steps run inside the container before the agent starts.
  • OpenSSH ForceCommand makes host SSHD run the gateway instead of the user's shell. OpenSSH ProxyCommand lets a local SSH client tunnel through the authenticated host connection into container SSH.

Binaries

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 SSHD ForceCommand helper 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
Loading

Build

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 --release

The 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.

Deployment Guides

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 container deployment 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.

Configuration

Gateway config lookup uses this precedence:

  1. --config PATH
  2. AW_GATEWAY_CONFIG
  3. User config file, when present: {AW_GATEWAY_CONFIG_HOME|XDG_CONFIG_HOME|~/.config}/aw-gateway/gateway.toml
  4. 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.toml

Once installed on PATH, validate the configs:

aw-gateway --config ./gateway.toml config validate
aw-container-agent --config ./container-agent.toml config validate

On 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 --json

Config 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.

SSH Workflows

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"]
Loading

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 no

Managed 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 command

Commands 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-dev

The 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-dev

The 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 Workstation Mode

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 default

SSH-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 = false

Use 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 --json

The generated SSH config is written under the workspace state directory and can be used by SSH-compatible tools.

Container Agent Services

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.pem

The 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.

Remote Access Flow Rotation

Server identity and relay trust are independent atomic generations. When the issuing CA changes, rotate them in this order:

  1. Issue a currently valid server-authentication certificate whose SAN matches the configured server_name. Keep its private key on the Proxy host.
  2. Atomically replace the relay PEM bundle with one bounded bundle containing both the old and new trust anchors. Send SIGHUP to aw-container-agent, not the foreground aw-gateway, and verify aggregate agent readiness.
  3. Atomically replace the Proxy certificate chain and matching private key, send SIGHUP to ACL Proxy, and verify Proxy readiness plus a real HTTP and HTTPS Access Flow through every relay population.
  4. After every relay has loaded the overlap bundle and old flows have drained, atomically replace the relay bundle with the new anchor only, send SIGHUP to 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 To Remote TLS Cutover

Unix and TLS/TCP are strict transport alternatives, not availability fallbacks. To move an existing route:

  1. Start and validate the remote Proxy TLS listener, server identity, workload bearer resolver, and deny-by-default policy before changing the relay.
  2. 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.
  3. Build and validate one complete agent configuration that replaces each selected route's unix transport with tls_tcp. Preserve its loopback listener and allowed destination ports; do not author alternate endpoints.
  4. 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.

Gateway Config Shape

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_container

Docker 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 = false

Replace 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 ordered use = [...].

  • [target_defaults.workspace]: default host workspace path, optional container_path mount 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 and format = "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 for pre_start, post_start_host, pre_stop, and post_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 ordered use = [...].

  • 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 uses source, target, and mode ("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 through container_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. Set sftp = "deny" to block SFTP and modern OpenSSH SCP. Set legacy_scp = "deny", "inbound", or "outbound" to control legacy scp -t/scp -f server 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 by aw-container-agent. Use depends_on and dependency health checks to order managed services. Required container_bootstrap_steps perform 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"

Host Socket Exposures

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 | outbound

Target 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].

Config Composition Order

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
Loading

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 Config Extends

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_exposures entry replaces the inherited entry as a whole rather than merging individual path or relabel fields.
  • container_agent.access_flow_relay replaces 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_context scalar, container_mounts, runtime argument arrays, command arrays, dependency arrays, allow-list arrays, and launch variable value arrays.
  • Named service arrays merge by name at target_defaults.container_agent.services, target_templates.<name>.container_agent.services, and targets.<name>.container_agent.services.
  • Named step arrays merge by name for target lifecycle, host, and container bootstrap steps, and for launch steps in 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 (Named Command Templates)

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:

  1. Load, include, and validate config.
  2. Resolve the named launch.
  3. Validate supplied --var key=value values, passthrough args, and apply defaults.
  4. Resolve and prepare the configured target.
  5. Run the existing target lifecycle, readiness checks, and target host_steps.
  6. Run launch post_ready steps in TOML order.
  7. Execute the final command inside the ready container.
  8. 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.

CLI

aw-gateway [--config PATH] [--log-level LEVEL] <command>

Global options:

  • --config PATH: gateway config path. Also available as AW_GATEWAY_CONFIG.
  • --log-level LEVEL: override configured gateway log level. Also available as AW_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; use up to start or hold a target without running a command. Foreground runs interrupted by SIGINT, SIGTERM, or SIGHUP are 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 configured identity.session_shell through 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 with allow_args = true and are spliced at the configured {args} argv element. Foreground launches interrupted by SIGINT, SIGTERM, or SIGHUP are 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 not never; 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 existing aw-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. --all cannot 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 with http listener is disabled in config when [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. --reset is equivalent to reset-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_keys file and the target container authorized-key file.
  • add-host-key: add one SSH public key to the user's host ~/.ssh/authorized_keys file.
  • 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 --help so 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-key prompts 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 as AW_CONTAINER_AGENT_CONFIG.
  • --log-level LEVEL: override configured container-agent log level. Also available as AW_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 as AW_CONTAINER_AGENT_CONFIG.
  • --bootstrap-config PATH: rendered bootstrap config path. Also available as AW_CONTAINER_BOOTSTRAP_CONFIG.
  • --log-level LEVEL: override configured container-agent log level. Also available as AW_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.

HTTP API

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.

Assets

The assets/ directory contains deployable helpers and image files:

  • assets/acl-proxy.example.toml: starter acl-proxy allowlist 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-conf arguments.
  • assets/copy-skel: copies top-level deployed skel files into a workspace without overwriting existing files. It takes explicit --skel-dir and --workspace arguments.
  • assets/copy-workspace-template: copies a workspace template into an empty gateway-owned workspace path. It takes explicit --template, --dest, and optional repeated --exclude arguments.
  • 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 container sshd.

Runtime Environment Contracts

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_UID and AW_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 conventional container-sshd service, and start-container-sshd replaces the global AuthorizedKeysFile directive with it.
  • AW_SSHD_SETENV_CONFIG: generated SSHD SetEnv snippet for configured session environment variables. When set, the helper requires this file to be readable and merges its values with base sshd_config_agent SetEnv defaults into one global SetEnv directive; 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

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.

Logging

Gateway and agent logs are configured in TOML:

[logging]
level = "info"
directory = "{state}/logs/gateway"
max_bytes = 104857600
max_files = 5
console = false

max_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}.

Lifecycle Diagrams

The user-facing ingress paths differ, but they converge on the same target resolution, readiness, operation execution, and cleanup machinery.

Managed SSH Attach

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
Loading

HTTP API Operation

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
Loading

Container Startup And Readiness

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
Loading

Session Exit, Grace Period, And Cleanup

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 --> [*]
Loading

Local Workstation Listen Mode

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
Loading

Development

cargo fmt --check
cargo test
cargo clippy --all-targets --all-features
cargo build --release

Release

Releases 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.3

The 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.md
  • LICENSE
  • CHANGELOG.md
  • aw-gateway.sample.toml
  • container-agent.sample.toml
  • docs/
  • 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"

Project Structure

  • src/bin/ - binary entrypoints.
  • src/config.rs - TOML schema, defaults, validation, and sample config.
  • src/gateway.rs and src/gateway/ - host-side CLI behavior, runtime orchestration, sessions, listeners, identity, and client config.
  • src/agent.rs and src/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.rs and src/runtime/ - Podman, Docker, Colima, and Apple container command construction and runtime support.
  • src/ssh_dispatch.rs - SSH_ORIGINAL_COMMAND parsing 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.

About

Convenience gateway for disposable or reusable container workspaces with CLI, SSH, and JSON HTTP interfaces; starts/reuses targets, supervises in-container services, and bridges clients to container-local SSH and operations.

Resources

Stars

41 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages