From 0631c769163981c4bdfe91171cd0eef1e5a067ce Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Tue, 4 Aug 2026 03:01:27 -0700 Subject: [PATCH 1/3] fix(ci): restore main image and platform gates --- .github/workflows/managed-images.yaml | 29 ++++++++++--------- .github/workflows/platform-vitest-main.yaml | 5 +++- agents/hermes/Dockerfile | 2 +- ...rify-managed-image-publication-evidence.sh | 3 +- test/hermes-final-image-layout.test.ts | 2 +- test/managed-image-capability-union.test.ts | 4 +-- ...managed-image-publication-evidence.test.ts | 8 +++-- ...managed-image-publication-workflow.test.ts | 4 +++ test/platform-vitest-main-workflow.test.ts | 6 ++++ test/snapshot-gateway-guard.test.ts | 3 +- 10 files changed, 44 insertions(+), 22 deletions(-) diff --git a/.github/workflows/managed-images.yaml b/.github/workflows/managed-images.yaml index 460b804ed7c..d510b458ec7 100644 --- a/.github/workflows/managed-images.yaml +++ b/.github/workflows/managed-images.yaml @@ -583,21 +583,24 @@ jobs: slack: ["@openclaw/slack", "2026.7.1"], whatsapp: ["@openclaw/whatsapp", "2026.7.1"], msteams: ["@openclaw/msteams", "2026.7.1"], + googlechat: ["@openclaw/googlechat", "2026.7.1"], }; + const projectsRoot = "/sandbox/.openclaw/npm/projects"; + const installedPackages = fs + .readdirSync(projectsRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .flatMap((entry) => { + const manifestPath = path.join(projectsRoot, entry.name, "package.json"); + if (!fs.existsSync(manifestPath)) return []; + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + return [{ name: manifest.name, version: manifest.version }]; + }); for (const [id, [name, version]] of Object.entries(packages)) { - const install = config.plugins?.installs?.[id]; - if ( - !install || - typeof install.installPath !== "string" || - config.plugins?.entries?.[id]?.enabled !== false - ) { - throw new Error(`managed OpenClaw plugin ${id} is missing or active`); - } - const manifest = JSON.parse( - fs.readFileSync(path.join(install.installPath, "package.json"), "utf8"), + const matches = installedPackages.filter( + (installed) => installed.name === name && installed.version === version, ); - if (manifest.name !== name || manifest.version !== version) { - throw new Error(`managed OpenClaw plugin ${id} has an unexpected package identity`); + if (matches.length !== 1 || config.plugins?.entries?.[id]?.enabled !== false) { + throw new Error(`managed OpenClaw plugin ${id} is missing, duplicated, or active`); } } for (const id of ["telegram", "tavily"]) { @@ -634,7 +637,7 @@ jobs: raise SystemExit("managed Hermes optional platform is not explicitly disabled") if metadata.version("microsoft-teams-apps") != "2.0.13.4": raise SystemExit("managed Hermes microsoft-teams-apps version is wrong") - if metadata.version("aiohttp") != "3.14.1": + if metadata.version("aiohttp") != "3.14.3": raise SystemExit("managed Hermes aiohttp version is wrong") VALIDATE_HERMES_UNION ;; diff --git a/.github/workflows/platform-vitest-main.yaml b/.github/workflows/platform-vitest-main.yaml index 16c30fed274..f812ebc9dc6 100644 --- a/.github/workflows/platform-vitest-main.yaml +++ b/.github/workflows/platform-vitest-main.yaml @@ -65,7 +65,10 @@ jobs: run: npm ci --ignore-scripts - name: Build CLI - run: npm run build:cli + run: | + git config --global --add safe.directory "$GITHUB_WORKSPACE" + test "$(git rev-parse --verify HEAD)" = "$GITHUB_SHA" + npm run build:cli - name: Run Ubuntu 26.04 compatibility contracts run: | diff --git a/agents/hermes/Dockerfile b/agents/hermes/Dockerfile index 610bd84ab07..6cf5107dc2c 100644 --- a/agents/hermes/Dockerfile +++ b/agents/hermes/Dockerfile @@ -699,7 +699,7 @@ RUN mkdir -p /sandbox/.nemoclaw/blueprints/0.1.0 \ RUN HERMES_HOME=/sandbox/.hermes /usr/local/bin/hermes doctor --fix \ && node --experimental-strip-types /opt/nemoclaw-hermes-config/generate-config.ts \ && if [ "$NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION" = "1" ]; then \ - /opt/hermes/.venv/bin/python -I -c 'import importlib.metadata as m, pathlib, yaml; config=yaml.safe_load(pathlib.Path("/sandbox/.hermes/config.yaml").read_text()); neutral={name: value for name, value in config["platforms"].items() if name != "api_server"}; assert len(neutral) == 30; assert all(value == {"enabled": False} for value in neutral.values()); assert m.version("microsoft-teams-apps") == "2.0.13.4"; assert m.version("aiohttp") == "3.14.1"'; \ + /opt/hermes/.venv/bin/python -I -c 'import importlib.metadata as m, pathlib, yaml; config=yaml.safe_load(pathlib.Path("/sandbox/.hermes/config.yaml").read_text()); neutral={name: value for name, value in config["platforms"].items() if name != "api_server"}; assert len(neutral) == 30; assert all(value == {"enabled": False} for value in neutral.values()); assert m.version("microsoft-teams-apps") == "2.0.13.4"; assert m.version("aiohttp") == "3.14.3"'; \ GOOGLE_CHAT_PROJECT_ID=nemoclaw-hostile \ GOOGLE_CHAT_SUBSCRIPTION_NAME=projects/nemoclaw-hostile/subscriptions/nemoclaw-hostile \ GOOGLE_CHAT_SERVICE_ACCOUNT_JSON='{"type":"service_account","project_id":"nemoclaw-hostile"}' \ diff --git a/scripts/checks/verify-managed-image-publication-evidence.sh b/scripts/checks/verify-managed-image-publication-evidence.sh index 65c36655cdb..a8d67496318 100755 --- a/scripts/checks/verify-managed-image-publication-evidence.sh +++ b/scripts/checks/verify-managed-image-publication-evidence.sh @@ -312,7 +312,8 @@ fi workload_sha256="${workload_digest#sha256:}" base_sha256="${base_reference##*@sha256:}" -base_dependency_uri="pkg:docker/${base_reference}?platform=${platform//\//%2F}" +base_repository="${base_reference%@sha256:*}" +base_dependency_uri="pkg:docker/${base_repository}?digest=sha256:${base_sha256}&platform=${platform//\//%2F}" source_url="https://github.com/${repository}" builder_id="${source_url}/actions/runs/${run_id}/attempts/${run_attempt}" if ! jq -e \ diff --git a/test/hermes-final-image-layout.test.ts b/test/hermes-final-image-layout.test.ts index 2c86d67f503..32501c846e7 100644 --- a/test/hermes-final-image-layout.test.ts +++ b/test/hermes-final-image-layout.test.ts @@ -414,7 +414,7 @@ describe("Hermes final image layout", () => { ); expect(doctorLayer).toContain('if [ "$NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION" = "1" ]; then'); expect(doctorLayer).toContain('assert m.version("microsoft-teams-apps") == "2.0.13.4"'); - expect(doctorLayer).toContain('assert m.version("aiohttp") == "3.14.1"'); + expect(doctorLayer).toContain('assert m.version("aiohttp") == "3.14.3"'); expect(doctorLayer).toContain("assert len(neutral) == 30"); expect(doctorLayer).toContain("neutral-platform-inertness"); expect(doctorLayer).toContain("GOOGLE_CHAT_SERVICE_ACCOUNT_JSON"); diff --git a/test/managed-image-capability-union.test.ts b/test/managed-image-capability-union.test.ts index d92ba178569..65df8376657 100644 --- a/test/managed-image-capability-union.test.ts +++ b/test/managed-image-capability-union.test.ts @@ -49,7 +49,7 @@ describe("managed-image capability union", () => { ); expect(collectManagedImageHermesUvPackages()).toEqual([ "microsoft-teams-apps==2.0.13.4", - "aiohttp==3.14.1", + "aiohttp==3.14.3", ]); }); @@ -149,7 +149,7 @@ describe("managed-image capability union", () => { NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION: "1", }); expect(fs.readFileSync(trace, "utf8").trim()).toBe( - "pip install --python /opt/hermes/.venv/bin/python --no-cache -- microsoft-teams-apps==2.0.13.4 aiohttp==3.14.1", + "pip install --python /opt/hermes/.venv/bin/python --no-cache -- microsoft-teams-apps==2.0.13.4 aiohttp==3.14.3", ); } finally { fs.rmSync(temporaryRoot, { force: true, recursive: true }); diff --git a/test/managed-image-publication-evidence.test.ts b/test/managed-image-publication-evidence.test.ts index 3f5d799726f..ef02fe8e0cf 100644 --- a/test/managed-image-publication-evidence.test.ts +++ b/test/managed-image-publication-evidence.test.ts @@ -27,6 +27,10 @@ const runAttempt = "1"; const cohort = `ghrun-${runId}-${runAttempt}`; const baseReference = `ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:${"5".repeat(64)}`; const image = "ghcr.io/nvidia/nemoclaw/openclaw-sandbox"; +const buildkitBaseDependencyUri = (targetPlatform: string) => { + const [repositoryName, digest] = baseReference.split("@sha256:"); + return `pkg:docker/${repositoryName}?digest=sha256:${digest}&platform=${targetPlatform.replace("/", "%2F")}`; +}; type FixtureOptions = { attestationWorkloadDigest?: string; @@ -117,7 +121,7 @@ function runEvidence(options: FixtureOptions = {}, digestOverride?: string) { internalParameters: { buildConfig: {} }, resolvedDependencies: [ { - uri: options.slsaDependencyUri ?? `pkg:docker/${baseReference}?platform=linux%2Famd64`, + uri: options.slsaDependencyUri ?? buildkitBaseDependencyUri(platform), digest: { sha256: baseReference.split("@sha256:")[1] }, }, ], @@ -488,7 +492,7 @@ describe("managed image publication evidence verifier", () => { [ "base dependency platform", { - slsaDependencyUri: `pkg:docker/${baseReference}?platform=linux%2Farm64`, + slsaDependencyUri: buildkitBaseDependencyUri("linux/arm64"), }, ], [ diff --git a/test/managed-image-publication-workflow.test.ts b/test/managed-image-publication-workflow.test.ts index 8442d5ebd48..9b0f14f7e21 100644 --- a/test/managed-image-publication-workflow.test.ts +++ b/test/managed-image-publication-workflow.test.ts @@ -181,6 +181,9 @@ function publicationBoundaryErrors(baseWorkflow: Workflow, managedWorkflow: Work "@openclaw/slack", "@openclaw/whatsapp", "@openclaw/msteams", + "@openclaw/googlechat", + "/sandbox/.openclaw/npm/projects", + "matches.length !== 1", "microsoft-teams-apps", "config.plugins?.entries?.[id]?.enabled !== false", 'config["platforms"].get(name) != {"enabled": False}', @@ -251,6 +254,7 @@ describe("complete managed-image publication workflow", () => { ); expect(publicationBoundaryErrors(baseWorkflow, managedWorkflow)).toEqual([]); + expect(JSON.stringify(managedWorkflow)).not.toContain("config.plugins?.installs?.[id]"); expect(publisher).toMatchObject({ needs: ["build-and-push-hermes", "build-and-push-dcode", "build-and-push-openclaw"], permissions: { diff --git a/test/platform-vitest-main-workflow.test.ts b/test/platform-vitest-main-workflow.test.ts index 8551e8cc16e..37fbd1a1b4d 100644 --- a/test/platform-vitest-main-workflow.test.ts +++ b/test/platform-vitest-main-workflow.test.ts @@ -32,6 +32,12 @@ function step(jobName: string, name: string): WorkflowStep { } describe("platform Vitest main workflow", () => { + it("marks the container checkout safe before generating build identity", () => { + const run = step("ubuntu-2604-contract", "Build CLI").run ?? ""; + expect(run).toContain('git config --global --add safe.directory "$GITHUB_WORKSPACE"'); + expect(run).toContain('test "$(git rev-parse --verify HEAD)" = "$GITHUB_SHA"'); + expect(run.indexOf("safe.directory")).toBeLessThan(run.indexOf("npm run build:cli")); + }); // source-shape-contract: security -- The trusted helper installs only checksum-verified official Node.js archives it("pins and verifies the Node.js archive in the trusted WSL helper", () => { const installSteps = [{ run: wslHelperSource }]; diff --git a/test/snapshot-gateway-guard.test.ts b/test/snapshot-gateway-guard.test.ts index 4449a692b29..bc86c4204dd 100644 --- a/test/snapshot-gateway-guard.test.ts +++ b/test/snapshot-gateway-guard.test.ts @@ -196,6 +196,7 @@ function makeVmRestoreToEnv( const cloneReadyMarker = path.join(home, "clone-1-ready"); const cloneRunningMarker = path.join(home, "clone-1-running"); const gatewayLifecycleLog = path.join(home, "gateway-lifecycle.log"); + const dashboardBind = process.env.WSL_DISTRO_NAME ? "0.0.0.0" : "127.0.0.1"; writeExecutable(path.join(localBin, "openshell"), [ 'case "$1 $2" in', ' "gateway info") printf "Gateway Info\\n\\nGateway: nemoclaw\\nGateway endpoint: https://127.0.0.1:8080/\\n"; exit 0 ;;', @@ -208,7 +209,7 @@ function makeVmRestoreToEnv( ' printf "NEMOCLAW_DCODE_PROBE=no-runtime\\n"; exit 0 ;;', ' "sandbox ssh-config") printf "Host openshell-alpha\\n HostName 127.0.0.1\\n User sandbox\\n"; exit 0 ;;', ` "sandbox create") touch ${JSON.stringify(cloneReadyMarker)} ${JSON.stringify(cloneRunningMarker)}; printf "created clone-1\\n"; exit 0 ;;`, - ` "forward list") printf "SANDBOX BIND PORT PID STATUS\\nclone-1 127.0.0.1 ${String(dashboardPort)} 4242 running\\n"; exit 0 ;;`, + ` "forward list") printf "SANDBOX BIND PORT PID STATUS\\nclone-1 ${dashboardBind} ${String(dashboardPort)} 4242 running\\n"; exit 0 ;;`, ' "forward stop") exit 1 ;;', "esac", 'if [ "$1" = "status" ]; then exit 0; fi', From dac40de77a4665d03da22bec8545f415d4897328 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Tue, 4 Aug 2026 03:22:35 -0700 Subject: [PATCH 2/3] fix(ci): validate complete neutral image contracts --- .github/workflows/managed-images.yaml | 1 + test/managed-image-publication-evidence.test.ts | 2 +- test/managed-image-publication-workflow.test.ts | 7 +++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/managed-images.yaml b/.github/workflows/managed-images.yaml index d510b458ec7..52862592d4a 100644 --- a/.github/workflows/managed-images.yaml +++ b/.github/workflows/managed-images.yaml @@ -615,6 +615,7 @@ jobs: "slack", "whatsapp", "msteams", + "googlechat", ]) { if (config.channels?.[id]?.enabled !== false) { throw new Error(`managed OpenClaw channel ${id} is not explicitly disabled`); diff --git a/test/managed-image-publication-evidence.test.ts b/test/managed-image-publication-evidence.test.ts index ef02fe8e0cf..9db1fc773f9 100644 --- a/test/managed-image-publication-evidence.test.ts +++ b/test/managed-image-publication-evidence.test.ts @@ -29,7 +29,7 @@ const baseReference = `ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:${"5".repeat( const image = "ghcr.io/nvidia/nemoclaw/openclaw-sandbox"; const buildkitBaseDependencyUri = (targetPlatform: string) => { const [repositoryName, digest] = baseReference.split("@sha256:"); - return `pkg:docker/${repositoryName}?digest=sha256:${digest}&platform=${targetPlatform.replace("/", "%2F")}`; + return `pkg:docker/${repositoryName}?digest=sha256:${digest}&platform=${targetPlatform.replaceAll("/", "%2F")}`; }; type FixtureOptions = { diff --git a/test/managed-image-publication-workflow.test.ts b/test/managed-image-publication-workflow.test.ts index 9b0f14f7e21..975abb1fa20 100644 --- a/test/managed-image-publication-workflow.test.ts +++ b/test/managed-image-publication-workflow.test.ts @@ -255,6 +255,13 @@ describe("complete managed-image publication workflow", () => { expect(publicationBoundaryErrors(baseWorkflow, managedWorkflow)).toEqual([]); expect(JSON.stringify(managedWorkflow)).not.toContain("config.plugins?.installs?.[id]"); + const validationRun = + step(managedPublisher(managedWorkflow), "Validate exact managed image before promotion") + .run ?? ""; + const channelGuardEnd = validationRun.indexOf("managed OpenClaw channel"); + const channelGuardStart = validationRun.lastIndexOf("for (const id of [", channelGuardEnd); + expect(channelGuardStart).toBeGreaterThan(-1); + expect(validationRun.slice(channelGuardStart, channelGuardEnd)).toContain('"googlechat",'); expect(publisher).toMatchObject({ needs: ["build-and-push-hermes", "build-and-push-dcode", "build-and-push-openclaw"], permissions: { From 6cd1da386c811720e4318c1b9317b49cf575fc64 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Tue, 4 Aug 2026 03:30:36 -0700 Subject: [PATCH 3/3] fix(ci): reject symlinked package manifests --- .github/workflows/managed-images.yaml | 2 +- test/managed-image-publication-workflow.test.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/managed-images.yaml b/.github/workflows/managed-images.yaml index 52862592d4a..8688efecef6 100644 --- a/.github/workflows/managed-images.yaml +++ b/.github/workflows/managed-images.yaml @@ -591,7 +591,7 @@ jobs: .filter((entry) => entry.isDirectory()) .flatMap((entry) => { const manifestPath = path.join(projectsRoot, entry.name, "package.json"); - if (!fs.existsSync(manifestPath)) return []; + if (!fs.existsSync(manifestPath) || !fs.lstatSync(manifestPath).isFile()) return []; const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); return [{ name: manifest.name, version: manifest.version }]; }); diff --git a/test/managed-image-publication-workflow.test.ts b/test/managed-image-publication-workflow.test.ts index 975abb1fa20..802b6d785dd 100644 --- a/test/managed-image-publication-workflow.test.ts +++ b/test/managed-image-publication-workflow.test.ts @@ -183,6 +183,7 @@ function publicationBoundaryErrors(baseWorkflow: Workflow, managedWorkflow: Work "@openclaw/msteams", "@openclaw/googlechat", "/sandbox/.openclaw/npm/projects", + "lstatSync(manifestPath).isFile()", "matches.length !== 1", "microsoft-teams-apps", "config.plugins?.entries?.[id]?.enabled !== false", @@ -262,6 +263,18 @@ describe("complete managed-image publication workflow", () => { const channelGuardStart = validationRun.lastIndexOf("for (const id of [", channelGuardEnd); expect(channelGuardStart).toBeGreaterThan(-1); expect(validationRun.slice(channelGuardStart, channelGuardEnd)).toContain('"googlechat",'); + const weakenedWorkflow = structuredClone(managedWorkflow); + const weakenedValidation = step( + managedPublisher(weakenedWorkflow), + "Validate exact managed image before promotion", + ); + weakenedValidation.run = weakenedValidation.run?.replace( + " || !fs.lstatSync(manifestPath).isFile()", + "", + ); + expect(publicationBoundaryErrors(baseWorkflow, weakenedWorkflow)).toContain( + "exact managed image validation is missing lstatSync(manifestPath).isFile()", + ); expect(publisher).toMatchObject({ needs: ["build-and-push-hermes", "build-and-push-dcode", "build-and-push-openclaw"], permissions: {