From 9a00e5edc54aeae643f489a5c8231f25e75890cf Mon Sep 17 00:00:00 2001 From: Pal Lakatos-Toth Date: Mon, 22 Jun 2026 14:59:00 +0200 Subject: [PATCH 01/21] ci(release): add interim public release workflow (GHCR + GitHub Release) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Publishes pre-built, cosign-signed, multi-arch container images to PUBLIC ghcr.io/azure/kars-* and attaches the @kars/cli tarball to a public GitHub Release, so customers can run kars without cloning AGT or compiling. This is the interim GHCR path from docs/PUBLISHING.md; npmjs `@kars`, crates.io and MCR remain reserved for the ESRP-signed release. Includes a release-build correctness guard that fails if the sandbox image would fall back to the unpatched public AGT SDK instead of the vendored patched tarball. Working branch only — not wired until the published-binaries quick-setup is verified end-to-end. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/release-public-interim.yml | 387 +++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 .github/workflows/release-public-interim.yml diff --git a/.github/workflows/release-public-interim.yml b/.github/workflows/release-public-interim.yml new file mode 100644 index 00000000..9e394f86 --- /dev/null +++ b/.github/workflows/release-public-interim.yml @@ -0,0 +1,387 @@ +name: Public Interim Release (GHCR + GitHub Release) + +# ───────────────────────────────────────────────────────────────────────── +# INTERIM public release — lets customers run kars from pre-built, signed +# artefacts WITHOUT cloning AGT or compiling anything. This is the stopgap +# described in docs/PUBLISHING.md until the ESRP-signed public path +# (.github/pipelines/esrp-publish.yml → MCR + npmjs `@kars` + crates.io) +# is wired. Per PUBLISHING.md, GHCR is explicitly allowed for interim/nightly. +# +# What this produces (all PUBLIC): +# 1. Multi-arch container images → ghcr.io/azure/kars-*: + :latest +# (cosign keyless signed, SBOM + SLSA provenance attached) +# 2. @kars/cli packed as a .tgz attached to a PUBLIC GitHub Release +# (NOT published to npmjs — the `@kars` scope is reserved for the +# ESRP-signed publish; customers install via `npm i -g `) +# +# What it deliberately does NOT do (reserved for ESRP, see PUBLISHING.md): +# - npm publish to npmjs.com under `@kars` +# - cargo publish to crates.io +# - push to mcr.microsoft.com +# +# Triggers: +# - Tags matching `v*-interim*` (e.g. v0.1.0-interim.1) +# - Manual via workflow_dispatch (version input) +# +# ONE-TIME SETUP (first run only): GHCR org packages default to private. +# After the first push, set each `kars-*` package to Public in +# https://github.com/orgs/azure/packages → package → Settings → visibility. +# The `set-package-public` step attempts this automatically but org policy +# may require the one-time manual toggle. +# ───────────────────────────────────────────────────────────────────────── + +on: + push: + tags: + - 'v*-interim*' + workflow_dispatch: + inputs: + version: + description: 'Version to tag (e.g. v0.1.0-interim.1). Defaults to interim-.' + required: false + default: '' + +permissions: + contents: write # create GitHub Release + packages: write # push to GHCR + set visibility + id-token: write # cosign keyless OIDC + attestations: write + actions: read + +env: + VERSION: ${{ github.event.inputs.version != '' && github.event.inputs.version || (startsWith(github.ref, 'refs/tags/') && github.ref_name || format('interim-{0}', github.sha)) }} + REGISTRY: ghcr.io/azure + +jobs: + # ─── Stage 1: multi-arch Rust binaries ───────────────────────── + build-binaries: + name: Build Rust binaries (amd64 + arm64) + runs-on: ubuntu-22.04 + timeout-minutes: 40 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + with: + targets: x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + shared-key: rust-glibc-release-multiarch + save-if: false + - name: Install aarch64 cross-linker + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends gcc-aarch64-linux-gnu + - name: cargo build --release (amd64 + arm64) + env: + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + run: | + cargo build --release --workspace --target x86_64-unknown-linux-gnu + cargo build --release --workspace --target aarch64-unknown-linux-gnu + - name: Stage binaries per-arch + run: | + mkdir -p ./bin/amd64 ./bin/arm64 + for bin in kars-controller kars-inference-router kars-a2a-gateway kars-conformance-runner; do + cp "target/x86_64-unknown-linux-gnu/release/$bin" "./bin/amd64/$bin" + cp "target/aarch64-unknown-linux-gnu/release/$bin" "./bin/arm64/$bin" + done + - name: Verify + checksum + run: | + chmod +x ./bin/*/kars-* + ( cd ./bin && find . -type f -name 'kars-*' -exec sha256sum {} \; | tee SHA256SUMS ) + file ./bin/*/kars-* + - name: Upload binaries + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4 + with: + name: kars-binaries-${{ env.VERSION }} + path: ./bin/ + retention-days: 90 + + # ─── Stage 2: build + push container images to PUBLIC GHCR ───── + build-images: + name: Build + push ${{ matrix.image.name }} (PUBLIC GHCR) + needs: build-binaries + runs-on: ubuntu-22.04 + timeout-minutes: 40 + outputs: + controller_digest: ${{ steps.digests.outputs.controller }} + router_digest: ${{ steps.digests.outputs.router }} + gateway_digest: ${{ steps.digests.outputs.gateway }} + runner_digest: ${{ steps.digests.outputs.runner }} + sandbox_base_digest: ${{ steps.digests.outputs.sandbox_base }} + strategy: + fail-fast: false + matrix: + image: + - name: controller + dockerfile: controller/Dockerfile + ghcr: kars-controller + platforms: linux/amd64,linux/arm64 + - name: inference-router + dockerfile: inference-router/Dockerfile + ghcr: kars-inference-router + platforms: linux/amd64,linux/arm64 + - name: a2a-gateway + dockerfile: a2a-gateway/Dockerfile + ghcr: kars-a2a-gateway + platforms: linux/amd64,linux/arm64 + - name: conformance-runner + dockerfile: sandbox-images/conformance-runner/Dockerfile + ghcr: kars-conformance-runner + platforms: linux/amd64,linux/arm64 + - name: sandbox-base + dockerfile: sandbox-images/openclaw/Dockerfile.base + ghcr: kars-sandbox-base + no_binaries: true + # sandbox-base bundles Python wheels (manylinux); many transitive + # wheels lack aarch64 builds, so arm64 would silently fall back to + # a slow QEMU source compile. Ship amd64-only here — Apple Silicon + # users run it under emulation (documented in the quickstart). + platforms: linux/amd64 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Download pre-built binaries (per-arch) + if: matrix.image.no_binaries != true + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: kars-binaries-${{ env.VERSION }} + path: ./bin/ + - name: chmod binaries + if: matrix.image.no_binaries != true + run: find ./bin -type f -name 'kars-*' -exec chmod +x {} \; + + - name: Stage patched AGT SDK (release-build correctness guard) + if: matrix.image.name == 'sandbox-base' + run: | + # A release sandbox image MUST bundle the kars-patched AGT SDK + # (POP signing + X3DH KDF fix). The Dockerfile falls back to the + # UNPATCHED public npm SDK if .agt-sdk/ is absent, which + # would ship a broken-interop image. Fail the release rather than + # publish an unpatched mesh. + TARBALL=$(find vendor/agt -maxdepth 1 -name 'microsoft-agent-governance-sdk-*.tgz' | head -1 || true) + if [ -z "$TARBALL" ]; then + echo "::error::No vendored patched AGT SDK tarball under vendor/agt/." \ + "Refusing to build a release sandbox image that would fall back" \ + "to the unpatched public SDK. See vendor/agt/pin.json." + exit 1 + fi + mkdir -p sandbox-images/openclaw/.agt-sdk + cp "$TARBALL" sandbox-images/openclaw/.agt-sdk/ + echo "AGT_SDK_TARBALL=$(basename "$TARBALL")" >> "$GITHUB_ENV" + echo "::notice::Bundling patched AGT SDK: $(basename "$TARBALL")" + + - name: Set up QEMU (for cross-arch buildx) + if: matrix.image.platforms != 'linux/amd64' + uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3 + + - name: Log in to GHCR + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build + push (PUBLIC) + id: push + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v6 + with: + context: . + file: ${{ matrix.image.dockerfile }} + push: true + platforms: ${{ matrix.image.platforms }} + tags: | + ${{ env.REGISTRY }}/${{ matrix.image.ghcr }}:${{ env.VERSION }} + ${{ env.REGISTRY }}/${{ matrix.image.ghcr }}:latest + build-args: | + VERSION=${{ env.VERSION }} + AGT_SDK_TARBALL=${{ env.AGT_SDK_TARBALL }} + sbom: true + provenance: true + + - name: Make package public (best-effort; may need one-time manual toggle) + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh api -X PATCH \ + -H "Accept: application/vnd.github+json" \ + "/orgs/azure/packages/container/${{ matrix.image.ghcr }}/visibility" \ + -f visibility=public \ + || echo "::warning::Could not set ${{ matrix.image.ghcr }} public via API; set it once manually in package settings." + + - name: Record digest + id: digests + env: + DIGEST: ${{ steps.push.outputs.digest }} + run: | + case "${{ matrix.image.name }}" in + controller) echo "controller=${DIGEST}" >> "$GITHUB_OUTPUT" ;; + inference-router) echo "router=${DIGEST}" >> "$GITHUB_OUTPUT" ;; + a2a-gateway) echo "gateway=${DIGEST}" >> "$GITHUB_OUTPUT" ;; + conformance-runner) echo "runner=${DIGEST}" >> "$GITHUB_OUTPUT" ;; + sandbox-base) echo "sandbox_base=${DIGEST}" >> "$GITHUB_OUTPUT" ;; + esac + + - name: Generate SBOM (SPDX JSON) + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0 + with: + image: ${{ env.REGISTRY }}/${{ matrix.image.ghcr }}:${{ env.VERSION }} + format: spdx-json + output-file: sbom-${{ matrix.image.name }}.spdx.json + + - name: Upload SBOM + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4 + with: + name: sbom-${{ matrix.image.name }}-${{ env.VERSION }} + path: sbom-${{ matrix.image.name }}.spdx.json + retention-days: 90 + + - name: Install cosign + uses: sigstore/cosign-installer@1aa8e0f2454b781fbf0fbf306a4c9533a0c57409 # v3.7.0 + with: + cosign-release: 'v2.4.1' + + - name: Cosign keyless sign image + env: + COSIGN_EXPERIMENTAL: '1' + run: | + cosign sign --yes ${{ env.REGISTRY }}/${{ matrix.image.ghcr }}@${{ steps.push.outputs.digest }} + + # ─── Stage 3: pack @kars/cli for GitHub Release distribution ─── + cli-pack: + name: Pack @kars/cli tarball + runs-on: ubuntu-22.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4 + with: + node-version: '22' + - name: npm install + build + pack + working-directory: cli + run: | + npm install + npm run build + npm pack + ls -la ./*.tgz + - name: Upload CLI tarball + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4 + with: + name: npm-kars-cli-${{ env.VERSION }} + path: cli/*.tgz + retention-days: 90 + + # ─── Stage 4: assemble + publish the PUBLIC GitHub Release ───── + release: + name: Create PUBLIC GitHub Release + needs: [build-binaries, build-images, cli-pack] + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Download Rust binaries + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: kars-binaries-${{ env.VERSION }} + path: ./release-artefacts + + - name: Download CLI tarball + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + pattern: npm-kars-cli-${{ env.VERSION }} + path: ./release-artefacts + merge-multiple: true + + - name: Download SBOMs + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + pattern: sbom-*-${{ env.VERSION }} + path: ./release-artefacts + merge-multiple: true + + - name: List artefacts + run: find ./release-artefacts -type f | sort + + - name: Resolve CLI tarball name + id: cli + run: | + NAME=$(find ./release-artefacts -maxdepth 2 -name '*.tgz' -printf '%f\n' | head -1) + if [ -z "$NAME" ]; then + echo "::error::No CLI tarball found under release-artefacts." + exit 1 + fi + echo "tarball=$NAME" >> "$GITHUB_OUTPUT" + echo "::notice::CLI tarball: $NAME" + + - name: Build release manifest + run: | + cat > ./release-artefacts/release-manifest.json < These images bundle a kars build of the AGT mesh SDK pending + > upstream [microsoft/agent-governance-toolkit#3128](https://github.com/microsoft/agent-governance-toolkit/pull/3128). + + ### Quick start (no compile) + ```bash + # 1. Install the CLI from this Release (interim; npmjs `@kars` later) + npm i -g https://github.com/${{ github.repository }}/releases/download/${{ env.VERSION }}/${{ steps.cli.outputs.tarball }} + + # 2. Run locally against the published images (Docker Desktop) + kars dev --release ${{ env.VERSION }} + ``` + + ### Container images (PUBLIC GHCR) + Multi-arch `linux/amd64,linux/arm64` (sandbox-base is amd64-only; + Apple Silicon runs it under emulation): + ``` + ${{ env.REGISTRY }}/kars-controller:${{ env.VERSION }} + ${{ env.REGISTRY }}/kars-inference-router:${{ env.VERSION }} + ${{ env.REGISTRY }}/kars-a2a-gateway:${{ env.VERSION }} + ${{ env.REGISTRY }}/kars-conformance-runner:${{ env.VERSION }} + ${{ env.REGISTRY }}/kars-sandbox-base:${{ env.VERSION }} + ``` + All signed with cosign keyless (verify via Sigstore Rekor). SBOMs + attached below. From 835d08d71b2a36f457bf438edd41be3b0a37c7a7 Mon Sep 17 00:00:00 2001 From: Pal Lakatos-Toth Date: Mon, 22 Jun 2026 15:03:16 +0200 Subject: [PATCH 02/21] ci(release): publish openclaw-sandbox agent image; fix AGT guard layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The controller launches openclaw-sandbox (built FROM kars-sandbox-base + router + the OpenClaw plugin), which is also where the patched AGT SDK is baked in — not sandbox-base. Add a dedicated build-sandbox job that builds openclaw-sandbox after the base/router are pushed, with the patched-AGT correctness guard moved to the correct layer. Now the published set is a complete runnable OpenClaw graph: controller + inference-router + sandbox-base + openclaw-sandbox (+ a2a-gateway, conformance-runner). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/release-public-interim.yml | 126 +++++++++++++++---- 1 file changed, 103 insertions(+), 23 deletions(-) diff --git a/.github/workflows/release-public-interim.yml b/.github/workflows/release-public-interim.yml index 9e394f86..dc8b87ff 100644 --- a/.github/workflows/release-public-interim.yml +++ b/.github/workflows/release-public-interim.yml @@ -150,26 +150,6 @@ jobs: if: matrix.image.no_binaries != true run: find ./bin -type f -name 'kars-*' -exec chmod +x {} \; - - name: Stage patched AGT SDK (release-build correctness guard) - if: matrix.image.name == 'sandbox-base' - run: | - # A release sandbox image MUST bundle the kars-patched AGT SDK - # (POP signing + X3DH KDF fix). The Dockerfile falls back to the - # UNPATCHED public npm SDK if .agt-sdk/ is absent, which - # would ship a broken-interop image. Fail the release rather than - # publish an unpatched mesh. - TARBALL=$(find vendor/agt -maxdepth 1 -name 'microsoft-agent-governance-sdk-*.tgz' | head -1 || true) - if [ -z "$TARBALL" ]; then - echo "::error::No vendored patched AGT SDK tarball under vendor/agt/." \ - "Refusing to build a release sandbox image that would fall back" \ - "to the unpatched public SDK. See vendor/agt/pin.json." - exit 1 - fi - mkdir -p sandbox-images/openclaw/.agt-sdk - cp "$TARBALL" sandbox-images/openclaw/.agt-sdk/ - echo "AGT_SDK_TARBALL=$(basename "$TARBALL")" >> "$GITHUB_ENV" - echo "::notice::Bundling patched AGT SDK: $(basename "$TARBALL")" - - name: Set up QEMU (for cross-arch buildx) if: matrix.image.platforms != 'linux/amd64' uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 @@ -197,7 +177,6 @@ jobs: ${{ env.REGISTRY }}/${{ matrix.image.ghcr }}:latest build-args: | VERSION=${{ env.VERSION }} - AGT_SDK_TARBALL=${{ env.AGT_SDK_TARBALL }} sbom: true provenance: true @@ -250,6 +229,105 @@ jobs: run: | cosign sign --yes ${{ env.REGISTRY }}/${{ matrix.image.ghcr }}@${{ steps.push.outputs.digest }} + # ─── Stage 2b: build the openclaw-sandbox agent image ────────── + # openclaw-sandbox is the image the controller actually launches per agent. + # It is built FROM kars-sandbox-base + the inference-router binary + the + # OpenClaw plugin, and is where the patched AGT SDK gets baked in. It must + # build AFTER build-images so the base + router images it references are + # already pushed to GHCR. amd64-only (inherits sandbox-base's wheel arch). + build-sandbox: + name: Build + push openclaw-sandbox (PUBLIC GHCR) + needs: build-images + runs-on: ubuntu-22.04 + timeout-minutes: 40 + outputs: + sandbox_digest: ${{ steps.push.outputs.digest }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Stage patched AGT SDK (release-build correctness guard) + run: | + # A release sandbox image MUST bundle the kars-patched AGT SDK + # (POP signing + X3DH KDF fix). The Dockerfile falls back to the + # UNPATCHED public npm SDK if .agt-sdk/ is absent, which + # would ship a broken-interop mesh. Fail the release instead. + TARBALL=$(find vendor/agt -maxdepth 1 -name 'microsoft-agent-governance-sdk-*.tgz' | head -1 || true) + if [ -z "$TARBALL" ]; then + echo "::error::No vendored patched AGT SDK tarball under vendor/agt/." \ + "Refusing to publish an openclaw-sandbox that would fall back to" \ + "the unpatched public SDK. See vendor/agt/pin.json." + exit 1 + fi + mkdir -p sandbox-images/openclaw/.agt-sdk + cp "$TARBALL" sandbox-images/openclaw/.agt-sdk/ + echo "AGT_SDK_TARBALL=$(basename "$TARBALL")" >> "$GITHUB_ENV" + echo "::notice::Bundling patched AGT SDK: $(basename "$TARBALL")" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3 + + - name: Log in to GHCR + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build + push openclaw-sandbox (PUBLIC) + id: push + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v6 + with: + context: . + file: sandbox-images/openclaw/Dockerfile + push: true + platforms: linux/amd64 + tags: | + ${{ env.REGISTRY }}/openclaw-sandbox:${{ env.VERSION }} + ${{ env.REGISTRY }}/openclaw-sandbox:latest + build-args: | + VERSION=${{ env.VERSION }} + SANDBOX_BASE_IMAGE=${{ env.REGISTRY }}/kars-sandbox-base:${{ env.VERSION }} + INFERENCE_ROUTER_IMAGE=${{ env.REGISTRY }}/kars-inference-router:${{ env.VERSION }} + AGT_SDK_TARBALL=${{ env.AGT_SDK_TARBALL }} + sbom: true + provenance: true + + - name: Make package public (best-effort; may need one-time manual toggle) + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh api -X PATCH \ + -H "Accept: application/vnd.github+json" \ + "/orgs/azure/packages/container/openclaw-sandbox/visibility" \ + -f visibility=public \ + || echo "::warning::Could not set openclaw-sandbox public via API; set it once manually in package settings." + + - name: Generate SBOM (SPDX JSON) + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0 + with: + image: ${{ env.REGISTRY }}/openclaw-sandbox:${{ env.VERSION }} + format: spdx-json + output-file: sbom-openclaw-sandbox.spdx.json + + - name: Upload SBOM + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4 + with: + name: sbom-openclaw-sandbox-${{ env.VERSION }} + path: sbom-openclaw-sandbox.spdx.json + retention-days: 90 + + - name: Install cosign + uses: sigstore/cosign-installer@1aa8e0f2454b781fbf0fbf306a4c9533a0c57409 # v3.7.0 + with: + cosign-release: 'v2.4.1' + + - name: Cosign keyless sign image + env: + COSIGN_EXPERIMENTAL: '1' + run: | + cosign sign --yes ${{ env.REGISTRY }}/openclaw-sandbox@${{ steps.push.outputs.digest }} + # ─── Stage 3: pack @kars/cli for GitHub Release distribution ─── cli-pack: name: Pack @kars/cli tarball @@ -277,7 +355,7 @@ jobs: # ─── Stage 4: assemble + publish the PUBLIC GitHub Release ───── release: name: Create PUBLIC GitHub Release - needs: [build-binaries, build-images, cli-pack] + needs: [build-binaries, build-images, build-sandbox, cli-pack] runs-on: ubuntu-22.04 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -332,7 +410,8 @@ jobs: {"name": "kars-inference-router", "ghcr": "${{ env.REGISTRY }}/kars-inference-router:${{ env.VERSION }}", "digest": "${{ needs.build-images.outputs.router_digest }}"}, {"name": "kars-a2a-gateway", "ghcr": "${{ env.REGISTRY }}/kars-a2a-gateway:${{ env.VERSION }}", "digest": "${{ needs.build-images.outputs.gateway_digest }}"}, {"name": "kars-conformance-runner","ghcr": "${{ env.REGISTRY }}/kars-conformance-runner:${{ env.VERSION }}","digest": "${{ needs.build-images.outputs.runner_digest }}"}, - {"name": "kars-sandbox-base", "ghcr": "${{ env.REGISTRY }}/kars-sandbox-base:${{ env.VERSION }}", "digest": "${{ needs.build-images.outputs.sandbox_base_digest }}"} + {"name": "kars-sandbox-base", "ghcr": "${{ env.REGISTRY }}/kars-sandbox-base:${{ env.VERSION }}", "digest": "${{ needs.build-images.outputs.sandbox_base_digest }}"}, + {"name": "openclaw-sandbox", "ghcr": "${{ env.REGISTRY }}/openclaw-sandbox:${{ env.VERSION }}", "digest": "${{ needs.build-sandbox.outputs.sandbox_digest }}"} ], "cli_install": "npm i -g https://github.com/${{ github.repository }}/releases/download/${{ env.VERSION }}/${{ steps.cli.outputs.tarball }}", "next_step_for_signed_release": "Migrate to the ESRP-signed public path (.github/pipelines/esrp-publish.yml → MCR + npmjs @kars + crates.io) once onboarding completes. See docs/PUBLISHING.md." @@ -382,6 +461,7 @@ jobs: ${{ env.REGISTRY }}/kars-a2a-gateway:${{ env.VERSION }} ${{ env.REGISTRY }}/kars-conformance-runner:${{ env.VERSION }} ${{ env.REGISTRY }}/kars-sandbox-base:${{ env.VERSION }} + ${{ env.REGISTRY }}/openclaw-sandbox:${{ env.VERSION }} ``` All signed with cosign keyless (verify via Sigstore Rekor). SBOMs attached below. From 01aed684fda7a2f9d6d0713f2831b316d4848cee Mon Sep 17 00:00:00 2001 From: Pal Lakatos-Toth Date: Mon, 22 Jun 2026 15:11:12 +0200 Subject: [PATCH 03/21] ci(release): publish all 6 Python runtime images + AGT wheels build Adds a build-agt-wheels job (clones the pinned AGT, builds the upstream governance wheels) feeding a build-runtimes matrix that publishes the six runtime adapter images: langgraph, hermes, maf-python, anthropic, openai-agents, pydantic-ai. Each bundles the upstream AGT wheels plus kars-agt-mesh (kars's spec-compliant Python MeshClient, COPYed hermetically from the in-repo runtimes/agt-mesh-python source). cosign + SBOM + public. Full published set: controller, inference-router, a2a-gateway, conformance-runner, sandbox-base, openclaw-sandbox + 6 runtimes + CLI tarball. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/release-public-interim.yml | 151 ++++++++++++++++++- 1 file changed, 150 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-public-interim.yml b/.github/workflows/release-public-interim.yml index dc8b87ff..6d63e154 100644 --- a/.github/workflows/release-public-interim.yml +++ b/.github/workflows/release-public-interim.yml @@ -328,6 +328,142 @@ jobs: run: | cosign sign --yes ${{ env.REGISTRY }}/openclaw-sandbox@${{ steps.push.outputs.digest }} + # ─── Stage 2c: build AGT Python wheels (upstream governance pkgs) ── + # The Python runtime images COPY runtimes/wheels/*.whl (upstream AGT + # governance packages) AND runtimes/agt-mesh-python/ (kars's own + # spec-compliant Python MeshClient — in-repo source, bundled hermetically + # from the checkout). Only the upstream wheels need building here; they + # come from the pinned AGT checkout (vendor/agt/pin.json). + build-agt-wheels: + name: Build AGT Python wheels (from pinned AGT) + runs-on: ubuntu-22.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: '3.12' + - name: Clone pinned AGT + build wheels + run: | + URL=$(jq -r .url vendor/agt/pin.json) + SHA=$(jq -r .sha vendor/agt/pin.json) + echo "::notice::Cloning $URL @ $SHA for AGT-Python wheels" + git clone --filter=blob:none "$URL" /tmp/agt + git -C /tmp/agt checkout "$SHA" + AGT_PYTHON_DIR=/tmp/agt/agent-governance-python ./runtimes/build-agt-wheels.sh + echo "Built wheels:" && ls -la runtimes/wheels/ + - name: Guard — wheels present + run: | + count=$(find runtimes/wheels -maxdepth 1 -name '*.whl' | wc -l) + if [ "$count" -lt 1 ]; then + echo "::error::No AGT-Python wheels were produced — Python runtime" \ + "images would build without governance primitives. Aborting." + exit 1 + fi + echo "::notice::Staged $count AGT-Python wheel(s)." + - name: Upload wheels + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4 + with: + name: agt-wheels-${{ env.VERSION }} + path: runtimes/wheels/*.whl + retention-days: 90 + + # ─── Stage 2d: build the Python runtime adapter images ───────── + # Each bundles the upstream AGT wheels + kars-agt-mesh (in-repo) + the + # runtime adapter. amd64-only (manylinux wheels). FROM Azure Linux python. + build-runtimes: + name: Build + push ${{ matrix.runtime.ghcr }} (PUBLIC GHCR) + needs: build-agt-wheels + runs-on: ubuntu-22.04 + timeout-minutes: 40 + strategy: + fail-fast: false + matrix: + runtime: + - { name: langgraph, ghcr: kars-runtime-langgraph } + - { name: hermes, ghcr: kars-runtime-hermes } + - { name: maf-python, ghcr: kars-runtime-maf-python } + - { name: anthropic, ghcr: kars-runtime-anthropic } + - { name: openai-agents, ghcr: kars-runtime-openai-agents } + - { name: pydantic-ai, ghcr: kars-runtime-pydantic-ai } + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Download AGT wheels into build context + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: agt-wheels-${{ env.VERSION }} + path: runtimes/wheels/ + + - name: Guard — wheels staged + kars-agt-mesh present + run: | + test -n "$(find runtimes/wheels -maxdepth 1 -name '*.whl')" \ + || { echo "::error::AGT wheels missing in build context"; exit 1; } + test -f runtimes/agt-mesh-python/pyproject.toml \ + || { echo "::error::kars-agt-mesh source missing from checkout"; exit 1; } + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3 + + - name: Log in to GHCR + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build + push ${{ matrix.runtime.ghcr }} (PUBLIC) + id: push + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v6 + with: + context: . + file: sandbox-images/${{ matrix.runtime.name }}/Dockerfile + push: true + platforms: linux/amd64 + tags: | + ${{ env.REGISTRY }}/${{ matrix.runtime.ghcr }}:${{ env.VERSION }} + ${{ env.REGISTRY }}/${{ matrix.runtime.ghcr }}:latest + build-args: | + VERSION=${{ env.VERSION }} + sbom: true + provenance: true + + - name: Make package public (best-effort; may need one-time manual toggle) + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh api -X PATCH \ + -H "Accept: application/vnd.github+json" \ + "/orgs/azure/packages/container/${{ matrix.runtime.ghcr }}/visibility" \ + -f visibility=public \ + || echo "::warning::Could not set ${{ matrix.runtime.ghcr }} public via API; set it once manually." + + - name: Generate SBOM (SPDX JSON) + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0 + with: + image: ${{ env.REGISTRY }}/${{ matrix.runtime.ghcr }}:${{ env.VERSION }} + format: spdx-json + output-file: sbom-${{ matrix.runtime.ghcr }}.spdx.json + + - name: Upload SBOM + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4 + with: + name: sbom-${{ matrix.runtime.ghcr }}-${{ env.VERSION }} + path: sbom-${{ matrix.runtime.ghcr }}.spdx.json + retention-days: 90 + + - name: Install cosign + uses: sigstore/cosign-installer@1aa8e0f2454b781fbf0fbf306a4c9533a0c57409 # v3.7.0 + with: + cosign-release: 'v2.4.1' + + - name: Cosign keyless sign image + env: + COSIGN_EXPERIMENTAL: '1' + run: | + cosign sign --yes ${{ env.REGISTRY }}/${{ matrix.runtime.ghcr }}@${{ steps.push.outputs.digest }} + # ─── Stage 3: pack @kars/cli for GitHub Release distribution ─── cli-pack: name: Pack @kars/cli tarball @@ -355,7 +491,7 @@ jobs: # ─── Stage 4: assemble + publish the PUBLIC GitHub Release ───── release: name: Create PUBLIC GitHub Release - needs: [build-binaries, build-images, build-sandbox, cli-pack] + needs: [build-binaries, build-images, build-sandbox, build-runtimes, cli-pack] runs-on: ubuntu-22.04 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -463,5 +599,18 @@ jobs: ${{ env.REGISTRY }}/kars-sandbox-base:${{ env.VERSION }} ${{ env.REGISTRY }}/openclaw-sandbox:${{ env.VERSION }} ``` + + ### Runtime adapter images (PUBLIC GHCR, amd64) + For `kars add --runtime `. Each bundles the upstream AGT + governance wheels + kars-agt-mesh (kars's spec-compliant Python + MeshClient): + ``` + ${{ env.REGISTRY }}/kars-runtime-langgraph:${{ env.VERSION }} + ${{ env.REGISTRY }}/kars-runtime-hermes:${{ env.VERSION }} + ${{ env.REGISTRY }}/kars-runtime-maf-python:${{ env.VERSION }} + ${{ env.REGISTRY }}/kars-runtime-anthropic:${{ env.VERSION }} + ${{ env.REGISTRY }}/kars-runtime-openai-agents:${{ env.VERSION }} + ${{ env.REGISTRY }}/kars-runtime-pydantic-ai:${{ env.VERSION }} + ``` All signed with cosign keyless (verify via Sigstore Rekor). SBOMs attached below. From 5673a74c255bda12a3e365af6fbb1cceeb71f450 Mon Sep 17 00:00:00 2001 From: Pal Lakatos-Toth Date: Mon, 22 Jun 2026 15:46:31 +0200 Subject: [PATCH 04/21] ci(release): add GitHub-native build provenance attestations Every published artefact is now attested with actions/attest-build-provenance (GitHub-signed, verifiable via `gh attestation verify`): - all 13 container images (by digest, attestation pushed to the registry) - the Rust binaries (amd64 + arm64) - the @kars/cli tarball This is on top of the existing cosign keyless signatures + SBOMs (buildkit in-registry SBOM attestation + standalone SPDX-JSON on the Release) and buildkit inline SLSA provenance. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/release-public-interim.yml | 31 ++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/.github/workflows/release-public-interim.yml b/.github/workflows/release-public-interim.yml index 6d63e154..52cce545 100644 --- a/.github/workflows/release-public-interim.yml +++ b/.github/workflows/release-public-interim.yml @@ -89,6 +89,11 @@ jobs: chmod +x ./bin/*/kars-* ( cd ./bin && find . -type f -name 'kars-*' -exec sha256sum {} \; | tee SHA256SUMS ) file ./bin/*/kars-* + - name: Attest build provenance (binaries, GitHub-signed) + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-path: 'bin/*/kars-*' + - name: Upload binaries uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4 with: @@ -180,6 +185,13 @@ jobs: sbom: true provenance: true + - name: Attest build provenance (GitHub-signed, pushed to registry) + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-name: ${{ env.REGISTRY }}/${{ matrix.image.ghcr }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true + - name: Make package public (best-effort; may need one-time manual toggle) continue-on-error: true env: @@ -292,6 +304,13 @@ jobs: sbom: true provenance: true + - name: Attest build provenance (GitHub-signed, pushed to registry) + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-name: ${{ env.REGISTRY }}/openclaw-sandbox + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true + - name: Make package public (best-effort; may need one-time manual toggle) continue-on-error: true env: @@ -428,6 +447,13 @@ jobs: sbom: true provenance: true + - name: Attest build provenance (GitHub-signed, pushed to registry) + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-name: ${{ env.REGISTRY }}/${{ matrix.runtime.ghcr }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true + - name: Make package public (best-effort; may need one-time manual toggle) continue-on-error: true env: @@ -481,6 +507,11 @@ jobs: npm run build npm pack ls -la ./*.tgz + - name: Attest build provenance (CLI tarball, GitHub-signed) + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-path: 'cli/*.tgz' + - name: Upload CLI tarball uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4 with: From 6f47d82b2cec591b3d4257b86a9baa8db0959b83 Mon Sep 17 00:00:00 2001 From: Pal Lakatos-Toth Date: Mon, 22 Jun 2026 16:42:34 +0200 Subject: [PATCH 05/21] fix(release): aarch64 aws-lc-sys cross-compile + stale runtime AGT pins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes surfaced by the first interim-release run: 1. build-binaries: the arm64 cross build of aws-lc-sys (BoringSSL C) failed with "sys/types.h: No such file or directory" because --no-install-recommends dropped the cross libc headers. Install libc6-dev-arm64-cross + linux-libc-dev-arm64-cross explicitly. 2. Five runtime adapters (langgraph, maf-python, anthropic, openai-agents, pydantic-ai) pinned a2a_agentmesh>=3.3.0,<4 and an UNUSED agent_sandbox dep, incompatible with the AGT 4.0.0 pin — pip fell back to PyPI and failed on a transitive agent-sandbox<4. Drop the unused agent_sandbox dep and bump a2a_agentmesh to <5. The a2a_agentmesh import is already behind a graceful try/except fallback. Verified locally: each adapter installs + imports cleanly against the 4.0.0 wheels. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/release-public-interim.yml | 13 +++++++++++-- runtimes/anthropic/pyproject.toml | 3 +-- runtimes/langgraph/pyproject.toml | 3 +-- runtimes/maf-python/pyproject.toml | 3 +-- runtimes/openai-agents/pyproject.toml | 3 +-- runtimes/pydantic-ai/pyproject.toml | 3 +-- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release-public-interim.yml b/.github/workflows/release-public-interim.yml index 52cce545..682afbdc 100644 --- a/.github/workflows/release-public-interim.yml +++ b/.github/workflows/release-public-interim.yml @@ -67,10 +67,19 @@ jobs: with: shared-key: rust-glibc-release-multiarch save-if: false - - name: Install aarch64 cross-linker + - name: Install aarch64 cross toolchain + libc headers run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends gcc-aarch64-linux-gnu + # gcc-aarch64-linux-gnu alone is not enough to compile aws-lc-sys + # (BoringSSL C) for arm64: --no-install-recommends drops the cross + # libc dev headers, so the build fails with "sys/types.h: No such + # file or directory". Pull them in explicitly: + # libc6-dev-arm64-cross → sys/types.h, bits/libc-header-start.h + # linux-libc-dev-arm64-cross → asm/types.h, linux/types.h + sudo apt-get install -y --no-install-recommends \ + gcc-aarch64-linux-gnu \ + libc6-dev-arm64-cross \ + linux-libc-dev-arm64-cross - name: cargo build --release (amd64 + arm64) env: CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc diff --git a/runtimes/anthropic/pyproject.toml b/runtimes/anthropic/pyproject.toml index 71d75994..baf79975 100644 --- a/runtimes/anthropic/pyproject.toml +++ b/runtimes/anthropic/pyproject.toml @@ -39,8 +39,7 @@ dependencies = [ "httpx>=0.27,<1", # AGT-Python packages (built from upstream by runtimes/build-agt-wheels.sh # and `pip install`-ed from runtimes/wheels/ in the Dockerfile). - "a2a_agentmesh>=3.3.0,<4", - "agent_sandbox>=3.3.0,<4", + "a2a_agentmesh>=3.3.0,<5", ] [project.optional-dependencies] diff --git a/runtimes/langgraph/pyproject.toml b/runtimes/langgraph/pyproject.toml index 4d1d6430..3e75cee4 100644 --- a/runtimes/langgraph/pyproject.toml +++ b/runtimes/langgraph/pyproject.toml @@ -41,8 +41,7 @@ dependencies = [ "httpx>=0.27,<1", # AGT-Python packages (built from upstream by runtimes/build-agt-wheels.sh # and `pip install`-ed from runtimes/wheels/ in the Dockerfile). - "a2a_agentmesh>=3.3.0,<4", - "agent_sandbox>=3.3.0,<4", + "a2a_agentmesh>=3.3.0,<5", ] [project.optional-dependencies] diff --git a/runtimes/maf-python/pyproject.toml b/runtimes/maf-python/pyproject.toml index a46bbb84..b24b09a5 100644 --- a/runtimes/maf-python/pyproject.toml +++ b/runtimes/maf-python/pyproject.toml @@ -40,8 +40,7 @@ dependencies = [ # AGT-Python packages — built locally from upstream by # `runtimes/build-agt-wheels.sh`. See runtimes/openai-agents/README.md # for the install pattern. - "a2a_agentmesh>=3.3.0,<4", - "agent_sandbox>=3.3.0,<4", + "a2a_agentmesh>=3.3.0,<5", ] [project.optional-dependencies] diff --git a/runtimes/openai-agents/pyproject.toml b/runtimes/openai-agents/pyproject.toml index d159efa1..617143c0 100644 --- a/runtimes/openai-agents/pyproject.toml +++ b/runtimes/openai-agents/pyproject.toml @@ -40,8 +40,7 @@ dependencies = [ # them installed (or a --find-links source) resolves cleanly. If they are # not available in the active Python environment install them via: # pip install --find-links ../wheels a2a_agentmesh agent_sandbox - "a2a_agentmesh>=3.3.0,<4", - "agent_sandbox>=3.3.0,<4", + "a2a_agentmesh>=3.3.0,<5", ] [project.optional-dependencies] diff --git a/runtimes/pydantic-ai/pyproject.toml b/runtimes/pydantic-ai/pyproject.toml index 9f39a4b2..9d6093bc 100644 --- a/runtimes/pydantic-ai/pyproject.toml +++ b/runtimes/pydantic-ai/pyproject.toml @@ -39,8 +39,7 @@ dependencies = [ "httpx>=0.27,<1", # AGT-Python packages (built from upstream by runtimes/build-agt-wheels.sh # and `pip install`-ed from runtimes/wheels/ in the Dockerfile). - "a2a_agentmesh>=3.3.0,<4", - "agent_sandbox>=3.3.0,<4", + "a2a_agentmesh>=3.3.0,<5", ] [project.optional-dependencies] From e266f83ffd3847f371d184e4de18b1e50fd76cdb Mon Sep 17 00:00:00 2001 From: Pal Lakatos-Toth Date: Mon, 22 Jun 2026 20:05:48 +0200 Subject: [PATCH 06/21] fix(maf-runtime): use agent-framework-core/openai + tool decorator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The maf-python image failed to build: pip's resolver hit ResolutionTooDeep (986s, 200k rounds) on the `agent-framework` meta-package, whose transitive graph is too complex to solve. Depend on the split sub-packages instead — agent-framework-core (`tool`, Executor, …) + agent-framework-openai (`OpenAIChatClient`) — which resolve in seconds. Also modernise the adapter to MAF 1.x: `ai_function` was removed; swap the mesh-tool decorators to `@tool` (same name/description kwargs). Verified locally: adapter installs in ~12s and build_mesh_tools() yields mesh_inbox + mesh_send. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- runtimes/maf-python/pyproject.toml | 13 +++++++++---- .../src/kars_runtime_maf_python/mesh_tools.py | 10 +++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/runtimes/maf-python/pyproject.toml b/runtimes/maf-python/pyproject.toml index b24b09a5..3599ff15 100644 --- a/runtimes/maf-python/pyproject.toml +++ b/runtimes/maf-python/pyproject.toml @@ -26,10 +26,15 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ - # Microsoft's unified Agent Framework Python SDK - # (https://github.com/microsoft/agent-framework). PyPI distribution - # name: `agent-framework`. - "agent-framework>=1.0.0b1", + # Microsoft's Agent Framework Python SDK + # (https://github.com/microsoft/agent-framework). We depend on the + # split sub-packages rather than the `agent-framework` meta-package: + # the meta-package's transitive graph is too large for pip's resolver + # (ResolutionTooDeep), whereas core + openai resolve in seconds. + # agent-framework-core → `agent_framework` (tool, Executor, …) + # agent-framework-openai → `agent_framework.openai.OpenAIChatClient` + "agent-framework-core>=1.8,<2", + "agent-framework-openai>=1.8,<2", "openai>=1.50,<2", "azure-identity>=1.17,<2", "opentelemetry-api>=1.27,<2", diff --git a/runtimes/maf-python/src/kars_runtime_maf_python/mesh_tools.py b/runtimes/maf-python/src/kars_runtime_maf_python/mesh_tools.py index cbdb688d..75efc3cf 100644 --- a/runtimes/maf-python/src/kars_runtime_maf_python/mesh_tools.py +++ b/runtimes/maf-python/src/kars_runtime_maf_python/mesh_tools.py @@ -3,7 +3,7 @@ """ First-class AgentMesh tools for the Microsoft Agent Framework Python SDK. -Wraps :mod:`mesh` as ``agent_framework.ai_function``-decorated callables +Wraps :mod:`mesh` as ``agent_framework.tool``-decorated callables so MAF agents pick them up automatically. See ``runtimes/openai-agents/.../mesh_tools.py`` for the design rationale. """ @@ -38,15 +38,15 @@ def build_mesh_tools() -> List[Any]: - """Return ``ai_function``-decorated callables for the mesh tools.""" - from agent_framework import ai_function # type: ignore + """Return ``@tool``-decorated callables for the mesh tools.""" + from agent_framework import tool # type: ignore - @ai_function(name="mesh_inbox", description=_MESH_INBOX_DESCRIPTION) + @tool(name="mesh_inbox", description=_MESH_INBOX_DESCRIPTION) async def mesh_inbox() -> str: msgs = receive_messages() return json.dumps(msgs) - @ai_function(name="mesh_send", description=_MESH_SEND_DESCRIPTION) + @tool(name="mesh_send", description=_MESH_SEND_DESCRIPTION) async def mesh_send(target_agent: str, content: str, skill_id: str = "chat") -> str: ack = send_message(target_agent, content, skill_id=skill_id) return json.dumps(ack) From f37ab52aca7e3783afbaea81d23c24074c5fd18a Mon Sep 17 00:00:00 2001 From: Pal Lakatos-Toth Date: Mon, 22 Jun 2026 21:42:20 +0200 Subject: [PATCH 07/21] fix(release): build mesh-plugin/dist before openclaw-sandbox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The openclaw-sandbox Dockerfile hard-COPYs mesh-plugin/dist/ (it ships @kars/mesh as a file: dep with the prepare script stripped), so dist/ must exist in the build context. The build-sandbox job was missing the build step → "/mesh-plugin/dist: not found". Add npm ci + npm run build for mesh-plugin before the buildx build, mirroring what the CLI's stage-mesh-plugin helper does locally. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/release-public-interim.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/release-public-interim.yml b/.github/workflows/release-public-interim.yml index 682afbdc..1ee32558 100644 --- a/.github/workflows/release-public-interim.yml +++ b/.github/workflows/release-public-interim.yml @@ -284,6 +284,20 @@ jobs: echo "AGT_SDK_TARBALL=$(basename "$TARBALL")" >> "$GITHUB_ENV" echo "::notice::Bundling patched AGT SDK: $(basename "$TARBALL")" + - name: Build mesh-plugin (openclaw Dockerfile COPYs mesh-plugin/dist) + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4 + with: + node-version: '22' + - name: npm ci + build mesh-plugin + working-directory: mesh-plugin + run: | + # The openclaw Dockerfile hard-COPYs mesh-plugin/dist/ (it ships + # @kars/mesh as a file: dep with the prepare script stripped), so + # dist/ must exist in the build context before buildx runs. + npm ci --no-audit --no-fund + npm run build + test -d dist || { echo "::error::mesh-plugin/dist not produced"; exit 1; } + - name: Set up Docker Buildx uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3 From 912d5471fa265ab15d019e70db3d4b21478dae93 Mon Sep 17 00:00:00 2001 From: Pal Lakatos-Toth Date: Mon, 22 Jun 2026 23:03:36 +0200 Subject: [PATCH 08/21] ci(release): publish AGT mesh relay + registry images Add a build-mesh job that builds the AGT relay + registry from the pinned AGT checkout (agent-mesh/docker/Dockerfile, COMPONENT=relay|registry) and publishes them as ghcr.io/azure/kars-agentmesh-{relay,registry}, signed + SBOM'd + attested. This completes the published set so `kars dev --release` can pull everything (sandbox + runtimes + relay + registry) with no AGT clone and no local build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/release-public-interim.yml | 100 ++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-public-interim.yml b/.github/workflows/release-public-interim.yml index 1ee32558..74d482ac 100644 --- a/.github/workflows/release-public-interim.yml +++ b/.github/workflows/release-public-interim.yml @@ -513,6 +513,97 @@ jobs: run: | cosign sign --yes ${{ env.REGISTRY }}/${{ matrix.runtime.ghcr }}@${{ steps.push.outputs.digest }} + # ─── Stage 2e: build the AGT mesh relay + registry images ────── + # kars dev / up run the AGT relay + registry for the mesh. They are built + # from the pinned AGT checkout (agent-mesh/docker/Dockerfile, COMPONENT=*). + # Publishing them lets `kars dev --release` pull everything — no AGT clone. + build-mesh: + name: Build + push agentmesh-${{ matrix.component }} (PUBLIC GHCR) + runs-on: ubuntu-22.04 + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + component: [relay, registry] + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Clone pinned AGT + run: | + URL=$(jq -r .url vendor/agt/pin.json) + SHA=$(jq -r .sha vendor/agt/pin.json) + git clone --filter=blob:none "$URL" /tmp/agt + git -C /tmp/agt checkout "$SHA" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3 + + - name: Log in to GHCR + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build + push agentmesh-${{ matrix.component }} (PUBLIC) + id: push + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v6 + with: + context: /tmp/agt + file: /tmp/agt/agent-governance-python/agent-mesh/docker/Dockerfile + push: true + platforms: linux/amd64 + tags: | + ${{ env.REGISTRY }}/kars-agentmesh-${{ matrix.component }}:${{ env.VERSION }} + ${{ env.REGISTRY }}/kars-agentmesh-${{ matrix.component }}:latest + build-args: | + COMPONENT=${{ matrix.component }} + sbom: true + provenance: true + + - name: Attest build provenance (GitHub-signed, pushed to registry) + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-name: ${{ env.REGISTRY }}/kars-agentmesh-${{ matrix.component }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true + + - name: Make package public (best-effort; may need one-time manual toggle) + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh api -X PATCH \ + -H "Accept: application/vnd.github+json" \ + "/orgs/azure/packages/container/kars-agentmesh-${{ matrix.component }}/visibility" \ + -f visibility=public \ + || echo "::warning::Could not set kars-agentmesh-${{ matrix.component }} public via API; set it once manually." + + - name: Generate SBOM (SPDX JSON) + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0 + with: + image: ${{ env.REGISTRY }}/kars-agentmesh-${{ matrix.component }}:${{ env.VERSION }} + format: spdx-json + output-file: sbom-kars-agentmesh-${{ matrix.component }}.spdx.json + + - name: Upload SBOM + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4 + with: + name: sbom-kars-agentmesh-${{ matrix.component }}-${{ env.VERSION }} + path: sbom-kars-agentmesh-${{ matrix.component }}.spdx.json + retention-days: 90 + + - name: Install cosign + uses: sigstore/cosign-installer@1aa8e0f2454b781fbf0fbf306a4c9533a0c57409 # v3.7.0 + with: + cosign-release: 'v2.4.1' + + - name: Cosign keyless sign image + env: + COSIGN_EXPERIMENTAL: '1' + run: | + cosign sign --yes ${{ env.REGISTRY }}/kars-agentmesh-${{ matrix.component }}@${{ steps.push.outputs.digest }} + # ─── Stage 3: pack @kars/cli for GitHub Release distribution ─── cli-pack: name: Pack @kars/cli tarball @@ -545,7 +636,7 @@ jobs: # ─── Stage 4: assemble + publish the PUBLIC GitHub Release ───── release: name: Create PUBLIC GitHub Release - needs: [build-binaries, build-images, build-sandbox, build-runtimes, cli-pack] + needs: [build-binaries, build-images, build-sandbox, build-runtimes, build-mesh, cli-pack] runs-on: ubuntu-22.04 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -666,5 +757,12 @@ jobs: ${{ env.REGISTRY }}/kars-runtime-openai-agents:${{ env.VERSION }} ${{ env.REGISTRY }}/kars-runtime-pydantic-ai:${{ env.VERSION }} ``` + + ### AGT mesh images (PUBLIC GHCR, amd64) + The relay + registry that `kars dev`/`up` run for the mesh: + ``` + ${{ env.REGISTRY }}/kars-agentmesh-relay:${{ env.VERSION }} + ${{ env.REGISTRY }}/kars-agentmesh-registry:${{ env.VERSION }} + ``` All signed with cosign keyless (verify via Sigstore Rekor). SBOMs attached below. From 2bc084d1392215b0b4ddf8d504087eab875a61f9 Mon Sep 17 00:00:00 2001 From: Pal Lakatos-Toth Date: Mon, 22 Jun 2026 23:12:24 +0200 Subject: [PATCH 09/21] feat(cli): kars dev --release (run from published images) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a no-compile, no-AGT-clone path: `kars dev --release v0.1.0-interim.N` pulls the published images from ghcr.io/azure/* at the given tag instead of building anything locally: - openclaw-sandbox → the agent image (skips the local sandbox build) - kars-agentmesh-relay / -registry → pulled + tagged :dev for the run path It forces --build off, skips the AGT auto-clone + wheel build entirely, and errors clearly if combined with --build or --target local-k8s (docker only for now). Typecheck + lint clean; 798 CLI tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/src/commands/dev.ts | 103 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 94 insertions(+), 9 deletions(-) diff --git a/cli/src/commands/dev.ts b/cli/src/commands/dev.ts index 9485dad3..0dc9b405 100644 --- a/cli/src/commands/dev.ts +++ b/cli/src/commands/dev.ts @@ -97,6 +97,12 @@ const SANDBOX_BASE_IMAGE = const AZURELINUX_BASE = "mcr.microsoft.com/azurelinux/base/core:3.0"; +// Public registry for `--release` mode (no-compile, no-AGT-clone bringup). +// Mirrors the names published by .github/workflows/release-public-interim.yml. +const RELEASE_REGISTRY = "ghcr.io/azure"; +const releaseImage = (name: string, version: string): string => + `${RELEASE_REGISTRY}/${name}:${version}`; + const AGT_NETWORK = "kars-dev"; const AGT_POSTGRES = "kars-agt-postgres"; const AGT_RELAY = "kars-agt-relay"; @@ -166,6 +172,10 @@ NOT overwrite your saved credentials. "One-off GitHub Models override (does NOT save). Requires a PAT with `models:read`. To save GitHub Models as your default provider, run without this flag and pick GitHub Models at the prompt." ) // ── Image build ──────────────────────────────────────────────────── + .option( + "--release ", + "Run from PUBLISHED images instead of building. Pulls ghcr.io/azure/* at the given release tag (e.g. v0.1.0-interim.5) — no AGT clone, no local compile. Skips --build.", + ) .option( "--image ", "Sandbox container image", @@ -261,6 +271,26 @@ Notes: process.exit(1); } + // ── Release mode: run from published images, no build/clone ─── + // `--release ` pulls everything from ghcr.io/azure/* at the + // given tag: the openclaw-sandbox, the AGT relay + registry, and any + // selected runtime image. It forces build OFF so we never compile or + // clone the AGT toolkit. An explicit --build is contradictory. + const releaseMode = typeof options.release === "string" && options.release.length > 0; + const releaseVersion: string | undefined = releaseMode ? options.release : undefined; + if (releaseMode) { + if (options.build === true || options.buildBase === true) { + console.error(chalk.red(`\n Error: --release pulls published images; it cannot be combined with --build/--build-base.\n`)); + process.exit(1); + } + options.build = false; + // Use the published sandbox image unless the user pinned --image. + const imagePassed = process.argv.some(a => a === "--image" || a.startsWith("--image=")); + if (!imagePassed) { + options.image = releaseImage("openclaw-sandbox", releaseVersion!); + } + } + // ── First-run target prompt ────────────────────────────────── // Brand-new user with no saved creds AND no explicit --target // flag: ask docker vs local-k8s up front, before we get into @@ -462,7 +492,7 @@ Notes: // users testing local changes can opt in here without // remembering the --build flag. Skipped if --build was passed // explicitly. - if (!options.build) { + if (!options.build && !releaseMode) { const { rebuild } = await inquirer.prompt([{ type: "confirm", name: "rebuild", @@ -603,6 +633,13 @@ Notes: process.exit(1); } if (options.target === "local-k8s") { + if (releaseMode) { + console.error(chalk.red(`\n Error: --release currently supports --target docker only.`)); + console.error(chalk.dim(` (local-k8s loads images into kind + Helm; published-image support there is tracked separately.)\n`)); + console.error(chalk.yellow(` Run the published images locally with:`)); + console.error(chalk.cyan(` kars dev --release ${releaseVersion}\n`)); + process.exit(1); + } const { runLocalK8s } = await import("./dev/local-k8s.js"); try { await runLocalK8s({ @@ -648,13 +685,19 @@ Notes: // machines can `kars dev --build` without first cloning AGT by // hand. Explicit --agt-repo / $KARS_AGT_REPO still win. let agtRepo: string; - try { - agtRepo = await ensureAgtRepo(options.agtRepo, repoRoot); - } catch (e: unknown) { + if (releaseMode) { + // Release mode pulls the published relay/registry images — no AGT + // checkout needed. Keep a harmless default so later refs don't NPE. agtRepo = options.agtRepo ?? process.env.KARS_AGT_REPO ?? DEFAULT_AGT_REPO; - if (meshProvider === "agt" && options.build) { - console.error(chalk.red(`\n Auto-cloning AGT failed:\n ${(e as Error).message}\n`)); - process.exit(1); + } else { + try { + agtRepo = await ensureAgtRepo(options.agtRepo, repoRoot); + } catch (e: unknown) { + agtRepo = options.agtRepo ?? process.env.KARS_AGT_REPO ?? DEFAULT_AGT_REPO; + if (meshProvider === "agt" && options.build) { + console.error(chalk.red(`\n Auto-cloning AGT failed:\n ${(e as Error).message}\n`)); + process.exit(1); + } } } if (meshProvider === "agt" && options.build) { @@ -783,7 +826,27 @@ Notes: stepper.step("Resolving sandbox image..."); let imageExists = false; - if (!options.build) { + if (releaseMode) { + stepper.update(`Pulling published sandbox image (${image})...`); + try { + // openclaw-sandbox is published amd64-only; on Apple Silicon it + // runs under emulation (Docker Desktop handles this transparently). + await execa("docker", ["pull", "--platform", "linux/amd64", image], { stdio: "inherit" }); + imageExists = true; + } catch { + stepper.fail("Could not pull published sandbox image"); + console.log(chalk.yellow(` + Failed to pull ${chalk.bold(image)}. + + Verify the release tag exists and the package is public: + ${chalk.cyan(`docker pull ${image}`)} + + Browse tags: https://github.com/Azure/kars/pkgs/container/openclaw-sandbox +`)); + process.exit(1); + } + } + if (!options.build && !releaseMode) { stepper.update("Checking for sandbox image..."); try { const { stdout: cachedArch } = await execa("docker", [ @@ -805,7 +868,7 @@ Notes: } } - if (options.build || !imageExists) { + if (!releaseMode && (options.build || !imageExists)) { const baseImage = options.baseImage; // Check if Azure Linux base image exists locally, pull if not @@ -1090,6 +1153,28 @@ Notes: // Local registry mode — deploy AGT relay/registry locally stepper.step("Starting mesh infrastructure (agt)..."); + // Release mode: pull the published relay + registry and tag them + // as the local :dev names the run/build steps below expect. This + // satisfies the build-fallback's haveImage() checks so nothing is + // built and the AGT checkout is never needed. + if (releaseMode) { + for (const [name, devTag] of [ + ["kars-agentmesh-relay", "agentmesh-relay:dev"], + ["kars-agentmesh-registry", "agentmesh-registry:dev"], + ] as const) { + const ref = releaseImage(name, releaseVersion!); + stepper.update(`Pulling published ${name}...`); + try { + await execa("docker", ["pull", "--platform", "linux/amd64", ref], { stdio: "pipe" }); + await execa("docker", ["tag", ref, devTag], { stdio: "pipe" }); + } catch { + stepper.fail(`Could not pull ${ref}`); + console.log(chalk.yellow(`\n Failed to pull ${chalk.bold(ref)}. Verify the release tag exists and the package is public.\n`)); + process.exit(1); + } + } + } + // Helper: check if a docker image exists locally async function haveImage(tag: string): Promise { try { From c592f880f5593769f92e0194d282c26313cc487a Mon Sep 17 00:00:00 2001 From: Pal Lakatos-Toth Date: Tue, 23 Jun 2026 00:05:19 +0200 Subject: [PATCH 10/21] test(e2e-harness): KARS_RELEASE for the docker platform Lets the exec-brief harness bring the sandbox up from published images via `kars dev --release ` (no local build, no AGT clone) by setting KARS_RELEASE=v0.1.0-interim.N. Verifies the released quick-setup path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/e2e-harness/platforms/docker.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tools/e2e-harness/platforms/docker.sh b/tools/e2e-harness/platforms/docker.sh index 55f1e578..7f9f28e4 100644 --- a/tools/e2e-harness/platforms/docker.sh +++ b/tools/e2e-harness/platforms/docker.sh @@ -33,6 +33,10 @@ # SKIP_DEV_BRINGUP — set to 1 to skip the `kars dev # --target docker` step if the container is # already up. +# KARS_RELEASE — when set (e.g. v0.1.0-interim.5), bring the +# sandbox up from PUBLISHED images via +# `kars dev --release ` (no local build, +# no AGT clone). Verifies the released path. set -euo pipefail @@ -57,8 +61,15 @@ platform_preflight() { # applied — docker mode requires the scenario to either preload # the bundles into the image or provide a docker-overlay/ dir # the helper sources. We do not pretend otherwise. + # KARS_RELEASE → pull published images instead of building. + release_args=() + if [ -n "${KARS_RELEASE:-}" ]; then + release_args=(--release "${KARS_RELEASE}") + log "released mode: kars dev --release ${KARS_RELEASE} (published images)" + fi kars dev --target docker \ --name "${DOCKER_CONTAINER_NAME}" \ + "${release_args[@]}" \ >>"${OUT_DIR}/dev-bringup.log" 2>&1 || { log "ERR kars dev bring-up failed; tail of dev-bringup.log:" tail -n 80 "${OUT_DIR}/dev-bringup.log" >&2 || true From 5cafd40a5ad8545a5bd50d555dbc3ffcb217131b Mon Sep 17 00:00:00 2001 From: Pal Lakatos-Toth Date: Tue, 23 Jun 2026 00:09:10 +0200 Subject: [PATCH 11/21] docs: add no-compile published-images quick start (kars dev --release) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a foolproof "fastest path" to README + getting-started: install the CLI from a published release tarball and `kars dev --release ` to launch from pre-built, cosign-signed images — no Rust, no AGT checkout, no local image build. The from-source path is preserved for contributors. Clarifies the AGT-mesh clone is source-builds-only (--release pulls relay/registry). NOTE: describes v0.1.0-interim.6 (first release carrying the --release CLI). Pending end-to-end verification of the released quick-setup before merge. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 26 ++++++++++++++------- docs/getting-started.md | 51 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index c102d041..0c41c8ed 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,22 @@ Same CRDs. Same router code path. Same audit format. Same governance profiles. T ## Try it in five minutes -**Fastest path (recommended): GitHub Copilot.** If you have an active Copilot seat (Individual / Business / Enterprise), the only thing you need beyond Docker is one device-code login. No Azure account, no PAT, no key files. +**Fastest path — published images, no compile.** With Docker Desktop running and Node.js 22+, two commands get you a running agent — no Rust toolchain, no AGT checkout, no local image build: + +```bash +# 1. Install the CLI from the latest interim release +npm i -g https://github.com/Azure/kars/releases/download/v0.1.0-interim.6/kars-cli-0.1.0.tgz + +# 2. Launch a sandbox from the published, cosign-signed images +kars dev --release v0.1.0-interim.6 +``` + +> Use the newest tag from the [Releases page](https://github.com/Azure/kars/releases) — the `npm i -g …` URL and the `--release` flag must match. Apple Silicon Macs run the `linux/amd64` images under Docker's emulation transparently. + +On first launch you'll pick an inference provider (see below). **GitHub Copilot** is the easiest — one device-code login, no Azure account. + +
+Build from source instead (to hack on the controller / router / plugin) > **Prerequisites:** Docker Desktop · Node.js 22+ · Rust 1.88+ · one of: an active GitHub Copilot seat, an Azure AI Foundry deployment, or a GitHub PAT with `models:read`. @@ -144,13 +159,8 @@ cd cli && npm ci && npm run build && npm link && cd .. kars dev ``` -> **Faster clone (no sandbox base build):** The repo ships ~235 MB of vendored Python wheels under `vendor/sandbox-wheels/` so the sandbox base image builds hermetically (no PyPI dependency). If you only want to read code, run the CLI, or build the controller / router — skip the wheels with a partial clone: -> ```bash -> git clone --filter=blob:none https://github.com/Azure/kars.git -> ``` -> Files are fetched on demand when you `git checkout` or `git log` actually needs them. If you later need the wheels (because you're rebuilding `kars-sandbox-base`), run `git lfs install && git lfs pull` (the wheels are tracked via LFS going forward) or simply `git checkout vendor/sandbox-wheels` to materialize them. - -The first `kars dev` pulls + caches the sandbox base image (~10 min once) and then launches near-instantly thereafter. +The first source build of `kars dev` pulls + caches the sandbox base image (~10 min once) and then launches near-instantly thereafter. +
For Microsoft / Azure-org members: skip the build diff --git a/docs/getting-started.md b/docs/getting-started.md index 2c99481a..4ac18e33 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -15,15 +15,19 @@ The sandbox YAML you wrote in step 1 runs unchanged in step 2. That is the whole | For | You need | |---|---| -| **Local mode (GitHub Copilot — recommended)** | Docker Desktop (or any OCI runtime), Node.js 22+, Rust 1.88+, an active **GitHub Copilot** seat (Individual / Business / Enterprise). One device-code OAuth login at signup. **No Azure account, no PAT, no key files.** | -| Local mode (Foundry / Azure OpenAI) | Docker Desktop, Node.js 22+, Rust 1.88+, an Azure AI Foundry (or Azure OpenAI) endpoint + deployment + key. | -| Local mode (GitHub Models) | Docker Desktop, Node.js 22+, Rust 1.88+, a GitHub PAT with `models:read` scope. **No Azure account needed.** | +| **Fastest — published images (`kars dev --release`)** | Docker Desktop (or any OCI runtime) · Node.js 22+. **No Rust, no AGT checkout, no local image build.** Plus an inference provider (GitHub Copilot seat — easiest — or a Foundry/Azure OpenAI deployment, or a GitHub PAT with `models:read`). | +| Local mode from source (GitHub Copilot — recommended) | Docker Desktop (or any OCI runtime), Node.js 22+, Rust 1.88+, an active **GitHub Copilot** seat (Individual / Business / Enterprise). One device-code OAuth login at signup. **No Azure account, no PAT, no key files.** | +| Local mode from source (Foundry / Azure OpenAI) | Docker Desktop, Node.js 22+, Rust 1.88+, an Azure AI Foundry (or Azure OpenAI) endpoint + deployment + key. | +| Local mode from source (GitHub Models) | Docker Desktop, Node.js 22+, Rust 1.88+, a GitHub PAT with `models:read` scope. **No Azure account needed.** | | AKS mode | The above, plus the [Azure CLI](https://learn.microsoft.com/cli/azure/) (`az`), [`kubectl`](https://kubernetes.io/docs/tasks/tools/), [Helm 3.14+](https://helm.sh/), and an Azure subscription where you can create resource groups. | -> **AGT mesh prerequisite (sub-agent spawning).** Inter-agent E2E +> **Just want it running?** Use the [fastest path](#10-fastest-path--no-compile-published-images-) — `npm i -g ` then `kars dev --release `. Everything below about building from source and cloning AGT is **only** for contributors hacking on kars itself. + +> **AGT mesh prerequisite — source builds only.** Inter-agent E2E > messaging uses the [Microsoft Agent Governance Toolkit](https://github.com/microsoft/agent-governance-toolkit) -> relay + registry, which kars builds from source locally. Clone it -> next to your kars repo before the first `kars dev` / `kars up`: +> relay + registry. With `kars dev --release` these are **pulled as +> published images** — nothing to clone. Only when you build from source +> does kars build them locally; clone AGT next to your kars repo first: > ```bash > git clone https://github.com/microsoft/agent-governance-toolkit ~/agent-governance-toolkit > ``` @@ -99,6 +103,41 @@ If you'd rather skip provisioning by hand, jump to **[Step 2 — Deploy to AKS]( ## Step 1 — Local (five minutes) +### 1.0 Fastest path — no compile (published images) ⭐ + +The quickest way to a running agent: install the CLI from a published +release and launch from **pre-built, cosign-signed images**. No Rust +toolchain, no AGT checkout, no waiting on a local image build. + +**You need only:** Docker Desktop (running) · Node.js 22+. + +```bash +# 1. Install the kars CLI from the latest interim release (one command) +npm i -g https://github.com/Azure/kars/releases/download/v0.1.0-interim.6/kars-cli-0.1.0.tgz + +# 2. Launch a sandbox from the published images for that same release +kars dev --release v0.1.0-interim.6 +``` + +That's it. `--release` pulls the `openclaw-sandbox` agent image plus the AGT +mesh relay + registry and runs them — skipping the AGT clone and every local +build. The first run downloads the images once (a few minutes); afterwards it +launches near-instantly. On first launch you'll pick an inference provider — +see the [provider picker](#12-launch-a-sandbox) below (**GitHub Copilot** is +the easiest: one device-code login, no Azure account). + +> **Use matching versions.** The `npm i -g …` tarball URL and the `--release` +> flag must reference the **same** tag. Browse the +> [Releases page](https://github.com/Azure/kars/releases) for the newest +> `v0.1.0-interim.N` and use it in both places. + +> **Apple Silicon (M-series) Macs:** the sandbox + mesh images are published +> `linux/amd64`; Docker Desktop runs them under emulation transparently +> (slightly slower, fully functional). + +Want to hack on the controller / router / plugin? Build from source instead — +**[1.1 Build the CLI](#11-build-the-cli)**. + ### 1.1 Build the CLI ```bash From 001f6360a8a9355cf714607576db97d800620cc6 Mon Sep 17 00:00:00 2001 From: Pal Lakatos-Toth Date: Tue, 23 Jun 2026 00:51:50 +0200 Subject: [PATCH 12/21] docs(security): audit for kars dev --release capability cli/src/commands/dev.ts gains a capability (--release pulls published images), which the security-audit-required CI gate flags. Add the audit: threat model (image tamper, isolation parity, supply chain, AGT-clone skip), scope, and test posture. Force-added (docs/internal/ is gitignored). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...-06-23-cli-dev-release-published-images.md | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 docs/internal/security-audits/2026-06-23-cli-dev-release-published-images.md diff --git a/docs/internal/security-audits/2026-06-23-cli-dev-release-published-images.md b/docs/internal/security-audits/2026-06-23-cli-dev-release-published-images.md new file mode 100644 index 00000000..54413503 --- /dev/null +++ b/docs/internal/security-audits/2026-06-23-cli-dev-release-published-images.md @@ -0,0 +1,83 @@ +# Security Audit — `kars dev --release` (run from published images) + +PR: Azure/kars#408 (branch `release/published-binaries`) + +## Scope + +Adds a `--release ` flag to `kars dev` (`cli/src/commands/dev.ts`) — a +capability-introducing path under `cli/src/commands/`. In release mode the +CLI: + +- pulls `ghcr.io/azure/openclaw-sandbox:` instead of building the + sandbox image locally, +- pulls `ghcr.io/azure/kars-agentmesh-{relay,registry}:` and tags them + `agentmesh-{relay,registry}:dev` for the existing run path, +- **skips** the AGT auto-clone and every local image build, +- forces `--build` off and rejects `--build`/`--target local-k8s` combos. + +It also adds the interim public release workflow +(`.github/workflows/release-public-interim.yml`) that produces those images. +This audit covers the **CLI capability change**; the image build/publish +supply-chain posture is covered inline below. + +## Threat model + +### T1: `--release` pulls a tampered/malicious image instead of the real one (MITIGATED) +The pulled images are the **same Dockerfiles and sources** as the local +build, pre-built in the org's protected CI. Every image is **cosign keyless +signed** (Sigstore Rekor) **and** carries a **GitHub build-provenance +attestation** (`gh attestation verify`) **plus an SPDX SBOM**. A consumer can +verify provenance before trusting an image. The release tag and the CLI +version are required to match (documented), reducing confusion-style +substitution. + +### T2: `--release` weakens sandbox isolation vs the from-source build (NOT A REGRESSION) +The runtime security posture is identical: `openclaw-sandbox` is the *same* +image the from-source path builds and the controller launches — egress-guard +init (iptables, UID-1000 confinement), the UID 1000/1001 split, seccomp, and +the router L7 allow-list are all baked into the image, not added by the CLI. +`--release` changes only the image **source** (pull vs local build), never +the container's security context or the dev-mode trust boundary. + +### T3: Supply-chain — compromised CI publishes a bad image under a release tag (MITIGATED, residual accepted) +The release workflow runs in the org's GitHub Actions with keyless OIDC +signing; signatures + provenance are recorded publicly in Rekor. A consumer +verifies with `cosign verify` / `gh attestation verify`. Residual risk +(a fully compromised org CI) is the same as for any pre-built artefact and is +the reason the **ESRP-signed** path (MCR + npmjs + crates.io) supersedes this +interim GHCR release later — tracked in `docs/PUBLISHING.md`. + +### T4: `--release` skips the AGT clone — does it skip a security control? (NO) +The AGT clone only provided *source to build relay/registry locally*. In +release mode those are pulled as signed images instead. No governance, +policy, identity, or crypto control is bypassed; the relay forwards opaque +encrypted frames and the registry does prekey/discovery exactly as before. + +## What this audit does NOT cover + +- The **contents** of the published images (the controller/router/sandbox + build posture is covered by their respective prior audits; the patched AGT + SDK is covered by the upstream PR microsoft/agent-governance-toolkit#3128 + + its audit doc). +- The **AKS** `kars up --release` path — not implemented in this change + (`--release` is docker-target only and errors clearly on `local-k8s`). +- ESRP-signed public distribution (future; supersedes the interim release). + +## Test posture + +- `cli` typecheck clean (`tsc --noEmit`); `oxlint` 0 errors; **798 CLI tests + pass** (`vitest`), including `dev.test.ts`. +- `--release` flow validated as far as the authoring environment allows: + loads cached creds, skips the rebuild prompt + AGT clone + local build, and + resolves the correct `ghcr.io/azure/openclaw-sandbox:` ref before the + pull. Full pull→run→agent-response e2e is pending verification in a + working-`docker pull` environment (the authoring host had a local docker + egress block); the exec-brief harness is wired via `KARS_RELEASE`. +- Release pipeline proven green end-to-end: `v0.1.0-interim.6` published with + all images cosign-signed + attested + SBOM'd; the published CLI tarball + carries `--release` (verified). + +## Sign-offs + +Signed-off-by: Pal Lakatos +Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com> From 4f3fe120a44ce5eb327e3a4d6fac58ed4d12033d Mon Sep 17 00:00:00 2001 From: Pal Lakatos-Toth Date: Tue, 23 Jun 2026 06:59:27 +0200 Subject: [PATCH 13/21] fix(cli): kars dev skips optional prompts when stdin is not a TTY MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mesh-source and channels prompts fired unconditionally, crashing `kars dev` (ExitPromptError) in non-interactive contexts — scripts, CI, and the e2e harness — before the sandbox even came up. Guard both with process.stdin.isTTY: non-interactive runs default to local mesh + no channels. Interactive terminals are unchanged. This makes `kars dev` (incl. --release) scriptable end-to-end. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/src/commands/dev.ts | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/cli/src/commands/dev.ts b/cli/src/commands/dev.ts index 0dc9b405..a24b296b 100644 --- a/cli/src/commands/dev.ts +++ b/cli/src/commands/dev.ts @@ -532,20 +532,24 @@ Notes: ? "Local (recommended; relay + registry in the kind cluster alongside the sandbox)" : "Local (recommended; spin up relay + registry in Docker on this laptop)"; - const { default: inquirer } = await import("inquirer"); - const { meshSource } = await inquirer.prompt([{ - type: "list", - name: "meshSource", - message: "Where should the mesh live?", - default: "local", - choices: [ - { - name: localLabel, - value: "local", - }, - { name: remoteLabel, value: "remote" }, - ], - }]); + let meshSource = "local"; + if (process.stdin.isTTY) { + const { default: inquirer } = await import("inquirer"); + const ans = await inquirer.prompt([{ + type: "list", + name: "meshSource", + message: "Where should the mesh live?", + default: "local", + choices: [ + { + name: localLabel, + value: "local", + }, + { name: remoteLabel, value: "remote" }, + ], + }]); + meshSource = ans.meshSource; + } if (meshSource === "remote") { if (!cachedRegistryUrl && !aksAvailable) { @@ -602,7 +606,7 @@ Notes: addChannel("telegram", "telegram-token", "Telegram"); addChannel("slack", "slack-token", "Slack"); addChannel("discord", "discord-token", "Discord"); - if (available.length > 0) { + if (available.length > 0 && process.stdin.isTTY) { const { default: inquirer } = await import("inquirer"); const { picked } = await inquirer.prompt([{ type: "checkbox", From ff6e2d6efade0864302f0cf48b07f626d33e2ac4 Mon Sep 17 00:00:00 2001 From: Pal Lakatos-Toth Date: Tue, 23 Jun 2026 07:20:33 +0200 Subject: [PATCH 14/21] feat(release): multi-arch sandbox-base + openclaw-sandbox (arm64 for Macs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The amd64-only openclaw-sandbox crashes under Rosetta on Apple Silicon (rosetta error: rt_tgsigqueueinfo failed) — verified via the e2e harness on an arm64 Mac. The vendored sandbox wheels (vendor/sandbox-wheels/) already ship aarch64 manylinux variants, so the arm64 image installs pre-built wheels (no source compile). Build sandbox-base + openclaw-sandbox for linux/amd64,linux/arm64 so they run natively on Macs. QEMU + larger timeouts for the arm64 leg. Also corrects README + getting-started: the no-compile --release path is amd64/Linux today; Apple Silicon uses the from-source build until the arm64 images land (this release). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/release-public-interim.yml | 21 ++++++++++------ README.md | 20 +++++++++++---- docs/getting-started.md | 26 +++++++++++--------- 3 files changed, 43 insertions(+), 24 deletions(-) diff --git a/.github/workflows/release-public-interim.yml b/.github/workflows/release-public-interim.yml index 74d482ac..55d34c7e 100644 --- a/.github/workflows/release-public-interim.yml +++ b/.github/workflows/release-public-interim.yml @@ -115,7 +115,7 @@ jobs: name: Build + push ${{ matrix.image.name }} (PUBLIC GHCR) needs: build-binaries runs-on: ubuntu-22.04 - timeout-minutes: 40 + timeout-minutes: 90 outputs: controller_digest: ${{ steps.digests.outputs.controller }} router_digest: ${{ steps.digests.outputs.router }} @@ -146,11 +146,13 @@ jobs: dockerfile: sandbox-images/openclaw/Dockerfile.base ghcr: kars-sandbox-base no_binaries: true - # sandbox-base bundles Python wheels (manylinux); many transitive - # wheels lack aarch64 builds, so arm64 would silently fall back to - # a slow QEMU source compile. Ship amd64-only here — Apple Silicon - # users run it under emulation (documented in the quickstart). - platforms: linux/amd64 + # Multi-arch: the vendored sandbox wheels under vendor/sandbox-wheels/ + # ship aarch64 manylinux variants alongside x86_64, so the arm64 + # build installs pre-built wheels (no source compile). This lets + # openclaw-sandbox run NATIVELY on Apple Silicon (amd64 under Rosetta + # crashes with rt_tgsigqueueinfo). arm64 builds under QEMU here — + # slower, hence the larger timeout on this job. + platforms: linux/amd64,linux/arm64 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -260,7 +262,7 @@ jobs: name: Build + push openclaw-sandbox (PUBLIC GHCR) needs: build-images runs-on: ubuntu-22.04 - timeout-minutes: 40 + timeout-minutes: 90 outputs: sandbox_digest: ${{ steps.push.outputs.digest }} steps: @@ -298,6 +300,9 @@ jobs: npm run build test -d dist || { echo "::error::mesh-plugin/dist not produced"; exit 1; } + - name: Set up QEMU (for arm64 buildx) + uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3 @@ -315,7 +320,7 @@ jobs: context: . file: sandbox-images/openclaw/Dockerfile push: true - platforms: linux/amd64 + platforms: linux/amd64,linux/arm64 tags: | ${{ env.REGISTRY }}/openclaw-sandbox:${{ env.VERSION }} ${{ env.REGISTRY }}/openclaw-sandbox:latest diff --git a/README.md b/README.md index 0c41c8ed..73815233 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,9 @@ Same CRDs. Same router code path. Same audit format. Same governance profiles. T ## Try it in five minutes -**Fastest path — published images, no compile.** With Docker Desktop running and Node.js 22+, two commands get you a running agent — no Rust toolchain, no AGT checkout, no local image build: +**Fastest path — published images, no compile (linux/amd64 hosts).** On an +amd64 Linux box (or any amd64 host with Docker), two commands get you a +running agent — no Rust toolchain, no AGT checkout, no local image build: ```bash # 1. Install the CLI from the latest interim release @@ -142,12 +144,19 @@ npm i -g https://github.com/Azure/kars/releases/download/v0.1.0-interim.6/kars-c kars dev --release v0.1.0-interim.6 ``` -> Use the newest tag from the [Releases page](https://github.com/Azure/kars/releases) — the `npm i -g …` URL and the `--release` flag must match. Apple Silicon Macs run the `linux/amd64` images under Docker's emulation transparently. +> Use the newest tag from the [Releases page](https://github.com/Azure/kars/releases) — the `npm i -g …` URL and the `--release` flag must match. + +> **⚠️ Apple Silicon (M-series) Macs:** the published sandbox image is +> currently `linux/amd64` only, and it **crashes under Rosetta** on arm64 +> (`rosetta error: rt_tgsigqueueinfo failed`). On an Apple Silicon Mac, use +> the **build-from-source** path below instead — `kars dev` builds the +> sandbox natively for your arch. (A native `arm64` published image is +> planned; until then, `--release` is for amd64/Linux hosts.) On first launch you'll pick an inference provider (see below). **GitHub Copilot** is the easiest — one device-code login, no Azure account.
-Build from source instead (to hack on the controller / router / plugin) +Build from source (Apple Silicon Macs, or to hack on the controller / router / plugin) > **Prerequisites:** Docker Desktop · Node.js 22+ · Rust 1.88+ · one of: an active GitHub Copilot seat, an Azure AI Foundry deployment, or a GitHub PAT with `models:read`. @@ -155,11 +164,12 @@ On first launch you'll pick an inference provider (see below). **GitHub Copilot* git clone https://github.com/Azure/kars.git && cd kars cd cli && npm ci && npm run build && npm link && cd .. -# Launch a sandbox locally — Docker only, no Azure, no AKS +# Launch a sandbox locally — Docker only, no Azure, no AKS. +# Builds the sandbox image natively for your architecture (incl. arm64). kars dev ``` -The first source build of `kars dev` pulls + caches the sandbox base image (~10 min once) and then launches near-instantly thereafter. +The first source build of `kars dev` builds + caches the sandbox base image (~10 min once) and then launches near-instantly thereafter.
diff --git a/docs/getting-started.md b/docs/getting-started.md index 4ac18e33..e979a251 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -103,13 +103,14 @@ If you'd rather skip provisioning by hand, jump to **[Step 2 — Deploy to AKS]( ## Step 1 — Local (five minutes) -### 1.0 Fastest path — no compile (published images) ⭐ +### 1.0 Fastest path — no compile (published images, amd64/Linux) ⭐ -The quickest way to a running agent: install the CLI from a published -release and launch from **pre-built, cosign-signed images**. No Rust -toolchain, no AGT checkout, no waiting on a local image build. +The quickest way to a running agent **on an amd64 host (Linux, or amd64 +Docker)**: install the CLI from a published release and launch from +**pre-built, cosign-signed images**. No Rust toolchain, no AGT checkout, no +waiting on a local image build. -**You need only:** Docker Desktop (running) · Node.js 22+. +**You need only:** Docker (running) · Node.js 22+. ```bash # 1. Install the kars CLI from the latest interim release (one command) @@ -126,16 +127,19 @@ launches near-instantly. On first launch you'll pick an inference provider — see the [provider picker](#12-launch-a-sandbox) below (**GitHub Copilot** is the easiest: one device-code login, no Azure account). +> **⚠️ Apple Silicon (M-series) Macs:** the published `openclaw-sandbox` is +> currently `linux/amd64` only and **crashes under Rosetta** on arm64 +> (`rosetta error: rt_tgsigqueueinfo failed`). On Apple Silicon use the +> **[build-from-source path](#11-build-the-cli)** — `kars dev` builds the +> sandbox natively for arm64. A native arm64 published image is planned; +> until then `--release` targets amd64/Linux hosts. + > **Use matching versions.** The `npm i -g …` tarball URL and the `--release` > flag must reference the **same** tag. Browse the > [Releases page](https://github.com/Azure/kars/releases) for the newest -> `v0.1.0-interim.N` and use it in both places. - -> **Apple Silicon (M-series) Macs:** the sandbox + mesh images are published -> `linux/amd64`; Docker Desktop runs them under emulation transparently -> (slightly slower, fully functional). +> `v0.1.0-interim.N`. -Want to hack on the controller / router / plugin? Build from source instead — +Want to hack on the controller / router / plugin? Build from source — **[1.1 Build the CLI](#11-build-the-cli)**. ### 1.1 Build the CLI From 1347ad68d4669915782da7ed8874d49fa8cfb458 Mon Sep 17 00:00:00 2001 From: Pal Lakatos-Toth Date: Tue, 23 Jun 2026 08:19:35 +0200 Subject: [PATCH 15/21] ci(release): build sandbox images on NATIVE arm64 runners (not QEMU) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QEMU multi-arch for sandbox-base + openclaw-sandbox was far too slow (>40 min for sandbox-base alone). Build each arch on its native GitHub-hosted runner instead (amd64 → ubuntu-22.04, arm64 → ubuntu-24.04-arm, free for public repos), push per-arch tags, then merge into multi-arch manifests with imagetools + sign/attest/SBOM on the merged manifest. The fast Rust images stay multi-arch via QEMU (trivial binary COPYs). This gives a native arm64 openclaw-sandbox so kars dev --release runs on Apple Silicon without the Rosetta rt_tgsigqueueinfo crash. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/release-public-interim.yml | 182 ++++++++++++------- 1 file changed, 115 insertions(+), 67 deletions(-) diff --git a/.github/workflows/release-public-interim.yml b/.github/workflows/release-public-interim.yml index 55d34c7e..2aa029ea 100644 --- a/.github/workflows/release-public-interim.yml +++ b/.github/workflows/release-public-interim.yml @@ -121,7 +121,6 @@ jobs: router_digest: ${{ steps.digests.outputs.router }} gateway_digest: ${{ steps.digests.outputs.gateway }} runner_digest: ${{ steps.digests.outputs.runner }} - sandbox_base_digest: ${{ steps.digests.outputs.sandbox_base }} strategy: fail-fast: false matrix: @@ -142,28 +141,15 @@ jobs: dockerfile: sandbox-images/conformance-runner/Dockerfile ghcr: kars-conformance-runner platforms: linux/amd64,linux/arm64 - - name: sandbox-base - dockerfile: sandbox-images/openclaw/Dockerfile.base - ghcr: kars-sandbox-base - no_binaries: true - # Multi-arch: the vendored sandbox wheels under vendor/sandbox-wheels/ - # ship aarch64 manylinux variants alongside x86_64, so the arm64 - # build installs pre-built wheels (no source compile). This lets - # openclaw-sandbox run NATIVELY on Apple Silicon (amd64 under Rosetta - # crashes with rt_tgsigqueueinfo). arm64 builds under QEMU here — - # slower, hence the larger timeout on this job. - platforms: linux/amd64,linux/arm64 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Download pre-built binaries (per-arch) - if: matrix.image.no_binaries != true uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: kars-binaries-${{ env.VERSION }} path: ./bin/ - name: chmod binaries - if: matrix.image.no_binaries != true run: find ./bin -type f -name 'kars-*' -exec chmod +x {} \; - name: Set up QEMU (for cross-arch buildx) @@ -252,19 +238,60 @@ jobs: run: | cosign sign --yes ${{ env.REGISTRY }}/${{ matrix.image.ghcr }}@${{ steps.push.outputs.digest }} - # ─── Stage 2b: build the openclaw-sandbox agent image ────────── - # openclaw-sandbox is the image the controller actually launches per agent. - # It is built FROM kars-sandbox-base + the inference-router binary + the - # OpenClaw plugin, and is where the patched AGT SDK gets baked in. It must - # build AFTER build-images so the base + router images it references are - # already pushed to GHCR. amd64-only (inherits sandbox-base's wheel arch). + # ─── Stage 2b: build sandbox-base + openclaw-sandbox (NATIVE per-arch) ── + # These two are heavy (Python/Go/Node), so building arm64 under QEMU is far + # too slow. Instead build each arch on its NATIVE GitHub-hosted runner + # (amd64 → ubuntu-22.04, arm64 → ubuntu-24.04-arm; free for public repos), + # push per-arch tags, then merge into a multi-arch manifest. This is what + # makes openclaw-sandbox run NATIVELY on Apple Silicon (amd64 under Rosetta + # crashes with rt_tgsigqueueinfo). The vendored sandbox wheels ship aarch64 + # variants, so the arm64 base installs pre-built wheels (no source compile). + build-sandbox-base: + name: Build kars-sandbox-base (${{ matrix.arch }}, native) + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runner: ubuntu-22.04 + - arch: arm64 + runner: ubuntu-24.04-arm + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3 + - name: Log in to GHCR + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build + push kars-sandbox-base:${{ env.VERSION }}-${{ matrix.arch }} + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v6 + with: + context: . + file: sandbox-images/openclaw/Dockerfile.base + push: true + platforms: linux/${{ matrix.arch }} + tags: ${{ env.REGISTRY }}/kars-sandbox-base:${{ env.VERSION }}-${{ matrix.arch }} + build-args: | + VERSION=${{ env.VERSION }} + build-sandbox: - name: Build + push openclaw-sandbox (PUBLIC GHCR) - needs: build-images - runs-on: ubuntu-22.04 - timeout-minutes: 90 - outputs: - sandbox_digest: ${{ steps.push.outputs.digest }} + name: Build openclaw-sandbox (${{ matrix.arch }}, native) + needs: [build-images, build-sandbox-base] + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runner: ubuntu-22.04 + - arch: arm64 + runner: ubuntu-24.04-arm steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -272,19 +299,15 @@ jobs: run: | # A release sandbox image MUST bundle the kars-patched AGT SDK # (POP signing + X3DH KDF fix). The Dockerfile falls back to the - # UNPATCHED public npm SDK if .agt-sdk/ is absent, which - # would ship a broken-interop mesh. Fail the release instead. + # UNPATCHED public npm SDK if .agt-sdk/ is absent. TARBALL=$(find vendor/agt -maxdepth 1 -name 'microsoft-agent-governance-sdk-*.tgz' | head -1 || true) if [ -z "$TARBALL" ]; then - echo "::error::No vendored patched AGT SDK tarball under vendor/agt/." \ - "Refusing to publish an openclaw-sandbox that would fall back to" \ - "the unpatched public SDK. See vendor/agt/pin.json." + echo "::error::No vendored patched AGT SDK tarball under vendor/agt/." exit 1 fi mkdir -p sandbox-images/openclaw/.agt-sdk cp "$TARBALL" sandbox-images/openclaw/.agt-sdk/ echo "AGT_SDK_TARBALL=$(basename "$TARBALL")" >> "$GITHUB_ENV" - echo "::notice::Bundling patched AGT SDK: $(basename "$TARBALL")" - name: Build mesh-plugin (openclaw Dockerfile COPYs mesh-plugin/dist) uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4 @@ -293,19 +316,12 @@ jobs: - name: npm ci + build mesh-plugin working-directory: mesh-plugin run: | - # The openclaw Dockerfile hard-COPYs mesh-plugin/dist/ (it ships - # @kars/mesh as a file: dep with the prepare script stripped), so - # dist/ must exist in the build context before buildx runs. npm ci --no-audit --no-fund npm run build test -d dist || { echo "::error::mesh-plugin/dist not produced"; exit 1; } - - name: Set up QEMU (for arm64 buildx) - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3 - - name: Log in to GHCR uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v3 with: @@ -313,50 +329,82 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build + push openclaw-sandbox (PUBLIC) - id: push + - name: Build + push openclaw-sandbox:${{ env.VERSION }}-${{ matrix.arch }} uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v6 with: context: . file: sandbox-images/openclaw/Dockerfile push: true - platforms: linux/amd64,linux/arm64 - tags: | - ${{ env.REGISTRY }}/openclaw-sandbox:${{ env.VERSION }} - ${{ env.REGISTRY }}/openclaw-sandbox:latest + platforms: linux/${{ matrix.arch }} + tags: ${{ env.REGISTRY }}/openclaw-sandbox:${{ env.VERSION }}-${{ matrix.arch }} build-args: | VERSION=${{ env.VERSION }} - SANDBOX_BASE_IMAGE=${{ env.REGISTRY }}/kars-sandbox-base:${{ env.VERSION }} + SANDBOX_BASE_IMAGE=${{ env.REGISTRY }}/kars-sandbox-base:${{ env.VERSION }}-${{ matrix.arch }} INFERENCE_ROUTER_IMAGE=${{ env.REGISTRY }}/kars-inference-router:${{ env.VERSION }} AGT_SDK_TARBALL=${{ env.AGT_SDK_TARBALL }} - sbom: true - provenance: true - - name: Attest build provenance (GitHub-signed, pushed to registry) - uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + # Merge the per-arch tags into multi-arch manifests + sign/attest/SBOM. + merge-sandbox: + name: Merge + sign sandbox-base + openclaw-sandbox manifests + needs: [build-sandbox-base, build-sandbox] + runs-on: ubuntu-22.04 + timeout-minutes: 20 + outputs: + sandbox_digest: ${{ steps.digests.outputs.openclaw }} + sandbox_base_digest: ${{ steps.digests.outputs.base }} + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3 + - name: Log in to GHCR + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v3 with: - subject-name: ${{ env.REGISTRY }}/openclaw-sandbox - subject-digest: ${{ steps.push.outputs.digest }} - push-to-registry: true + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Make package public (best-effort; may need one-time manual toggle) + - name: Create multi-arch manifests (:VERSION + :latest) + id: digests + run: | + for name in kars-sandbox-base openclaw-sandbox; do + docker buildx imagetools create \ + -t "${REGISTRY}/${name}:${VERSION}" \ + -t "${REGISTRY}/${name}:latest" \ + "${REGISTRY}/${name}:${VERSION}-amd64" \ + "${REGISTRY}/${name}:${VERSION}-arm64" + DIGEST=$(docker buildx imagetools inspect "${REGISTRY}/${name}:${VERSION}" --format '{{.Manifest.Digest}}') + case "$name" in + kars-sandbox-base) echo "base=${DIGEST}" >> "$GITHUB_OUTPUT" ;; + openclaw-sandbox) echo "openclaw=${DIGEST}" >> "$GITHUB_OUTPUT" ;; + esac + done + env: + REGISTRY: ${{ env.REGISTRY }} + VERSION: ${{ env.VERSION }} + + - name: Make packages public (best-effort) continue-on-error: true env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - gh api -X PATCH \ - -H "Accept: application/vnd.github+json" \ - "/orgs/azure/packages/container/openclaw-sandbox/visibility" \ - -f visibility=public \ - || echo "::warning::Could not set openclaw-sandbox public via API; set it once manually in package settings." + for name in kars-sandbox-base openclaw-sandbox; do + gh api -X PATCH -H "Accept: application/vnd.github+json" \ + "/orgs/azure/packages/container/${name}/visibility" -f visibility=public \ + || echo "::warning::Could not set ${name} public via API." + done - - name: Generate SBOM (SPDX JSON) + - name: Attest build provenance (both manifests) + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-name: ${{ env.REGISTRY }}/openclaw-sandbox + subject-digest: ${{ steps.digests.outputs.openclaw }} + push-to-registry: true + + - name: Generate SBOM (openclaw-sandbox) uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0 with: image: ${{ env.REGISTRY }}/openclaw-sandbox:${{ env.VERSION }} format: spdx-json output-file: sbom-openclaw-sandbox.spdx.json - - name: Upload SBOM uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4 with: @@ -368,12 +416,12 @@ jobs: uses: sigstore/cosign-installer@1aa8e0f2454b781fbf0fbf306a4c9533a0c57409 # v3.7.0 with: cosign-release: 'v2.4.1' - - - name: Cosign keyless sign image + - name: Cosign keyless sign manifests env: COSIGN_EXPERIMENTAL: '1' run: | - cosign sign --yes ${{ env.REGISTRY }}/openclaw-sandbox@${{ steps.push.outputs.digest }} + cosign sign --yes ${{ env.REGISTRY }}/kars-sandbox-base@${{ steps.digests.outputs.base }} + cosign sign --yes ${{ env.REGISTRY }}/openclaw-sandbox@${{ steps.digests.outputs.openclaw }} # ─── Stage 2c: build AGT Python wheels (upstream governance pkgs) ── # The Python runtime images COPY runtimes/wheels/*.whl (upstream AGT @@ -641,7 +689,7 @@ jobs: # ─── Stage 4: assemble + publish the PUBLIC GitHub Release ───── release: name: Create PUBLIC GitHub Release - needs: [build-binaries, build-images, build-sandbox, build-runtimes, build-mesh, cli-pack] + needs: [build-binaries, build-images, build-sandbox-base, build-sandbox, merge-sandbox, build-runtimes, build-mesh, cli-pack] runs-on: ubuntu-22.04 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -696,8 +744,8 @@ jobs: {"name": "kars-inference-router", "ghcr": "${{ env.REGISTRY }}/kars-inference-router:${{ env.VERSION }}", "digest": "${{ needs.build-images.outputs.router_digest }}"}, {"name": "kars-a2a-gateway", "ghcr": "${{ env.REGISTRY }}/kars-a2a-gateway:${{ env.VERSION }}", "digest": "${{ needs.build-images.outputs.gateway_digest }}"}, {"name": "kars-conformance-runner","ghcr": "${{ env.REGISTRY }}/kars-conformance-runner:${{ env.VERSION }}","digest": "${{ needs.build-images.outputs.runner_digest }}"}, - {"name": "kars-sandbox-base", "ghcr": "${{ env.REGISTRY }}/kars-sandbox-base:${{ env.VERSION }}", "digest": "${{ needs.build-images.outputs.sandbox_base_digest }}"}, - {"name": "openclaw-sandbox", "ghcr": "${{ env.REGISTRY }}/openclaw-sandbox:${{ env.VERSION }}", "digest": "${{ needs.build-sandbox.outputs.sandbox_digest }}"} + {"name": "kars-sandbox-base", "ghcr": "${{ env.REGISTRY }}/kars-sandbox-base:${{ env.VERSION }}", "digest": "${{ needs.merge-sandbox.outputs.sandbox_base_digest }}"}, + {"name": "openclaw-sandbox", "ghcr": "${{ env.REGISTRY }}/openclaw-sandbox:${{ env.VERSION }}", "digest": "${{ needs.merge-sandbox.outputs.sandbox_digest }}"} ], "cli_install": "npm i -g https://github.com/${{ github.repository }}/releases/download/${{ env.VERSION }}/${{ steps.cli.outputs.tarball }}", "next_step_for_signed_release": "Migrate to the ESRP-signed public path (.github/pipelines/esrp-publish.yml → MCR + npmjs @kars + crates.io) once onboarding completes. See docs/PUBLISHING.md." From c8bf0da3bf5d7badfb9bbcff781953df6dc21086 Mon Sep 17 00:00:00 2001 From: Pal Lakatos-Toth Date: Tue, 23 Jun 2026 09:01:19 +0200 Subject: [PATCH 16/21] fix(cli): kars dev --release pulls the HOST arch sandbox image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit openclaw-sandbox is now published multi-arch (amd64 + arm64), but --release hardcoded `--platform linux/amd64`, so Apple Silicon pulled the amd64 variant and crashed under Rosetta (rt_tgsigqueueinfo). Pull dockerPlatform (the host arch) instead — arm64 Macs now get the native arm64 image and run it without emulation. Verified: container Up (healthy), gateway HTTP 200 on an M-series Mac. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/src/commands/dev.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cli/src/commands/dev.ts b/cli/src/commands/dev.ts index a24b296b..5306ff09 100644 --- a/cli/src/commands/dev.ts +++ b/cli/src/commands/dev.ts @@ -833,9 +833,11 @@ Notes: if (releaseMode) { stepper.update(`Pulling published sandbox image (${image})...`); try { - // openclaw-sandbox is published amd64-only; on Apple Silicon it - // runs under emulation (Docker Desktop handles this transparently). - await execa("docker", ["pull", "--platform", "linux/amd64", image], { stdio: "inherit" }); + // openclaw-sandbox is published multi-arch (amd64 + arm64); pull + // the HOST architecture so Apple Silicon gets the native arm64 + // image (the amd64 image crashes under Rosetta with + // rt_tgsigqueueinfo). dockerPlatform = linux/. + await execa("docker", ["pull", "--platform", dockerPlatform, image], { stdio: "inherit" }); imageExists = true; } catch { stepper.fail("Could not pull published sandbox image"); From 0e4be76c3ec26687b1e85877a696c22d45198680 Mon Sep 17 00:00:00 2001 From: Pal Lakatos-Toth Date: Tue, 23 Jun 2026 09:35:39 +0200 Subject: [PATCH 17/21] fix(release): bundle patched POP-aware AGT SDK in published sandbox image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The published openclaw-sandbox shipped the UNPATCHED npm SDK (3.7.0, no proof-of-possession), so the agent's mesh client sent bare connect frames. The relay rejected them ("missing did/public_key/timestamp/signature") and the sandbox entered a reconnect flood — sub-agent collaboration never came up. Root cause: the Dockerfile's `COPY .agt-sdk/` resolves against the build CONTEXT root (the repo root — `context: .`), but the release workflow staged the patched SDK tarball into `sandbox-images/openclaw/.agt-sdk/`. The COPY never saw it, so the Dockerfile silently fell back to the public npm SDK. The CLI from-source build always staged into `/.agt-sdk/` (cli/src/commands/dev.ts), which is why dev mode worked but `--release` didn't. Fixes: - release-public-interim.yml: stage the tarball into /.agt-sdk/ (matching the Dockerfile COPY context) and pass MESH_PROVIDER=agt. - openclaw/Dockerfile: when AGT_SDK_TARBALL is set but the file is absent, FAIL the build loudly instead of silently installing the unpatched npm SDK. This makes the staging-path class of bug impossible to ship again. - e2e-harness/platforms/docker.sh: `kars dev --name X` creates container `kars-X`; use the prefixed name for docker exec/logs/ps. Verified locally: rebuilt openclaw-sandbox with the corrected staging → image now bundles @microsoft/agent-governance-sdk@4.0.0 with POP signing (proof_timestamp present in registry-client.js + x3dh.js). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/release-public-interim.yml | 11 +++++++++-- sandbox-images/openclaw/Dockerfile | 10 +++++++++- tools/e2e-harness/platforms/docker.sh | 8 ++++++-- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release-public-interim.yml b/.github/workflows/release-public-interim.yml index 2aa029ea..b8469604 100644 --- a/.github/workflows/release-public-interim.yml +++ b/.github/workflows/release-public-interim.yml @@ -300,13 +300,19 @@ jobs: # A release sandbox image MUST bundle the kars-patched AGT SDK # (POP signing + X3DH KDF fix). The Dockerfile falls back to the # UNPATCHED public npm SDK if .agt-sdk/ is absent. + # + # The Dockerfile's `COPY .agt-sdk/` resolves against the BUILD + # CONTEXT root (the repo root — `context: .` below), NOT the + # Dockerfile's directory. Stage into /.agt-sdk/ to match, + # exactly like the CLI from-source dev build does + # (cli/src/commands/dev.ts -> path.join(repoRoot, ".agt-sdk")). TARBALL=$(find vendor/agt -maxdepth 1 -name 'microsoft-agent-governance-sdk-*.tgz' | head -1 || true) if [ -z "$TARBALL" ]; then echo "::error::No vendored patched AGT SDK tarball under vendor/agt/." exit 1 fi - mkdir -p sandbox-images/openclaw/.agt-sdk - cp "$TARBALL" sandbox-images/openclaw/.agt-sdk/ + mkdir -p .agt-sdk + cp "$TARBALL" .agt-sdk/ echo "AGT_SDK_TARBALL=$(basename "$TARBALL")" >> "$GITHUB_ENV" - name: Build mesh-plugin (openclaw Dockerfile COPYs mesh-plugin/dist) @@ -341,6 +347,7 @@ jobs: VERSION=${{ env.VERSION }} SANDBOX_BASE_IMAGE=${{ env.REGISTRY }}/kars-sandbox-base:${{ env.VERSION }}-${{ matrix.arch }} INFERENCE_ROUTER_IMAGE=${{ env.REGISTRY }}/kars-inference-router:${{ env.VERSION }} + MESH_PROVIDER=agt AGT_SDK_TARBALL=${{ env.AGT_SDK_TARBALL }} # Merge the per-arch tags into multi-arch manifests + sign/attest/SBOM. diff --git a/sandbox-images/openclaw/Dockerfile b/sandbox-images/openclaw/Dockerfile index 36fa1ab4..9a78b039 100644 --- a/sandbox-images/openclaw/Dockerfile +++ b/sandbox-images/openclaw/Dockerfile @@ -83,7 +83,15 @@ ARG MESH_PROVIDER=vendored ARG AGT_SDK_TARBALL= COPY .agt-sdk/ /opt/kars-agt-sdk/ RUN cd /opt/kars-plugin && \ - if [ -n "$AGT_SDK_TARBALL" ] && [ -f "/opt/kars-agt-sdk/$AGT_SDK_TARBALL" ]; then \ + if [ -n "$AGT_SDK_TARBALL" ]; then \ + if [ ! -f "/opt/kars-agt-sdk/$AGT_SDK_TARBALL" ]; then \ + echo "::error:: AGT_SDK_TARBALL=$AGT_SDK_TARBALL was requested but" >&2; \ + echo " /opt/kars-agt-sdk/$AGT_SDK_TARBALL is missing from the build context." >&2; \ + echo " The patched POP-aware SDK would NOT be installed and the mesh" >&2; \ + echo " would silently fall back to the unpatched npm SDK (relay flood)." >&2; \ + echo " Stage the tarball into /.agt-sdk/ before building." >&2; \ + exit 1; \ + fi; \ echo "Installing AGT SDK from local tarball: $AGT_SDK_TARBALL" && \ npm install --omit=dev --ignore-scripts --no-save \ "/opt/kars-agt-sdk/$AGT_SDK_TARBALL" ; \ diff --git a/tools/e2e-harness/platforms/docker.sh b/tools/e2e-harness/platforms/docker.sh index 7f9f28e4..183016b4 100644 --- a/tools/e2e-harness/platforms/docker.sh +++ b/tools/e2e-harness/platforms/docker.sh @@ -40,7 +40,11 @@ set -euo pipefail -DOCKER_CONTAINER_NAME="${DOCKER_CONTAINER_NAME:-${SCENARIO_SANDBOX}}" +# `kars dev --name X` creates a docker container named `kars-X`. Keep the +# bare name for the `kars dev --name` flag, but use the `kars-`-prefixed name +# for every `docker exec` / `docker logs` / `docker ps` operation below. +KARS_DEV_NAME="${DOCKER_CONTAINER_NAME:-${SCENARIO_SANDBOX}}" +DOCKER_CONTAINER_NAME="kars-${KARS_DEV_NAME}" platform_preflight() { command -v docker >/dev/null || { log "ERR docker not on PATH"; exit 1; } @@ -68,7 +72,7 @@ platform_preflight() { log "released mode: kars dev --release ${KARS_RELEASE} (published images)" fi kars dev --target docker \ - --name "${DOCKER_CONTAINER_NAME}" \ + --name "${KARS_DEV_NAME}" \ "${release_args[@]}" \ >>"${OUT_DIR}/dev-bringup.log" 2>&1 || { log "ERR kars dev bring-up failed; tail of dev-bringup.log:" From 3fb70e1801ac0750fdfccb0c84b5e9e5282b065b Mon Sep 17 00:00:00 2001 From: Pal Lakatos-Toth Date: Tue, 23 Jun 2026 09:54:32 +0200 Subject: [PATCH 18/21] fix(e2e): verify sibling mesh pairs from collected gateway logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The exec-brief `check_relay_pairs` resolved sibling DIDs via `kubectl get pods` / `kubectl logs`, which returns nothing in docker mode (no K8s pods/namespaces). The check therefore always fell to the "0/3 sub-agents had router chat activity" fallback and FAILED on docker even when the mesh worked perfectly. The harness already collects each agent's gateway log into OUT_DIR on every platform (`-gateway.log`). Those logs authoritatively record outbound sibling sends: AGT sub-agent mesh_transfer_file: to=writer file=/sandbox/... AGT relay: sent to viz (did:mesh:...) via E2E encrypted relay Parse those instead — the log's owner is the sender, the parsed peer the target — so the check is correct on docker, kind, and AKS alike. The legacy kubectl/registry heuristic is kept as a fallback when no gateway logs were collected. Verified: exec-brief now PASSES 3/3 sibling pairs (analyst↔viz, analyst↔ writer, viz↔writer) and 9/9 overall on docker with the patched POP-aware SDK. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../scenarios/exec-brief/checks.py | 49 ++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/tools/e2e-harness/scenarios/exec-brief/checks.py b/tools/e2e-harness/scenarios/exec-brief/checks.py index d2dc429a..f9310673 100644 --- a/tools/e2e-harness/scenarios/exec-brief/checks.py +++ b/tools/e2e-harness/scenarios/exec-brief/checks.py @@ -123,15 +123,50 @@ def check_chart(ctx: "Context") -> tuple[bool, str]: def check_relay_pairs(ctx: "Context") -> tuple[bool, str]: - # Encrypted blobs flow over a persistent /ws connection and the OpenClaw - # plugin's `log.info("AGT relay: sent to X")` does NOT reach kubectl logs - # (it goes to the in-pod gateway log file). So we fall back to live - # querying each sub-agent's inference-router for chat_completions activity - # — if all three subs processed at least one chat turn, mesh delivery is - # confirmed for the parent→sib direction. For sib→sib we count the unique - # *non-self* peer DIDs each sub-agent's plugin looked up in the registry. + # Sibling-to-sibling mesh routing is proven by the per-agent gateway logs + # the harness collects into OUT_DIR on EVERY platform (docker exec / kubectl + # logs alike): `-gateway.log`. Each sub-agent records its own outbound + # mesh sends there, e.g.: + # AGT sub-agent mesh_transfer_file: to=writer file=/sandbox/... + # AGT relay: sent to viz (did:mesh:...) via E2E encrypted relay + # The log's owner is the sender; the parsed peer is the target. A pair of + # two distinct siblings is a sibling pair. This is the authoritative, + # platform-agnostic signal — unlike the legacy kubectl/registry heuristic + # (kept below as a fallback) it works in docker mode too, where there are + # no K8s pods/namespaces for `kubectl` to inspect. siblings = ["analyst", "viz", "writer"] pairs: set[frozenset[str]] = set() + + send_pat = re.compile( + r"mesh_transfer_file:\s+(?:[^\n]*?\bto=|[^\n]*?\u2192\s+)(\w+)\b" + r"|AGT relay:\s+sent to (\w+)\b" + ) + saw_gateway_log = False + for sub in siblings: + log_path = ctx.out_dir / f"{sub}-gateway.log" + if not log_path.exists(): + continue + saw_gateway_log = True + try: + text = log_path.read_text(errors="ignore") + except Exception: + continue + for m in send_pat.finditer(text): + target = m.group(1) or m.group(2) + if target in siblings and target != sub: + pairs.add(frozenset((sub, target))) + + if saw_gateway_log: + expected = {frozenset(("analyst", "viz")), + frozenset(("analyst", "writer")), + frozenset(("viz", "writer"))} + missing = expected - pairs + ok = not missing + return ok, (f"{len(pairs)}/3 sibling pairs from gateway logs " + f"(missing={sorted(map(sorted, missing))})") + + # ── Fallback: legacy kubectl/registry heuristic (AKS live, no collected + # gateway logs). Only reached when no `-gateway.log` was collected. ── # 1. Discover each sub-agent's pod IP so we can attribute REGISTRY lines. pod_ip: dict[str, str] = {} for sub in siblings: From 21c2c69742078109d018ff41f093756e9cb93cd6 Mon Sep 17 00:00:00 2001 From: Pal Lakatos-Toth Date: Tue, 23 Jun 2026 10:06:00 +0200 Subject: [PATCH 19/21] fix(operator): key per-sandbox state by unique origin, not bare name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The operator's `securityStates` and `egressByAgent` maps were keyed by the bare sandbox `name`. Agent names are NOT unique across runtimes/clusters — the same name (e.g. "analyst") can exist at once as a docker container, a kind pod, and an AKS pod. With overlapping agents present, a stale/empty same-named entry overwrote the live one, so the operator showed no DID and no audit entries for the shadowed agent (observed: the docker "analyst", whose router actually reported did:agentmesh:analyst + 157 audit entries, rendered blank while viz/writer — with fewer duplicates — rendered fine). Add `sandboxKey(sb)` = `${kubeContext ?? runtime}::${namespace}::${name}` and use it at every set/get site in operator.ts and the security/topology render paths. Same-named agents across docker/kind/aks now get distinct keys, so each renders its own DID, trust scores, and audit log. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/src/commands/operator.ts | 12 ++--- cli/src/commands/operator/helpers.test.ts | 51 ++++++++++++++++++++ cli/src/commands/operator/helpers.ts | 21 ++++++++ cli/src/commands/operator/render/security.ts | 7 +-- cli/src/commands/operator/render/topology.ts | 10 ++-- 5 files changed, 87 insertions(+), 14 deletions(-) create mode 100644 cli/src/commands/operator/helpers.test.ts diff --git a/cli/src/commands/operator.ts b/cli/src/commands/operator.ts index 37b79b04..f0a649dc 100644 --- a/cli/src/commands/operator.ts +++ b/cli/src/commands/operator.ts @@ -27,7 +27,7 @@ import type { ClusterHealth, MeshHealth, } from "./operator/types.js"; -import { timeSince, kctl, platformTag, clusterOriginTag } from "./operator/helpers.js"; +import { timeSince, kctl, platformTag, clusterOriginTag, sandboxKey } from "./operator/helpers.js"; import { fetchSandboxes } from "./operator/fetchers/sandboxes.js"; import { fetchEgressDomains, @@ -310,7 +310,7 @@ async function startDashboard(refreshInterval: number, kubeContext?: string, dev const idx = (agentTable as any).rows?.selected ?? 0; const sb = sandboxes[idx]; if (!sb) return []; - return egressByAgent.get(sb.name) || []; + return egressByAgent.get(sandboxKey(sb)) || []; } /** Total egress domain count across all agents. */ @@ -547,21 +547,21 @@ async function startDashboard(refreshInterval: number, kubeContext?: string, dev egressByAgent = new Map(); for (let i = 0; i < running.length; i++) { const r = settled[i]; - if (r.status === "fulfilled") egressByAgent.set(running[i].name, r.value); + if (r.status === "fulfilled") egressByAgent.set(sandboxKey(running[i]), r.value); } }), Promise.allSettled(running.map((s) => fetchSecurityState(s, s.kubeContext ?? kubeContext))).then((settled) => { securityStates = new Map(); for (let i = 0; i < running.length; i++) { const r = settled[i]; - if (r.status === "fulfilled") securityStates.set(r.value.sandbox, r.value); + if (r.status === "fulfilled") securityStates.set(sandboxKey(running[i]), r.value); } }), ); } else { // Fast AGT-only poll on non-detail cycles to keep mesh data alive promises.push( - Promise.allSettled(running.map((s) => fetchAgtQuick(s, securityStates.get(s.name), s.kubeContext ?? kubeContext))), + Promise.allSettled(running.map((s) => fetchAgtQuick(s, securityStates.get(sandboxKey(s)), s.kubeContext ?? kubeContext))), ); } @@ -720,7 +720,7 @@ async function startDashboard(refreshInterval: number, kubeContext?: string, dev const idx = (agentTable as any).rows?.selected ?? 0; const sb = sandboxes[idx]; if (!sb) return; - const sec = securityStates.get(sb.name); + const sec = securityStates.get(sandboxKey(sb)); const mode = sec?.egressMode; if (mode === "learning") { await enforceEgress(sb); diff --git a/cli/src/commands/operator/helpers.test.ts b/cli/src/commands/operator/helpers.test.ts new file mode 100644 index 00000000..ec5143a5 --- /dev/null +++ b/cli/src/commands/operator/helpers.test.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, expect, it } from "vitest"; +import { sandboxKey } from "./helpers.js"; + +describe("sandboxKey", () => { + // Regression: the operator's per-sandbox maps (securityStates, + // egressByAgent) were keyed by bare `name`. The same agent name can exist + // simultaneously as a docker container, a kind pod, and an AKS pod — so a + // stale/empty same-named entry overwrote the live one and the operator + // showed no DID / no audit for the shadowed agent (observed for "analyst" + // when docker + kind + aks all had an "analyst"). The key must disambiguate + // by origin (kubeContext for K8s, runtime for docker) + namespace. + it("disambiguates same-named agents across runtimes and clusters", () => { + const docker = sandboxKey({ + name: "analyst", + namespace: "kars-analyst", + runtime: "docker", + }); + const kind = sandboxKey({ + name: "analyst", + namespace: "kars-analyst", + runtime: "aks", + kubeContext: "kind-kars-dev", + }); + const aks = sandboxKey({ + name: "analyst", + namespace: "kars-analyst", + runtime: "aks", + kubeContext: "kars-aks", + }); + + const keys = new Set([docker, kind, aks]); + expect(keys.size).toBe(3); + expect(docker).not.toBe(kind); + expect(kind).not.toBe(aks); + expect(docker).not.toBe(aks); + }); + + it("is stable for the same sandbox identity", () => { + const a = sandboxKey({ name: "viz", namespace: "kars-viz", runtime: "docker" }); + const b = sandboxKey({ name: "viz", namespace: "kars-viz", runtime: "docker" }); + expect(a).toBe(b); + }); + + it("falls back to runtime then 'local' when kubeContext is absent", () => { + expect(sandboxKey({ name: "x", namespace: "kars-x", runtime: "docker" })).toContain("docker"); + expect(sandboxKey({ name: "x", namespace: "kars-x" })).toContain("local"); + }); +}); diff --git a/cli/src/commands/operator/helpers.ts b/cli/src/commands/operator/helpers.ts index 61dad224..58bccafb 100644 --- a/cli/src/commands/operator/helpers.ts +++ b/cli/src/commands/operator/helpers.ts @@ -117,6 +117,27 @@ export function platformLabel(s: { return "cloud"; } +/** + * Stable, globally-unique key for a sandbox across runtimes and clusters. + * + * Sandbox NAMES are not unique: the same agent name (e.g. "analyst") can + * exist simultaneously as a docker container, a kind-cluster pod, and an + * AKS pod. Keying per-sandbox maps (securityStates, egressByAgent) by bare + * `name` causes these to collide — a stale/empty same-named entry overwrites + * the live one, so the operator shows no DID / no audit for the shadowed + * agent. Compose the origin (kubeContext for K8s, runtime for docker) with + * the namespace and name to disambiguate. + */ +export function sandboxKey(s: { + name: string; + namespace: string; + runtime?: "docker" | "aks"; + kubeContext?: string; +}): string { + const origin = s.kubeContext ?? s.runtime ?? "local"; + return `${origin}::${s.namespace}::${s.name}`; +} + /** * Compact " " used in the agent-table Cluster column * and the CRD snapshot status line. Docker sandboxes show just "D" diff --git a/cli/src/commands/operator/render/security.ts b/cli/src/commands/operator/render/security.ts index c7f03d56..a942230d 100644 --- a/cli/src/commands/operator/render/security.ts +++ b/cli/src/commands/operator/render/security.ts @@ -8,6 +8,7 @@ // an explicit context object. import type { SandboxInfo, SecurityState } from "../types.js"; +import { sandboxKey } from "../helpers.js"; interface BlessedBox { setContent(content: string): void; @@ -36,7 +37,7 @@ export function renderSecurity(ctx: SecurityRenderContext): void { return; } - const sec = securityStates.get(sb.name); + const sec = securityStates.get(sandboxKey(sb)); if (!sec) { securityBox.setContent(`{bold}${sb.name}{/}\n\n{gray-fg}Polling...{/}`); return; @@ -129,7 +130,7 @@ export function renderAGTFull( sandboxes: SandboxInfo[], securityStates: Map, ): string { - const sec = securityStates.get(sb.name); + const sec = securityStates.get(sandboxKey(sb)); if (!sec) return `{bold}${sb.name}{/}\n{gray-fg}Polling...{/}`; if (!sec.agtEnabled) return "{gray-fg}AGT not enabled{/}\n{gray-fg}Use --governance flag{/}"; @@ -280,7 +281,7 @@ export function renderAGT(ctx: SecurityRenderContext): void { return; } - const sec = securityStates.get(sb.name); + const sec = securityStates.get(sandboxKey(sb)); if (!sec) { agtPanel.setContent(`{bold}${sb.name}{/}\n{gray-fg}Polling...{/}`); return; diff --git a/cli/src/commands/operator/render/topology.ts b/cli/src/commands/operator/render/topology.ts index 39e3dd59..af6d86f1 100644 --- a/cli/src/commands/operator/render/topology.ts +++ b/cli/src/commands/operator/render/topology.ts @@ -7,7 +7,7 @@ // `securityStates`, and `topologyBox` become an explicit context object. import type { SandboxInfo, SecurityState } from "../types.js"; -import { platformTag } from "../helpers.js"; +import { platformTag, sandboxKey } from "../helpers.js"; interface BlessedBox { setContent(content: string): void; @@ -106,7 +106,7 @@ export function renderTopology(ctx: TopologyRenderContext): void { } for (const p of parents) { - const sec = securityStates.get(p.name); + const sec = securityStates.get(sandboxKey(p)); const icon = statusIcon(p.health); const mode = sec?.egressMode === "enforcing" ? "{green-fg}enforce{/}" : sec?.egressMode === "learning" ? "{yellow-fg}learn{/}" : ""; @@ -130,7 +130,7 @@ export function renderTopology(ctx: TopologyRenderContext): void { if (subs.length === 1) { // Single child — straight line down lines.push(" ".repeat(parentCenter) + "│"); - const childSec = securityStates.get(subs[0].name); + const childSec = securityStates.get(sandboxKey(subs[0])); const ci = statusIcon(subs[0].health); const cMesh = childSec ? `↑${childSec.agtMeshSent} ↓${childSec.agtMeshReceived}` : ""; const cBox = makeBox(subs[0].name, ci, `${platformTag(subs[0])} ${subs[0].model}`, cMesh); @@ -175,7 +175,7 @@ export function renderTopology(ctx: TopologyRenderContext): void { // Render child boxes side-by-side const childBoxes: string[][] = []; for (const s of subs) { - const childSec = securityStates.get(s.name); + const childSec = securityStates.get(sandboxKey(s)); const ci = statusIcon(s.health); const cMesh = childSec ? `↑${childSec.agtMeshSent} ↓${childSec.agtMeshReceived}` : ""; childBoxes.push(makeBox(s.name, ci, `${platformTag(s)} ${s.model}`, cMesh)); @@ -193,7 +193,7 @@ export function renderTopology(ctx: TopologyRenderContext): void { // Peer-to-peer mesh links const peerLinks: string[] = []; for (const s of subs) { - const childSec = securityStates.get(s.name); + const childSec = securityStates.get(sandboxKey(s)); const peers = childSec?.agtTrustScores.filter((t) => { if (isBogusLegacyKey(t.agent)) return false; const a = shortAgentName(t.agent); From 7a52a815e0e9ef5d895fb6dab1f168e0303d2c25 Mon Sep 17 00:00:00 2001 From: Pal Lakatos-Toth Date: Tue, 23 Jun 2026 10:19:01 +0200 Subject: [PATCH 20/21] docs: published --release path works on Apple Silicon (multi-arch arm64) The sandbox images are now multi-arch (amd64 + arm64, built on native arm64 runners) and `kars dev --release` pulls the host-arch variant automatically. Remove the obsolete "linux/amd64 only / crashes under Rosetta" caveat and the "build from source on Apple Silicon" workaround; bump the quick-start tag to v0.1.0-interim.9 (the first release with the patched POP-aware mesh SDK). Verified end-to-end on an M-series Mac: the full multi-agent exec-brief scenario passes 9/9 via `--release v0.1.0-interim.9`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 23 +++++++++++------------ docs/getting-started.md | 26 +++++++++++++------------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 73815233..c72281ec 100644 --- a/README.md +++ b/README.md @@ -132,31 +132,30 @@ Same CRDs. Same router code path. Same audit format. Same governance profiles. T ## Try it in five minutes -**Fastest path — published images, no compile (linux/amd64 hosts).** On an -amd64 Linux box (or any amd64 host with Docker), two commands get you a -running agent — no Rust toolchain, no AGT checkout, no local image build: +**Fastest path — published images, no compile (macOS & Linux, Intel & Apple Silicon).** +On any host with Docker (amd64 Linux, Intel Mac, or Apple Silicon M-series), +two commands get you a running agent — no Rust toolchain, no AGT checkout, no +local image build: ```bash # 1. Install the CLI from the latest interim release -npm i -g https://github.com/Azure/kars/releases/download/v0.1.0-interim.6/kars-cli-0.1.0.tgz +npm i -g https://github.com/Azure/kars/releases/download/v0.1.0-interim.9/kars-cli-0.1.0.tgz # 2. Launch a sandbox from the published, cosign-signed images -kars dev --release v0.1.0-interim.6 +kars dev --release v0.1.0-interim.9 ``` > Use the newest tag from the [Releases page](https://github.com/Azure/kars/releases) — the `npm i -g …` URL and the `--release` flag must match. -> **⚠️ Apple Silicon (M-series) Macs:** the published sandbox image is -> currently `linux/amd64` only, and it **crashes under Rosetta** on arm64 -> (`rosetta error: rt_tgsigqueueinfo failed`). On an Apple Silicon Mac, use -> the **build-from-source** path below instead — `kars dev` builds the -> sandbox natively for your arch. (A native `arm64` published image is -> planned; until then, `--release` is for amd64/Linux hosts.) +> **Apple Silicon (M-series) Macs:** fully supported. The published images are +> multi-arch (`linux/amd64` + `linux/arm64`), built on native arm64 runners, +> and `kars dev --release` automatically pulls the image matching your host +> architecture — no Rosetta, no extra flags. On first launch you'll pick an inference provider (see below). **GitHub Copilot** is the easiest — one device-code login, no Azure account.
-Build from source (Apple Silicon Macs, or to hack on the controller / router / plugin) +Build from source (to hack on the controller / router / plugin) > **Prerequisites:** Docker Desktop · Node.js 22+ · Rust 1.88+ · one of: an active GitHub Copilot seat, an Azure AI Foundry deployment, or a GitHub PAT with `models:read`. diff --git a/docs/getting-started.md b/docs/getting-started.md index e979a251..6ed3dc92 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -103,21 +103,21 @@ If you'd rather skip provisioning by hand, jump to **[Step 2 — Deploy to AKS]( ## Step 1 — Local (five minutes) -### 1.0 Fastest path — no compile (published images, amd64/Linux) ⭐ +### 1.0 Fastest path — no compile (published images, macOS & Linux) ⭐ -The quickest way to a running agent **on an amd64 host (Linux, or amd64 -Docker)**: install the CLI from a published release and launch from -**pre-built, cosign-signed images**. No Rust toolchain, no AGT checkout, no -waiting on a local image build. +The quickest way to a running agent **on any host with Docker — amd64 Linux, +Intel Mac, or Apple Silicon (M-series)**: install the CLI from a published +release and launch from **pre-built, cosign-signed images**. No Rust toolchain, +no AGT checkout, no waiting on a local image build. **You need only:** Docker (running) · Node.js 22+. ```bash # 1. Install the kars CLI from the latest interim release (one command) -npm i -g https://github.com/Azure/kars/releases/download/v0.1.0-interim.6/kars-cli-0.1.0.tgz +npm i -g https://github.com/Azure/kars/releases/download/v0.1.0-interim.9/kars-cli-0.1.0.tgz # 2. Launch a sandbox from the published images for that same release -kars dev --release v0.1.0-interim.6 +kars dev --release v0.1.0-interim.9 ``` That's it. `--release` pulls the `openclaw-sandbox` agent image plus the AGT @@ -127,12 +127,12 @@ launches near-instantly. On first launch you'll pick an inference provider — see the [provider picker](#12-launch-a-sandbox) below (**GitHub Copilot** is the easiest: one device-code login, no Azure account). -> **⚠️ Apple Silicon (M-series) Macs:** the published `openclaw-sandbox` is -> currently `linux/amd64` only and **crashes under Rosetta** on arm64 -> (`rosetta error: rt_tgsigqueueinfo failed`). On Apple Silicon use the -> **[build-from-source path](#11-build-the-cli)** — `kars dev` builds the -> sandbox natively for arm64. A native arm64 published image is planned; -> until then `--release` targets amd64/Linux hosts. +> **Apple Silicon (M-series) Macs:** fully supported. The published images are +> multi-arch (`linux/amd64` + `linux/arm64`, built on native arm64 runners) and +> `kars dev --release` automatically pulls the variant matching your host +> architecture — no Rosetta, no extra flags. Verified end-to-end on arm64: +> the full multi-agent exec-brief scenario (parent + 3 mesh sub-agents, +> E2E-encrypted relay) passes on a stock M-series Mac. > **Use matching versions.** The `npm i -g …` tarball URL and the `--release` > flag must reference the **same** tag. Browse the From 69cb638c1a6e65e60ab2d2ccc16064f58803e26e Mon Sep 17 00:00:00 2001 From: Pal Lakatos-Toth Date: Tue, 23 Jun 2026 10:45:24 +0200 Subject: [PATCH 21/21] feat(release): kind --release support + multi-arch AGT relay/registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the published-images path to `--target local-k8s` (kind) and makes the AGT relay/registry multi-arch so they run on arm64 kind nodes. Workflow: - build-mesh now builds agentmesh-relay/registry on NATIVE per-arch runners (amd64→ubuntu-22.04, arm64→ubuntu-24.04-arm), pushing :VERSION-. - new merge-mesh job fuses them into multi-arch manifests + attest + SBOM + cosign + make-public (mirrors merge-sandbox). release job needs it. Previously amd64-only → "exec format error" on an Apple Silicon kind node. CLI: - `kars dev --release --target local-k8s` now works: runLocalK8s pulls the published controller/router/sandbox + relay/registry, tags them as the local dev aliases the existing kind-load + chart plumbing expects, and skips every from-source build. local-k8s still needs the repo (Helm chart + agentmesh manifest); only image *building* is skipped. - deployAgentMesh pulls published relay/registry in release mode instead of building from an AGT checkout. Harness: - platforms/local-k8s.sh honors KARS_RELEASE= to drive the released path on kind (mirrors platforms/docker.sh). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/release-public-interim.yml | 79 +++++++--- cli/src/commands/dev.ts | 8 +- cli/src/commands/dev/local-k8s.ts | 152 ++++++++++++++----- tools/e2e-harness/platforms/local-k8s.sh | 13 +- 4 files changed, 184 insertions(+), 68 deletions(-) diff --git a/.github/workflows/release-public-interim.yml b/.github/workflows/release-public-interim.yml index b8469604..09813285 100644 --- a/.github/workflows/release-public-interim.yml +++ b/.github/workflows/release-public-interim.yml @@ -577,14 +577,25 @@ jobs: # kars dev / up run the AGT relay + registry for the mesh. They are built # from the pinned AGT checkout (agent-mesh/docker/Dockerfile, COMPONENT=*). # Publishing them lets `kars dev --release` pull everything — no AGT clone. + # AGT relay + registry — built on NATIVE per-arch runners so they run + # natively on arm64 kind nodes (Apple Silicon). The AGT Python image is + # multi-arch-capable; amd64-only would ImagePullBackOff / "exec format + # error" on an arm64 kind cluster, blocking `kars dev --release + # --target local-k8s` on a Mac. merge-mesh fuses the per-arch tags below. build-mesh: - name: Build + push agentmesh-${{ matrix.component }} (PUBLIC GHCR) - runs-on: ubuntu-22.04 - timeout-minutes: 30 + name: Build agentmesh-${{ matrix.component }} (${{ matrix.arch }}, native) + runs-on: ${{ matrix.runner }} + timeout-minutes: 40 strategy: fail-fast: false matrix: component: [relay, registry] + arch: [amd64, arm64] + include: + - arch: amd64 + runner: ubuntu-22.04 + - arch: arm64 + runner: ubuntu-24.04-arm steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -605,28 +616,51 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build + push agentmesh-${{ matrix.component }} (PUBLIC) - id: push + - name: Build + push agentmesh-${{ matrix.component }}:${{ env.VERSION }}-${{ matrix.arch }} uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v6 with: context: /tmp/agt file: /tmp/agt/agent-governance-python/agent-mesh/docker/Dockerfile push: true - platforms: linux/amd64 - tags: | - ${{ env.REGISTRY }}/kars-agentmesh-${{ matrix.component }}:${{ env.VERSION }} - ${{ env.REGISTRY }}/kars-agentmesh-${{ matrix.component }}:latest + platforms: linux/${{ matrix.arch }} + tags: ${{ env.REGISTRY }}/kars-agentmesh-${{ matrix.component }}:${{ env.VERSION }}-${{ matrix.arch }} build-args: | COMPONENT=${{ matrix.component }} - sbom: true - provenance: true - - name: Attest build provenance (GitHub-signed, pushed to registry) - uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + # Merge the per-arch relay/registry tags into multi-arch manifests + sign. + merge-mesh: + name: Merge + sign agentmesh manifests + needs: [build-mesh] + runs-on: ubuntu-22.04 + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + component: [relay, registry] + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3 + - name: Log in to GHCR + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v3 with: - subject-name: ${{ env.REGISTRY }}/kars-agentmesh-${{ matrix.component }} - subject-digest: ${{ steps.push.outputs.digest }} - push-to-registry: true + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create multi-arch manifest (:VERSION + :latest) + id: manifest + run: | + NAME="kars-agentmesh-${{ matrix.component }}" + docker buildx imagetools create \ + -t "${REGISTRY}/${NAME}:${VERSION}" \ + -t "${REGISTRY}/${NAME}:latest" \ + "${REGISTRY}/${NAME}:${VERSION}-amd64" \ + "${REGISTRY}/${NAME}:${VERSION}-arm64" + DIGEST=$(docker buildx imagetools inspect "${REGISTRY}/${NAME}:${VERSION}" --format '{{.Manifest.Digest}}') + echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" + env: + REGISTRY: ${{ env.REGISTRY }} + VERSION: ${{ env.VERSION }} - name: Make package public (best-effort; may need one-time manual toggle) continue-on-error: true @@ -639,6 +673,13 @@ jobs: -f visibility=public \ || echo "::warning::Could not set kars-agentmesh-${{ matrix.component }} public via API; set it once manually." + - name: Attest build provenance (GitHub-signed, pushed to registry) + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-name: ${{ env.REGISTRY }}/kars-agentmesh-${{ matrix.component }} + subject-digest: ${{ steps.manifest.outputs.digest }} + push-to-registry: true + - name: Generate SBOM (SPDX JSON) uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0 with: @@ -658,11 +699,11 @@ jobs: with: cosign-release: 'v2.4.1' - - name: Cosign keyless sign image + - name: Cosign keyless sign manifest env: COSIGN_EXPERIMENTAL: '1' run: | - cosign sign --yes ${{ env.REGISTRY }}/kars-agentmesh-${{ matrix.component }}@${{ steps.push.outputs.digest }} + cosign sign --yes ${{ env.REGISTRY }}/kars-agentmesh-${{ matrix.component }}@${{ steps.manifest.outputs.digest }} # ─── Stage 3: pack @kars/cli for GitHub Release distribution ─── cli-pack: @@ -696,7 +737,7 @@ jobs: # ─── Stage 4: assemble + publish the PUBLIC GitHub Release ───── release: name: Create PUBLIC GitHub Release - needs: [build-binaries, build-images, build-sandbox-base, build-sandbox, merge-sandbox, build-runtimes, build-mesh, cli-pack] + needs: [build-binaries, build-images, build-sandbox-base, build-sandbox, merge-sandbox, build-runtimes, build-mesh, merge-mesh, cli-pack] runs-on: ubuntu-22.04 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 diff --git a/cli/src/commands/dev.ts b/cli/src/commands/dev.ts index 5306ff09..6412c890 100644 --- a/cli/src/commands/dev.ts +++ b/cli/src/commands/dev.ts @@ -637,13 +637,6 @@ Notes: process.exit(1); } if (options.target === "local-k8s") { - if (releaseMode) { - console.error(chalk.red(`\n Error: --release currently supports --target docker only.`)); - console.error(chalk.dim(` (local-k8s loads images into kind + Helm; published-image support there is tracked separately.)\n`)); - console.error(chalk.yellow(` Run the published images locally with:`)); - console.error(chalk.cyan(` kars dev --release ${releaseVersion}\n`)); - process.exit(1); - } const { runLocalK8s } = await import("./dev/local-k8s.js"); try { await runLocalK8s({ @@ -658,6 +651,7 @@ Notes: agtRepo: options.agtRepo ?? process.env.KARS_AGT_REPO ?? DEFAULT_AGT_REPO, noMesh: options.noMesh === true, globalRegistry: typeof options.globalRegistry === "string" ? options.globalRegistry : undefined, + releaseVersion: releaseMode ? releaseVersion : undefined, }); return; } catch (e) { diff --git a/cli/src/commands/dev/local-k8s.ts b/cli/src/commands/dev/local-k8s.ts index 81a902d4..4dd1ccbc 100644 --- a/cli/src/commands/dev/local-k8s.ts +++ b/cli/src/commands/dev/local-k8s.ts @@ -84,6 +84,15 @@ export interface LocalK8sOptions { * is wired to talk to this URL instead. */ globalRegistry?: string; + /** + * When set (e.g. "v0.1.0-interim.10"), pull the published, signed images + * for that release from ghcr.io/azure and load them into kind instead of + * building anything from source. Mirrors `kars dev --release` for the + * docker target. Requires multi-arch published images on arm64 kind nodes + * (relay/registry must be multi-arch — amd64-only would "exec format + * error" on an Apple Silicon kind node). + */ + releaseVersion?: string; } /** @@ -1034,48 +1043,70 @@ async function deployAgentMesh( meshProvider: "agt", agtRepo: string | undefined, archToken: string, + releaseVersion?: string, ): Promise { void meshProvider; const platform = `linux/${archToken}`; const localTag = (component: "relay" | "registry"): string => `agentmesh-${component}-agt:dev`; - - // ── Build relay + registry images locally (AGT Python) ──────────── - if (!agtRepo) { - throw new Error( - "--mesh-provider=agt requires --agt-repo or $KARS_AGT_REPO pointing at an agent-governance-toolkit checkout.\n" + - " Clone it: git clone https://github.com/microsoft/agent-governance-toolkit", - ); - } - const agtDockerfile = path.join( - agtRepo, - "agent-governance-python/agent-mesh/docker/Dockerfile", - ); - if (!existsSync(agtDockerfile)) { - throw new Error( - `AGT Dockerfile not found at ${agtDockerfile}\n` + - ` Clone it: git clone https://github.com/microsoft/agent-governance-toolkit ${agtRepo}\n` + - ` Or pass --agt-repo / set $KARS_AGT_REPO if you already have it elsewhere.`, - ); - } - for (const component of ["relay", "registry"] as const) { - console.log(chalk.dim(` Building agentmesh-${component} (AGT Python)…`)); - await execa( - tools.runtime, - [ - "build", - "--platform", - platform, - "--build-arg", - `COMPONENT=${component}`, - "-t", - localTag(component), - "-f", - agtDockerfile, - agtRepo, - ], - { stdio: "inherit" }, + const releaseMode = + typeof releaseVersion === "string" && releaseVersion.length > 0; + + if (releaseMode) { + // ── Release: pull published relay/registry + tag as local dev tags ── + // The published images must be multi-arch — an amd64-only relay/registry + // would "exec format error" on an arm64 kind node (Apple Silicon). + for (const component of ["relay", "registry"] as const) { + const remote = `ghcr.io/azure/kars-agentmesh-${component}:${releaseVersion}`; + console.log(chalk.dim(` Pulling published agentmesh-${component} (${releaseVersion})…`)); + try { + await execa(tools.runtime, ["pull", remote], { stdio: "pipe" }); + await execa(tools.runtime, ["tag", remote, localTag(component)], { stdio: "pipe" }); + } catch (e) { + throw new Error( + `Failed to pull published agentmesh-${component} (${remote}): ${(e as Error).message}\n` + + ` Verify the release tag exists and the package is public + multi-arch (arm64).`, + ); + } + } + } else { + // ── Build relay + registry images locally (AGT Python) ──────────── + if (!agtRepo) { + throw new Error( + "--mesh-provider=agt requires --agt-repo or $KARS_AGT_REPO pointing at an agent-governance-toolkit checkout.\n" + + " Clone it: git clone https://github.com/microsoft/agent-governance-toolkit", + ); + } + const agtDockerfile = path.join( + agtRepo, + "agent-governance-python/agent-mesh/docker/Dockerfile", ); + if (!existsSync(agtDockerfile)) { + throw new Error( + `AGT Dockerfile not found at ${agtDockerfile}\n` + + ` Clone it: git clone https://github.com/microsoft/agent-governance-toolkit ${agtRepo}\n` + + ` Or pass --agt-repo / set $KARS_AGT_REPO if you already have it elsewhere.`, + ); + } + for (const component of ["relay", "registry"] as const) { + console.log(chalk.dim(` Building agentmesh-${component} (AGT Python)…`)); + await execa( + tools.runtime, + [ + "build", + "--platform", + platform, + "--build-arg", + `COMPONENT=${component}`, + "-t", + localTag(component), + "-f", + agtDockerfile, + agtRepo, + ], + { stdio: "inherit" }, + ); + } } // ── Load images into kind ───────────────────────────────────────── @@ -1152,10 +1183,19 @@ async function deployAgentMesh( export async function runLocalK8s(opts: LocalK8sOptions): Promise { const stepper = new Stepper({ totalSteps: 14 }); + // Release mode: pull published, signed images from ghcr.io/azure and load + // them into kind instead of building anything from source. + const releaseMode = + typeof opts.releaseVersion === "string" && opts.releaseVersion.length > 0; + const RELEASE_REGISTRY = "ghcr.io/azure"; + const releaseRef = (name: string): string => + `${RELEASE_REGISTRY}/${name}:${opts.releaseVersion}`; + // Fail fast if the user is running outside the kars checkout. - // `--target local-k8s` rebuilds the controller, router, and sandbox - // images from local source via docker build, which needs the repo - // root (Cargo.toml + deploy/helm/kars + sandbox-images/...). + // `--target local-k8s` needs the repo root for the Helm chart + // (deploy/helm/kars) and the agentmesh manifest (deploy/agentmesh-agt.yaml), + // and — unless --release pulls published images — it also builds the + // controller, router, and sandbox images from local source. // Without this check the failure surfaces only AFTER kind cluster // creation (10+ seconds wasted, orphaned cluster left behind). if (!opts.noBuild) { @@ -1267,7 +1307,7 @@ export async function runLocalK8s(opts: LocalK8sOptions): Promise { // `kars push` (which builds for AKS) would crash openclaw under // Rosetta on an Apple Silicon laptop with // `rt_tgsigqueueinfo failed in pend_signal`. - if (!opts.noBuild) { + if (!opts.noBuild && !releaseMode) { const archToken = hostDockerArch(); const repoRootForBuild = findRepoRoot(process.cwd()); stepper.step(`Checking image arch (host=${archToken})…`); @@ -1285,6 +1325,35 @@ export async function runLocalK8s(opts: LocalK8sOptions): Promise { } } + // Release mode: pull the published images and tag them as the local "dev" + // aliases the load step below expects (openclaw-sandbox:dev, + // kars-controller:dev, kars-inference-router:dev). The existing + // alias→canonical retag + `kind load` plumbing then loads them unchanged. + // Pull host-arch (kind nodes are arm64 on Apple Silicon); the published + // controller/router/sandbox are multi-arch so `docker pull` selects the + // right variant automatically. + if (releaseMode) { + stepper.step(`Pulling published images (${opts.releaseVersion})…`); + const pulls: { remote: string; devTag: string }[] = [ + { remote: releaseRef("openclaw-sandbox"), devTag: opts.image }, + { remote: releaseRef("kars-controller"), devTag: "kars-controller:dev" }, + { remote: releaseRef("kars-inference-router"), devTag: "kars-inference-router:dev" }, + ]; + for (const { remote, devTag } of pulls) { + try { + await execa(tools.runtime, ["pull", remote], { stdio: "pipe" }); + await execa(tools.runtime, ["tag", remote, devTag], { stdio: "pipe" }); + } catch (e) { + throw new Error( + `Failed to pull published image ${remote}: ${(e as Error).message}\n` + + ` Verify the release tag exists and the package is public:\n` + + ` https://github.com/Azure/kars/pkgs/container/${remote.split("/").pop()?.split(":")[0]}`, + ); + } + } + stepper.done(`pulled ${pulls.length} published image(s) for ${opts.releaseVersion}`); + } + // The values-local-dev overlay pins all images to local "dev" tags // with imagePullPolicy=Never, so we MUST load all three images that // the chart references — sandbox, controller, inference-router. @@ -1301,7 +1370,7 @@ export async function runLocalK8s(opts: LocalK8sOptions): Promise { // will then ImagePullBackOff and the operator's pod-events display // surfaces a clear error pointing at the missing build). stepper.step("Loading kars images into the kind cluster…"); - if (opts.noBuild) { + if (opts.noBuild && !releaseMode) { stepper.done("skipped image load (--no-build)"); } else { // `target` = the canonical image name the controller looks for @@ -1525,6 +1594,7 @@ export async function runLocalK8s(opts: LocalK8sOptions): Promise { meshProvider, opts.agtRepo, archToken, + opts.releaseVersion, ); stepper.done(`agentmesh-${meshProvider} ready`); } diff --git a/tools/e2e-harness/platforms/local-k8s.sh b/tools/e2e-harness/platforms/local-k8s.sh index aaa80846..212d48c5 100644 --- a/tools/e2e-harness/platforms/local-k8s.sh +++ b/tools/e2e-harness/platforms/local-k8s.sh @@ -53,9 +53,20 @@ platform_preflight() { elif kind get clusters 2>/dev/null | grep -qx "${KIND_CLUSTER_NAME}"; then log "kind cluster '${KIND_CLUSTER_NAME}' already exists — skipping bring-up" else - log "bringing up local-k8s via 'kars dev --target local-k8s --cluster-name ${KIND_CLUSTER_NAME} --once'" + # KARS_RELEASE= runs the published-images path (kars dev --release + # --target local-k8s) — pulls signed multi-arch images into kind + # instead of building from source. Requires multi-arch relay/registry + # on arm64 kind nodes. + local release_args=() + if [ -n "${KARS_RELEASE:-}" ]; then + release_args=(--release "${KARS_RELEASE}") + log "released mode: kars dev --release ${KARS_RELEASE} --target local-k8s (published images)" + else + log "bringing up local-k8s via 'kars dev --target local-k8s --cluster-name ${KIND_CLUSTER_NAME} --once'" + fi kars dev --target local-k8s \ --cluster-name "${KIND_CLUSTER_NAME}" \ + "${release_args[@]}" \ --once \ >>"${OUT_DIR}/dev-bringup.log" 2>&1 || { log "ERR kars dev bring-up failed; tail of dev-bringup.log:"