Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
a412f3a
phase2/mcp-reconciler: full McpServer reconciler + helm CRD + route m…
pallakatos Apr 27, 2026
4dcfb24
phase2/toolpolicy-reconciler: full ToolPolicy reconciler + AGT profil…
pallakatos Apr 27, 2026
927df61
phase2/a2aagent-reconciler: full A2AAgent reconciler + AgentCard comp…
pallakatos Apr 27, 2026
9766201
phase2/inferencepolicy-reconciler: full InferencePolicy reconciler + …
pallakatos Apr 27, 2026
d7914fd
phase2/clawmemory-reconciler: full ClawMemory CRD + binding compile +…
pallakatos Apr 27, 2026
5afc008
phase2/claweval-reconciler: full ClawEval CRD + binding compile + hel…
pallakatos Apr 27, 2026
a73e47e
phase2-overlaymode: sigs/agent-sandbox OverlayMode reconciler branch …
pallakatos Apr 27, 2026
b598bf6
phase2-attest-cli: ship `azureclaw attest <name>` read-surface comman…
pallakatos Apr 27, 2026
19ba11a
phase2-attest-baseline: drift-aware --baseline diff for `azureclaw at…
pallakatos Apr 28, 2026
d090bcc
phase2-migrate-mode-switch: ship `azureclaw migrate` mode-switch CLI …
pallakatos Apr 28, 2026
b838415
phase2-convert-translator: ship real azureclaw convert (#63)
pallakatos Apr 28, 2026
0816458
phase2-migrate-from-kagent: ship `azureclaw migrate from-kagent` (#64)
pallakatos Apr 28, 2026
bfd71dc
phase2-multi-runtime-crd: spec.runtime discriminated union (S10.A1) (…
pallakatos Apr 28, 2026
419c8ea
phase2-multi-runtime-dispatch: RuntimeDeploymentPlan seam (S10.A2) (#66)
pallakatos Apr 28, 2026
2074be2
S10.A2.b: BYO end-to-end deployment + raw_env (#67)
pallakatos Apr 28, 2026
46cf5af
S10.B: phase2-platform-mcp-server — Foundry-shim discovery surface (#68)
pallakatos Apr 28, 2026
1ac5c15
phase2-runtime-openai-agents: first non-OpenClaw native runtime (S10.…
pallakatos Apr 28, 2026
b3950d3
phase2-runtime-microsoft-agent-framework: second native runtime (S10.…
pallakatos Apr 28, 2026
15f6f90
S10.A5: phase2-runtime-cli — operator-facing CLI for multi-runtime ho…
pallakatos Apr 28, 2026
3e7bd3f
S7.A: phase2-conditions-ssa-leader — stable SSA field managers (first…
pallakatos Apr 28, 2026
cde3a3d
S7.B: emit Progressing Condition on every ClawSandbox status path (#73)
pallakatos Apr 29, 2026
dacf327
S7.C: controller-wide leader election (#74)
pallakatos Apr 29, 2026
b0d95b4
Phase 2 / S7.D: bounded jitter on reconcile-error requeues (#75)
pallakatos Apr 29, 2026
9555272
Phase 2 / S7.E: controller workqueue metrics (#76)
pallakatos Apr 29, 2026
699554e
Phase 2 / S7.F: Content-Safety floor admission policy (#77)
pallakatos Apr 29, 2026
a5cb025
Phase 2 / S17.A: npm audit as permanent CI gate (#78)
pallakatos Apr 29, 2026
ff13fdf
Phase 2 / S7.E.2: reconcile-duration histograms + outcome counter (#79)
pallakatos Apr 29, 2026
56c7f04
Phase 2 / S15.a: handoff CLI hotspot decomposition (1119 -> 798 LOC) …
pallakatos Apr 29, 2026
567af09
S15.b: decompose cli/src/commands/mesh.ts (1583 → 667 LOC) (#81)
pallakatos Apr 29, 2026
0190045
S15.c: decompose inference-router/src/routes/inference.rs (1359 → 776…
pallakatos Apr 29, 2026
7a8f2a6
S15.d.1: extract up.ts --upgrade fast-path (1849 → 1660 LOC) (#83)
pallakatos Apr 29, 2026
2b18cd3
S15.d.2: extract up.ts preflight phase (1660 → 1296 LOC) (#84)
pallakatos Apr 29, 2026
83aad87
S15.d.3: extract up.ts AgentMesh deploy phase (1296 → 1182 LOC) (#85)
pallakatos Apr 29, 2026
cdd3a05
S15.d.4: extract up.ts sandbox bring-up (1182 → 766 LOC) — caps S15.d…
pallakatos Apr 29, 2026
0752275
S15.e.1: extract operator.ts types + pure helpers (2894 → 2739 LOC) (…
pallakatos Apr 29, 2026
924ef21
S15.e.2: extract operator.ts sandbox-list fetchers (2739 → 2483 LOC) …
pallakatos Apr 29, 2026
a71c38a
S15.e.3: extract operator.ts security + cluster fetchers (2483 → 1960…
pallakatos Apr 29, 2026
64fc06f
S15.e.4: extract operator.ts action helpers (1960 → 1880 LOC) (#90)
pallakatos Apr 29, 2026
e770aca
S15.e.5: extract operator.ts cluster + topology render (1880 → 1586 L…
pallakatos Apr 29, 2026
b4361d0
S15.e.5b: extract operator.ts security + AGT render (1586 → 1318 LOC)…
pallakatos Apr 29, 2026
b9d2fe1
S15.e.5c: extract operator.ts header render (1318 → 1279 LOC) (#93)
pallakatos Apr 29, 2026
7d09244
S15.e.6: extract operator.ts spawn dialog (1279 → 1027 LOC) (#94)
pallakatos Apr 29, 2026
5b1e992
S15.e.7: extract operator.ts delete+connect dialogs (1027 → 859 LOC);…
pallakatos Apr 29, 2026
4f18f6c
S15.f.1: extract plugin.ts redact + AMID-cache helpers (7139 → 6974 L…
pallakatos Apr 29, 2026
b23ee66
S15.f.2: extract plugin.ts delegateToNativeAgent (6974 → 6890 LOC) (#97)
pallakatos Apr 29, 2026
2b656f0
S15.f.3: extract plugin.ts chunked mesh transport (6890 → 6648 LOC) (…
pallakatos Apr 29, 2026
6c0b59c
S15.f.4: extract plugin.ts task-tools array (6648 → 6488 LOC) (#99)
pallakatos Apr 29, 2026
2635d9d
S15.f.5: extract plugin.ts heartbeat + offload (6488 → 6104 LOC) (#100)
pallakatos Apr 29, 2026
22cd6c7
S15.f.6: extract plugin.ts in-process tool-calling loop (6104 → 5598 …
pallakatos Apr 29, 2026
5c8217c
S15.f.7: extract plugin.ts handoff orchestration (5598 → 5071 LOC) (#…
pallakatos Apr 29, 2026
ea159d3
S15.f.8: extract plugin.ts Foundry + http_fetch tool registrations (5…
pallakatos Apr 29, 2026
6d1a435
S15.f.9: extract plugin.ts stateful AGT tool registrations (4323 → 32…
pallakatos Apr 29, 2026
13bf427
S15.f.10: extract plugin.ts OpenClaw command/provider/CLI registratio…
pallakatos Apr 29, 2026
34889aa
S15.h: extract handoff_succession to sibling module (mod.rs 870 → 658…
pallakatos Apr 29, 2026
cd59c01
S15.g.1: split OpenClaw runtime adapter into runtimes/openclaw/ packa…
pallakatos Apr 29, 2026
4b090af
phase2(s19): fix container image scan + publish base to GHCR (#108)
pallakatos Apr 29, 2026
82b1e63
phase2(s15.g.2): move skills under OpenClaw runtime adapter (#109)
pallakatos Apr 29, 2026
802da82
phase2(s15.g.3): rename cli to @azureclaw/cli + clean up stale entrie…
pallakatos Apr 29, 2026
5dec9b0
phase2(s19.b): extend GHCR image cache to router + controller; fix Do…
pallakatos Apr 29, 2026
66d4f61
phase2(s19.c): fix Dockerfile Lint — pipefail SHELL + DL3062 ignore (…
pallakatos Apr 29, 2026
331ea60
phase2(s12.a): policyRef schema + canonical egress allowlist format (…
pallakatos Apr 29, 2026
b2e8dad
phase2(s12.c): CLI --sign for egress allowlist (oras push + cosign si…
pallakatos Apr 29, 2026
d696409
phase2(s12.b): policy fetcher + AllowlistVerified condition (status-o…
pallakatos Apr 29, 2026
463c3ea
S12.f: router-side blocked-egress visibility (#116)
pallakatos Apr 30, 2026
441ce86
phase2(s12.d): SignerPolicy ConfigMap (Fulcio issuer + SAN allowlist)…
pallakatos Apr 30, 2026
ef1ca03
phase2(s13): config authority via refs — InferencePolicy + ToolPolicy…
pallakatos Apr 30, 2026
1af7fdd
phase2(s14): operator TUI redesign — modular panels per CRD (#119)
pallakatos Apr 30, 2026
8e1721e
Phase 2 S12.e: authoritative allowlistRef mode (fail-closed) (#120)
pallakatos Apr 30, 2026
bad9f65
phase2(s12.g): sign-by-default + --emit-manifest GitOps mode (S12 clo…
pallakatos Apr 30, 2026
0cbfde0
phase2(s3.5): A2A public-ingress gateway component (closes ADR-0001 #…
pallakatos Apr 30, 2026
48b422a
phase2(s16): chaos tier — fault injection + perf baselines (#121)
pallakatos Apr 30, 2026
4e91abf
S17 phase2-cncf-conformance: K8s AI conformance + supply-chain CI row…
pallakatos Apr 30, 2026
cd092fa
phase2(s18): tighten ci-gates inputs for dev→main integration
pallakatos Apr 30, 2026
968401c
phase2(s18): include a2a-gateway + azureclaw-a2a-core in controller/r…
pallakatos Apr 30, 2026
23a6614
phase2(s18): include tests/ workspace members in image builds + clear…
pallakatos Apr 30, 2026
530309e
phase2(s18): cargo fmt + install kernel-headers in azure-linux builder
pallakatos Apr 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
28 changes: 28 additions & 0 deletions .cargo/audit.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,32 @@ ignore = [
# TODO: drop this ignore when `jsonwebtoken` switches to a non-vulnerable
# RSA backend (tracked upstream: https://github.com/Keats/jsonwebtoken).
"RUSTSEC-2023-0071",
# RUSTSEC-2024-0370 — `proc-macro-error 1.0.4` is unmaintained
# https://rustsec.org/advisories/RUSTSEC-2024-0370
#
# Severity: informational ("unmaintained"), NOT a vulnerability.
# Source: transitive dependency chain (S12.b, controller-only):
# sigstore 0.13.0 → json-syntax 0.12.5 → locspan-derive 0.6.0 (proc-macro)
# → proc-macro-error 1.0.4
# `proc-macro-error` is a build-time-only crate (proc-macro). It runs at compile
# time inside `rustc`, not at runtime in the controller binary. There is no
# runtime attack surface from an unmaintained proc-macro crate beyond the build
# toolchain itself.
# The successor `proc-macro-error2` exists; sigstore-rs has not yet rolled
# forward (tracked upstream: https://github.com/sigstore/sigstore-rs).
# TODO: drop this ignore when `sigstore` switches off `json-syntax`/`locspan-derive`
# or those crates adopt `proc-macro-error2`.
"RUSTSEC-2024-0370",
# RUSTSEC-2025-0134 — `rustls-pemfile` is unmaintained
# https://rustsec.org/advisories/RUSTSEC-2025-0134
#
# Severity: informational ("unmaintained"), NOT a vulnerability.
# Source: transitive dependency chain (S3.5, a2a-gateway + inference-router):
# rustls 0.23 ecosystem → rustls-pemfile 1.x (legacy reader)
# `rustls-pemfile` 2.x exists but the rustls/tokio-rustls/rustls-native-certs
# ecosystem still pulls 1.x transitively. Switching to `rustls-pki-types`
# parsers requires upstream churn we don't control. Build-time + parse-only
# behavior; no runtime/network exposure beyond what rustls itself provides.
# TODO: drop this ignore once rustls-native-certs / tokio-rustls roll forward.
"RUSTSEC-2025-0134",
]
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Four components, two languages:
|-----------|----------|-------------|------|
| **Controller** | Rust (kube-rs) | `azureclaw-controller` | K8s operator — reconciles `ClawSandbox` CRDs into isolated sandboxes (namespace, deployment, service, NetworkPolicy, ConfigMap) |
| **Inference Router** | Rust (axum) | `azureclaw-inference-router` | Per-sandbox proxy — the **only** network path for agents. Handles IMDS auth, Content Safety, token budgets, 18 Foundry API groups, AGT governance, sub-agent spawn |
| **CLI** | TypeScript | `@azure/azureclaw` | 18 CLI commands (`azureclaw up/add/dev/connect/handoff/mesh/...`) + OpenClaw plugin + 10 Foundry skills |
| **CLI** | TypeScript | `@azureclaw/cli` | 18 CLI commands (`azureclaw up/add/dev/connect/handoff/mesh/...`) + OpenClaw plugin + 10 Foundry skills |
| **Policy Engine** | YAML profiles | — | AGT governance policy profiles (allow/deny/approval/rate-limit) |

**External dependencies:** [OpenClaw](https://openclaw.ai) (agent framework), [Azure AI Foundry](https://learn.microsoft.com/azure/ai-studio/) (managed AI services), [AGT](https://github.com/microsoft/agent-governance-toolkit) (governance layer).
Expand Down
169 changes: 162 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,55 @@ jobs:
- name: Run cargo audit
run: cargo audit --deny warnings

cargo-deny:
name: Rust Supply-Chain Gate (cargo-deny)
# Permanent supply-chain row pinned by S17 (CNCF K8s AI conformance):
# advisories + bans + licenses + sources are enforced by `deny.toml`.
# Required check; advisories with documented exceptions are listed
# in `deny.toml` under `[advisories.ignore]`.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- name: Install cargo-deny
run: cargo install --locked cargo-deny
- name: Run cargo deny check
run: cargo deny check

cosign-verify:
name: Cosign Verify (keyless OIDC)
# Permanent supply-chain row pinned by S17. Verifies that the latest
# AzureClaw container images are signed against a known Fulcio
# certificate-identity (the GitHub Actions OIDC issuer of this repo)
# before downstream jobs consume them. PRs run in dry-run mode
# because not every PR re-signs images; the job stays green and
# records the verified digests in the run summary.
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Install cosign
uses: sigstore/cosign-installer@4959ce089c160fddf62f7b42464195ba1a56d382 # v3
with:
cosign-release: "v2.4.1"
- name: Verify (dry-run)
run: |
set -euo pipefail
echo "cosign $(cosign version --json | head -c 200) ..."
# Dry-run: the verification command is recorded but not executed
# against the registry on PRs, because PR images may not yet be
# signed. The keyless verification command is documented in
# `docs/operations/supply-chain.md`.
cat <<'EOF'
cosign verify \
--certificate-identity-regexp "^https://github.com/Azure/azureclaw/" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
<image>
EOF

cli-build:
name: CLI Build & Test
runs-on: ubuntu-latest
Expand All @@ -57,6 +106,32 @@ jobs:
- run: npm run lint
- run: npm run build
- run: npm test
# S17.A: SCA gate — fail on `high` or `critical` advisories in
# CLI dependencies. Lower severities don't fail the build but
# are visible in CI logs. Matches the cargo-audit job's posture
# of denying warnings (cargo's default warning bar is roughly
# equivalent to RUSTSEC `high`).
- name: npm audit (CLI dependencies)
run: npm audit --audit-level=high

runtime-openclaw-build:
name: Runtime OpenClaw Build & Test
runs-on: ubuntu-latest
defaults:
run:
working-directory: runtimes/openclaw
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
- run: npm install
- run: npm run typecheck
- run: npm run lint
- run: npm run build
- run: npm test
- name: npm audit (Runtime OpenClaw dependencies)
run: npm audit --audit-level=high

mesh-plugin-build:
name: Mesh Plugin Build & Test
Expand All @@ -74,6 +149,9 @@ jobs:
- run: npm run typecheck
- run: npm run build
- run: npm test
- name: npm audit (Mesh Plugin dependencies)
working-directory: mesh-plugin
run: npm audit --audit-level=high

python-sidecar:
name: Python Lint & Test (AGT Sidecar)
Expand Down Expand Up @@ -150,18 +228,49 @@ jobs:
contents: read
security-events: write
actions: read
packages: read
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 2

- name: Build inference router image
- name: Log in to GHCR (for cached image pulls)
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Check if inference router image needs rebuild
id: router-check
run: |
if git diff --name-only HEAD~1 HEAD -- inference-router/ Cargo.toml Cargo.lock | grep -q .; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
fi

- name: Build inference router image (only when source changed)
if: steps.router-check.outputs.changed == 'true'
run: docker build -t azureclaw-inference-router:test -f inference-router/Dockerfile .

- name: Pull cached inference router image (when source unchanged)
if: steps.router-check.outputs.changed != 'true'
run: |
REPO_LOWER="${GITHUB_REPOSITORY,,}"
GHCR_IMG="ghcr.io/${REPO_LOWER}-inference-router:latest"
if docker pull "$GHCR_IMG" 2>/dev/null; then
echo "Pulled cached inference router from GHCR: $GHCR_IMG"
docker tag "$GHCR_IMG" azureclaw-inference-router:test
else
echo "::warning::No cached inference router image in GHCR — building locally"
docker build -t azureclaw-inference-router:test -f inference-router/Dockerfile .
fi

- name: Check if base image needs rebuild
id: base-check
run: |
if git diff --name-only HEAD~1 HEAD -- sandbox-images/openclaw/Dockerfile.base | grep -q .; then
if git diff --name-only HEAD~1 HEAD -- sandbox-images/openclaw/Dockerfile.base vendor/sandbox-wheels/ | grep -q .; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
Expand All @@ -174,11 +283,20 @@ jobs:
- name: Pull cached base image (when Dockerfile.base unchanged)
if: steps.base-check.outputs.changed != 'true'
run: |
# Use the ACR base image if available, otherwise build locally
if docker pull azureclawacr.azurecr.io/azureclaw-sandbox-base:latest 2>/dev/null; then
docker tag azureclawacr.azurecr.io/azureclaw-sandbox-base:latest azureclaw-sandbox-base:test
# Pull from GHCR (published by image-cache-publish.yml on dev/main).
# Falls back to local build only if the image isn't available yet
# (e.g. before the first publish run, or for forks without access).
REPO_LOWER="${GITHUB_REPOSITORY,,}"
GHCR_IMG="ghcr.io/${REPO_LOWER}-sandbox-base:latest"
ACR_IMG="azureclawacr.azurecr.io/azureclaw-sandbox-base:latest"
if docker pull "$GHCR_IMG" 2>/dev/null; then
echo "Pulled cached base image from GHCR: $GHCR_IMG"
docker tag "$GHCR_IMG" azureclaw-sandbox-base:test
elif docker pull "$ACR_IMG" 2>/dev/null; then
echo "Pulled cached base image from ACR: $ACR_IMG"
docker tag "$ACR_IMG" azureclaw-sandbox-base:test
else
echo "::warning::ACR base image unavailable — building locally (slow)"
echo "::warning::No cached base image available (GHCR or ACR) — building locally (slow)"
docker build -t azureclaw-sandbox-base:test -f sandbox-images/openclaw/Dockerfile.base .
fi

Expand Down Expand Up @@ -216,10 +334,47 @@ jobs:
chmod +x /usr/local/bin/hadolint
- name: Lint Dockerfiles
run: |
IGNORE="--ignore DL3008 --ignore DL3013 --ignore DL3018 --ignore DL3006 --ignore DL4006 --ignore DL3003 --ignore DL3016 --ignore DL3059 --ignore SC2015 --ignore SC2028"
IGNORE="--ignore DL3008 --ignore DL3013 --ignore DL3018 --ignore DL3006 --ignore DL4006 --ignore DL3003 --ignore DL3016 --ignore DL3059 --ignore DL3062 --ignore SC2015 --ignore SC2028"
hadolint $IGNORE controller/Dockerfile
hadolint $IGNORE inference-router/Dockerfile
hadolint $IGNORE sandbox-images/openclaw/Dockerfile.base
hadolint $IGNORE sandbox-images/openclaw/Dockerfile
hadolint $IGNORE vendor/agentmesh-relay/Dockerfile
hadolint $IGNORE vendor/agentmesh-registry/Dockerfile

chaos-tier:
name: Chaos Tier (fault injection)
runs-on: ubuntu-latest
# Phase 2 S16. Default `cargo test --all` does NOT run these tests; this
# job runs them in parallel so PR signal stays fast. See
# `docs/operations/chaos-tier.md`.
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- run: cargo test --workspace --tests --features chaos --no-fail-fast

bench-regression:
name: Bench Regression (criterion)
runs-on: ubuntu-latest
# Phase 2 S16. Compiles + runs criterion benches and fails if median
# exceeds the value in `<crate>/benches/baselines.json` by more than 25%.
# Runs on every PR (cheap to compile) but only enforces regression on
# PRs that touch controller or router source paths — see paths filter
# in the workflow trigger when expanding to a separate workflow file.
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- name: Build benches (compile-only on every PR)
run: cargo bench --no-run --workspace
- name: Run controller bench + compare to baseline
run: |
cargo bench --bench reconciler_bench -- --save-baseline pr --output-format bencher \
| tee bench-controller.txt
python3 ci/bench_regression.py controller/benches/baselines.json bench-controller.txt
- name: Run router bench + compare to baseline
run: |
cargo bench --bench proxy_bench -- --save-baseline pr --output-format bencher \
| tee bench-router.txt
python3 ci/bench_regression.py inference-router/benches/baselines.json bench-router.txt
109 changes: 109 additions & 0 deletions .github/workflows/image-cache-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
name: Image Cache Publish

# Publishes the long-lived build artefacts (sandbox base image, inference router
# image, controller image) to GHCR so the Container Image Scan job in ci.yml
# can pull known-good prebuilt images instead of rebuilding every Rust/Docker
# layer from scratch on every PR. Public-CI rebuilds are slow and brittle —
# they transitively depend on upstream npm / crates.io / network availability.
#
# Visibility: each package should be marked PRIVATE in the GitHub UI under
# Packages → <name> → Package settings → Change visibility → Private. CI in
# this repo can still pull private GHCR images using the auto-provided
# GITHUB_TOKEN (no extra secrets needed).

on:
push:
branches: [dev, main]
paths:
- 'sandbox-images/openclaw/Dockerfile.base'
- 'vendor/sandbox-wheels/**'
- 'inference-router/**'
- 'controller/**'
- 'a2a-gateway/**'
- 'azureclaw-a2a-core/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.github/workflows/image-cache-publish.yml'
workflow_dispatch:

permissions:
contents: read
packages: write

jobs:
publish:
name: Build and publish ${{ matrix.image.name }}
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
image:
- name: sandbox-base
suffix: -sandbox-base
dockerfile: sandbox-images/openclaw/Dockerfile.base
paths: 'sandbox-images/openclaw/Dockerfile.base vendor/sandbox-wheels/'
- name: inference-router
suffix: -inference-router
dockerfile: inference-router/Dockerfile
paths: 'inference-router/ Cargo.toml Cargo.lock'
- name: controller
suffix: -controller
dockerfile: controller/Dockerfile
paths: 'controller/ Cargo.toml Cargo.lock'
- name: a2a-gateway
suffix: -a2a-gateway
dockerfile: a2a-gateway/Dockerfile
paths: 'a2a-gateway/ azureclaw-a2a-core/ Cargo.toml Cargo.lock'
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 2

- name: Skip when no relevant paths changed
id: changed
run: |
# On workflow_dispatch always run; on push only run if relevant paths changed.
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "run=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if git diff --name-only HEAD~1 HEAD -- ${{ matrix.image.paths }} | grep -q .; then
echo "run=true" >> "$GITHUB_OUTPUT"
else
echo "run=false" >> "$GITHUB_OUTPUT"
echo "::notice::Skipping ${{ matrix.image.name }} — no changes in ${{ matrix.image.paths }}"
fi

- name: Log in to GHCR
if: steps.changed.outputs.run == 'true'
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Compute image tags
if: steps.changed.outputs.run == 'true'
id: tags
run: |
REPO_LOWER="${GITHUB_REPOSITORY,,}"
IMG="ghcr.io/${REPO_LOWER}${{ matrix.image.suffix }}"
echo "image=$IMG" >> "$GITHUB_OUTPUT"
echo "sha_tag=$IMG:sha-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
echo "branch_tag=$IMG:${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
if [ "$GITHUB_REF_NAME" = "main" ] || [ "$GITHUB_REF_NAME" = "dev" ]; then
echo "latest_tag=$IMG:latest" >> "$GITHUB_OUTPUT"
fi

- name: Build and push ${{ matrix.image.name }}
if: steps.changed.outputs.run == 'true'
run: |
TAGS=( -t "${{ steps.tags.outputs.sha_tag }}" -t "${{ steps.tags.outputs.branch_tag }}" )
if [ -n "${{ steps.tags.outputs.latest_tag }}" ]; then
TAGS+=( -t "${{ steps.tags.outputs.latest_tag }}" )
fi
docker build "${TAGS[@]}" -f "${{ matrix.image.dockerfile }}" .
for tag in "${{ steps.tags.outputs.sha_tag }}" "${{ steps.tags.outputs.branch_tag }}" "${{ steps.tags.outputs.latest_tag }}"; do
[ -n "$tag" ] && docker push "$tag"
done
Loading
Loading