Amber is a compiler that turns declarative “agent system” manifests (agents, LLM routers, MCP tools, HTTP services, etc.) into a fully linked, type-checked runtime graph (“Scenario”), then emits artifacts to understand it (diagrams) and run it (Docker Compose).
flowchart LR
%% Inputs
M["Component manifests (JSON5)<br/>program + components + slots + provides + bindings + exports"] --> R
%% Compiler pipeline
subgraph Compiler["amber-compiler pipeline"]
R["Resolve tree<br/>(fetch URLs, apply environments/resolvers,<br/>cache by digest)"] --> L
L["Link + validate<br/>(typecheck bindings,<br/>validate config_schema,<br/>resolve exports)"] --> O
O["Optimize passes<br/>(DCE + flatten)"] --> S["Scenario<br/>(fully linked graph)"]
end
%% Outputs
S --> IR["Scenario IR (JSON)<br/>primary output"]
S --> DOT["Graph (Graphviz DOT)<br/>--dot"]
S --> DC["Docker Compose (YAML)<br/>--docker-compose/--compose"]
DC --> RUN["docker compose up<br/>(run the system)"]
%% Note about check vs compile
CHECK["amber check<br/>(lints + link errors)<br/>(no artifacts)"] -.-> R
COMPILE["amber compile<br/>(artifacts)"] -.-> R
flowchart TB
subgraph Parent["Parent manifest (composite component)"]
Pslots["Parent slots<br/>(what parent needs)"]
Pkids["Parent components<br/>(#router, #agent, #tools, ...)"]
Pbind["Parent bindings<br/>wire provides -> slots"]
Pexp["Parent exports<br/>what the parent exposes upward"]
end
subgraph ChildA["Child A (provider)"]
Aprov["provides:<br/>llm (kind=llm)<br/>admin_api (kind=mcp)"]
end
subgraph ChildB["Child B (consumer)"]
Bslot["slots:<br/>llm (kind=llm)<br/>admin_api (kind=mcp)"]
Bprog["program:<br/>entrypoint/env can interpolate<br/>\${slots.*} + \${config.*}"]
end
ChildA -->|"bind: ChildA.llm → ChildB.llm"| ChildB
ChildA -->|"bind: ChildA.admin_api → ChildB.admin_api"| ChildB
Bslot --> Bprog
Aprov --> Pexp
L1["Rule: slot kind/profile must match provide kind/profile"]
L2["Rule: every declared slot must be bound (unless optional exists in future)"]
This is the “tau2” example manifest graph compiled into a Scenario:
flowchart LR
Tau2["tau2 (OpenEnv)<br/>provides: env (mcp/openenv)"]
GR["green_router (LiteLLM)<br/>provides: llm (llm)<br/>+ admin_api (mcp via wrapper)"]
PR["purple_router (LiteLLM)<br/>provides: llm (llm)<br/>+ admin_api (mcp via wrapper)"]
Purple["purple (tau2-agent)<br/>slots: llm<br/>provides: a2a"]
Green["green (tau2-evaluator)<br/>slots: env, llm, agent, admin_api<br/>provides: results (a2a)"]
Export["Scenario export:<br/>green (a2a)"]
Tau2 -->|"env (mcp/openenv)"| Green
GR -->|"llm (llm)"| Green
PR -->|"llm (llm)"| Purple
Purple -->|"a2a (a2a)"| Green
PR -->|"admin_api (mcp)"| Green
Green --> Export
The Docker Compose reporter emits two services per runnable component:
- a sidecar (sets firewall rules + runs local TCP proxies)
- the program container (your actual image), sharing the sidecar network namespace
Sidecars run the published image ghcr.io/rdi-foundation/amber-sidecar:main.
flowchart LR
Host["Host / developer<br/>(127.0.0.1)"] -->|"Scenario exports<br/>published on loopback<br/>(ports 18000+)"| SidecarP
subgraph Mesh["Docker Compose network (dynamic subnet + DNS)"]
subgraph Provider["Provider component (cX)"]
SidecarP["cX-net (sidecar)<br/>- DNS name<br/>- iptables allowlist (DNS-resolved)<br/>- publishes exports"]
ProgP["cX (program)<br/>network_mode: service:cX-net<br/>(listens on provide ports)"]
ProgP --- SidecarP
end
subgraph Consumer["Consumer component (cY)"]
SidecarC["cY-net (sidecar)<br/>- socat local proxies<br/>127.0.0.1:20000 -> cX-net:PORT"]
ProgC["cY (program)<br/>uses \${slots.*}<br/>(e.g. http://127.0.0.1:20000)"]
ProgC --- SidecarC
end
SidecarC -->|"TCP proxy<br/>(bound capability only)"| SidecarP
end
Note["Effect:<br/>Programs only talk to dependencies via loopback slot URLs.<br/>Unbound inbound traffic is dropped by sidecar firewall."]
- Rust toolchain:
nightly-2025-10-30(pinned inrust-toolchain.toml) - Docker + Docker Compose: needed to run the emitted
docker-compose.yaml - Graphviz (optional): to render
.dotfiles into SVG/PNG
If you don't want to build from source, download the latest nightly artifacts (linux/amd64, linux/arm64, macos/arm64) from:
https://github.com/RDI-Foundation/amber/actions/workflows/publish.yaml?query=branch%3Amain
Open the most recent amber-publish run and download the amber-cli-<platform>.tar.gz artifact. Each archive contains the amber binary.
Example:
tar -xzf amber-cli-linux-amd64.tar.gz
./amber --help# From repo root
cargo buildIf you don’t have the pinned nightly installed:
rustup toolchain install nightly-2025-10-30
# optional but convenient if you want to force it:
rustup override set nightly-2025-10-30cargo run -p amber-cli -- --helpcargo run -p amber-cli -- check examples/tau2/scenario.json5To treat warnings as errors (useful in CI):
cargo run -p amber-cli -- check -D warnings examples/tau2/scenario.json5amber compile requires at least one output flag (--output, --dot, --docker-compose, or --bundle).
rm -rf target/amber-out
cargo run -p amber-cli -- \
compile \
--output target/amber-out/scenario \
--dot target/amber-out/scenario.dot \
--docker-compose target/amber-out/scenario.docker-compose.yaml \
examples/tau2/scenario.json5Expected outputs (for the paths above):
target/amber-out/scenario— primary output (Scenario IR JSON)target/amber-out/scenario.dot— graph diagram (Graphviz DOT)target/amber-out/scenario.docker-compose.yaml— runnable Docker Compose
Render the DOT to SVG:
dot -Tsvg target/amber-out/scenario.dot -o target/amber-out/scenario.svgdocker compose -f target/amber-out/scenario.docker-compose.yaml upFinding the host ports for Scenario exports:
- Exports are published on host loopback (
127.0.0.1) starting at port 18000. - The compose file includes
x-amber.exports, a mapping ofexport_name -> published_host/published_port/target_portplus component/provide/endpoint. - Each
*-netsidecar service also carries anamber.exportslabel with the same JSON mapping for runtime discovery.
You can still locate the raw publishes under each *-net sidecar service’s ports: section:
rg -n "127.0.0.1:18" target/amber-out/scenario.docker-compose.yaml-
Manifest (authored input, JSON5):
- describes one component
- may contain child components
- declares slots (inputs) and provides (outputs)
- wires things via bindings
- exposes a public interface via exports
-
Scenario (compiler output):
- a fully linked component tree with stable component IDs
- every binding is resolved to concrete
(component, capability)→(component, slot) - scenario exports are resolved to concrete provides with their capability type
- suitable for deterministic tooling (IR/graph/runtime emit)
A capability is typed by:
kind: one ofmcp|llm|http|a2a- optional
profile(commonly formcp, e.g.openenv)
Linking rule: a slot can only be bound by a provide with the same (kind, profile).
A binding expresses:
(<provider>.<provide>) -> (<consumer>.<slot>)
weak: true means:
- the edge is still a real wiring edge (it will still get proxied / allowed)
- but it does not participate in dependency ordering / cycle detection
This is important when you have “peer-ish” connections that shouldn’t force startup ordering.
Manifests can declare config_schema (JSON Schema, constrained to Amber's supported profile; see manifest/README.md). When a parent instantiates that component
with components.<name>.config, the compiler validates the config against the schema at link time.
Schema nodes can be annotated with secret: true to mark secret values; children inherit the secret marker.
Programs can interpolate:
${config.<path>}from the component’s config${slots.<slot>.<field>}from the resolved slot value
For Docker Compose output, slot fields include:
url(currently emitted ashttp://127.0.0.1:<local_port>)host(usually127.0.0.1)port(local proxy port)
If you need a non-HTTP scheme, prefer composing your own value using host and port.
This is a Rust workspace:
-
cli/(amber-cli)amberexecutable:check,compile,docs manifest -
compiler/(amber-compiler) resolve tree → link/validate → passes → reporters Key modules:frontend: resolve manifests (URLs, envs/resolvers, caching, cycle detection)linker: typecheck bindings + exports, validate config schema, ensure slots are boundpasses: graph rewrites (currently DCE + flatten)reporter: emit Scenario IR / DOT / Docker Compose YAML
-
manifest/(amber-manifest) manifest parsing + validation + linting Full format docs live inmanifest/README.md(also printed viaamber docs manifest) -
scenario/(amber-scenario) Scenario graph types + graph utilities + Scenario IR conversions -
docker/amber-sidecar/Dockerfile for the published sidecar image used by the Docker Compose reporter -
docker/amber-compose-helper/Dockerfile for the helper image used to render runtime config in Docker Compose output -
examples/config-forwarding/: root config forwarding into a child componenttau2/: end-to-end agent/evaluator/router/env compositionreexport/: minimal example demonstrating export forwarding through components
-
node/(amber-node) placeholder crate for future standalone runtime functionality
- Implement
Reporterincompiler/src/reporter/*. - Wire it into
amber-clias a--flagunderCompileArgs. - Add a CLI test under
cli/tests/compile_outputs.rsor a new one.
- Implement
ScenarioPass(compiler/src/passes/mod.rs). - Register it in the pass manager sequence (currently DCE then flatten).
- Ensure you preserve scenario invariants (
Scenario::assert_invariants()is used in debug).
You’ll need to update:
- manifest types / parsing (
amber-manifest) - any link-time compatibility checks (
amber-compilerlinker) - tooling assumptions (e.g., Docker Compose reporter may have protocol limitations)
Run everything:
cargo testCLI-focused tests:
cargo test -p amber-cliUI tests live in cli/tests/ui/:
- each
*.json5case has an expected*.stderr(and optionally*.stdout) - tests run
amber checkand compare normalized output
Docker-backed tests are ignored by default (they require Docker):
cargo test -p amber-compiler --all-features docker_smoke_ocap_blocks_unbound_callers -- --ignored --test-threads=1
cargo test -p amber-compiler --all-features docker_smoke_config_forwarding_runtime_validation -- --ignored --test-threads=1
cargo test -p amber-compose-helper --all-features helper_image_executes_run_plan_in_scratch -- --ignored --test-threads=1- Docker Compose slot wiring is implemented as TCP forwarding (via
socat) plus an iptables allowlist in each sidecar. - If two provides route to different endpoints on the same port, Compose emission will fail (an L4 backend can’t separate them).