Files
molecule-ai-workspace-templ…/.gitea/workflows/ci.yml
T
hongmingandClaude Opus 5 74048092b2
CI / Template validation (static) (pull_request) Successful in 7s
CI / Template validation (static) (push) Successful in 7s
meta-ci-advisory / meta (advisory) (pull_request) Successful in 8s
verify-providers-projection / Regenerate projection, fail on drift, assert registry ⊆ template (pull_request) Successful in 14s
Secret scan / secret-scan (pull_request) Successful in 17s
CI / Adapter unit tests (pull_request) Successful in 38s
CI / Adapter unit tests (push) Successful in 39s
CI / T4 tier-4 conformance (live) (pull_request) Successful in 1m22s
CI / Template validation (runtime) (push) Successful in 1m25s
CI / Template validation (runtime) (pull_request) Successful in 1m27s
CI / validate (pull_request) Successful in 3s
CI / T4 tier-4 conformance (live) (push) Successful in 1m29s
CI / validate (push) Successful in 4s
ci: keep the docker build log when the build fails
Class fix, ported from template-hermes. Both jobs that build this template's
image ran

    docker build ... --no-cache 2>&1 | tail -5

A failing buildkit run ends with the offending Dockerfile fragment and a
"failed to solve ... exit code: 1" summary, so those five lines are the same
every time and never contain the cause. On the hermes sibling that turned a
transient into a manual sweep against the live forge just to distinguish a bad
pin from a network blip; the rerun was green, so the only lasting artifact of
the incident was that CI could not explain its own failure. All four workspace
templates carried the identical pipe.

Nothing here changes what CI accepts -- only what it can tell you when it
refuses.

The build is now redirected to a file instead of piped, and the whole log is
printed inside a collapsed ::group:: when it fails. The FULL log, not a bigger
tail: buildkit interleaves parallel stages, so the failing layer's output can
sit arbitrarily far from the end, and a larger constant is the same defect with
a different number. Redirect rather than pipe because `tail` should not be in a
position to own the build's exit status.

Kept INLINE rather than extracted to a helper: tests/test_ci_runtime_image_pin.py
already pins "exactly one `docker build` step per job" as part of the
runtime-image contract, and a first attempt to source a shell helper broke that
guard in all four templates. The guard was right; the refactor was wrong. The
failure-path echoes say "image build" for the same reason -- "docker build" in
an echo would inflate that count.

test_failing_image_build_surfaces_its_own_output covers both jobs and kills
three mutants: restoring the pipe, printing a tail instead of the full log on
failure, and swallowing the build's exit status.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 04:25:54 -07:00

651 lines
33 KiB
YAML

name: CI
# Repository-local validation pipeline. It fetches the canonical validator
# from molecule-ci at a reviewed immutable commit on each run, while keeping
# this repository's protected context names and runner requirements explicit.
#
# Workflow compatibility choices:
# - No workflow_dispatch.inputs
# - No merge_group trigger
# - No cross-repo uses:
# - GITHUB_SERVER_URL pinned at workflow level
# - No on.push.paths: filter
# - timeout-minutes on every job
on: [push, pull_request]
# Defense-in-depth de-dup ONLY (the t4-conformance unique-name fix is the
# actual fail-closed primitive against the shared-host-daemon race; see
# that job). Scope per workflow + ref + EVENT so the push run and the
# pull_request run of the same internal-PR commit get DISTINCT groups —
# they must both complete (each emits its own required-status context;
# feedback_gitea_gate_check_required_list_not_combined_status). Never
# per-SHA-global: that silently cross-cancels legit required checks
# (feedback_concurrency_group_per_sha). cancel-in-progress:false so an
# in-flight live T4 probe is never aborted mid-assertion (a cancelled
# privileged probe would look like a gate failure / flake); a newer push
# to the same ref+event simply queues behind it.
concurrency:
group: ci-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: false
env:
GITHUB_SERVER_URL: https://git.moleculesai.app
# Canonical validation tooling is reviewed and content-addressed.
MOLECULE_CI_REF: 11b8598e5c0b3f0b1031733a8d5f6bc238f146a4
permissions:
contents: read
jobs:
validate-static:
name: Template validation (static)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Fetch immutable molecule-ci canonical scripts
run: |
set -euo pipefail
CI_ROOT=".molecule-ci-canonical"
mkdir "$CI_ROOT"
git -C "$CI_ROOT" init -q
git -C "$CI_ROOT" remote add origin https://git.moleculesai.app/molecule-ai/molecule-ci.git
fetched=false
for attempt in 1 2 3; do
if GIT_ASKPASS=/bin/false GIT_TERMINAL_PROMPT=0 git -c credential.helper= -c http.userAgent=curl/8.4.0 -C "$CI_ROOT" fetch --no-tags --depth 1 origin "$MOLECULE_CI_REF"; then
fetched=true
break
fi
sleep "$attempt"
done
if [ "$fetched" != true ]; then
echo "::error::immutable molecule-ci validator fetch failed"
exit 1
fi
git -C "$CI_ROOT" checkout -q --detach FETCH_HEAD
ACTUAL_CI_REF="$(git -C "$CI_ROOT" rev-parse HEAD)"
if [ "$ACTUAL_CI_REF" != "$MOLECULE_CI_REF" ]; then
echo "::error::immutable molecule-ci validator ref mismatch"
exit 1
fi
- name: Check for secrets
run: |
python3 - << 'PYEOF'
import os, re, sys
from pathlib import Path
PATTERNS = [
re.compile(r'''["']sk-ant-[a-zA-Z0-9]{50,}["']'''),
re.compile(r'''["']ghp_[a-zA-Z0-9]{36,}["']'''),
re.compile(r'''["']AKIA[A-Z0-9]{16}["']'''),
re.compile(r'''["'][a-zA-Z0-9/+=]{40}["']'''),
re.compile(r'''["']sk_test_[a-zA-Z0-9]{24,}["']'''),
re.compile(r'''["']Bearer\s+[a-zA-Z0-9_.-]{20,}["']'''),
re.compile(r'''ghp_[a-zA-Z0-9]{36,}'''),
re.compile(r'''sk-ant-[a-zA-Z0-9]{50,}'''),
]
SKIP_DIRS = {'.molecule-ci-canonical', '.git', 'node_modules', '__pycache__'}
EXTENSIONS = {'.yaml', '.yml', '.md', '.py', '.sh'}
def is_false_positive(line):
ctx = line.lower()
return '...' in ctx or '<example' in ctx or '</example' in ctx
root = Path(os.environ.get('GITHUB_WORKSPACE', '.'))
warnings = []
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
for filename in filenames:
if Path(filename).suffix not in EXTENSIONS:
continue
filepath = Path(dirpath) / filename
try:
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
for lineno, line in enumerate(f.readlines(), 1):
for pattern in PATTERNS:
for match in pattern.finditer(line):
if not is_false_positive(line):
warnings.append(f" {filepath}:{lineno}: {match.group(0)[:40]}...")
except Exception:
pass
if warnings:
print("::error::Potential secret found in committed files:")
for w in warnings:
print(w)
sys.exit(1)
else:
print("::notice::No secrets detected")
PYEOF
- run: pip install pyyaml packaging -q
- name: Reject legacy declared-plugin installer
run: |
python3 - << 'PYEOF'
from pathlib import Path
from packaging.requirements import Requirement
from packaging.version import Version
entrypoint = Path("entrypoint.sh").read_text(encoding="utf-8")
executable_lines = "\n".join(
line
for line in entrypoint.splitlines()
if not line.lstrip().startswith("#")
)
forbidden = (
"MOLECULE_DECLARED_PLUGINS",
"molecule_runtime.plugin_sources",
"install_declared_plugins",
"for _plg_src in $MOLECULE_DECLARED_PLUGINS",
"/archive/${_plg_ref}.tar.gz",
"Authorization: token ${MOLECULE_TEMPLATE_REPO_TOKEN}",
'mkdir -p "/configs/plugins/$_plg_name"',
'cp -a "$_plg_dir/." "/configs/plugins/$_plg_name/"',
)
found = [fragment for fragment in forbidden if fragment in executable_lines]
if found:
raise SystemExit(
"legacy privileged declared-plugin installer detected: "
+ ", ".join(found)
)
runtime_version = Version(Path(".runtime-version").read_text().strip())
hardened_version = Version("0.4.0")
runtime_requirement = next(
Requirement(line)
for line in Path("requirements.txt").read_text().splitlines()
if line.strip().startswith("molecules-workspace-runtime")
)
if runtime_version < hardened_version:
raise SystemExit("runtime pin predates hardened plugin installer")
if runtime_version not in runtime_requirement.specifier:
raise SystemExit("runtime pin is outside the template requirement")
hardened_floor = any(
specifier.operator in {">=", ">", "==", "===", "~="}
and Version(specifier.version) >= hardened_version
for specifier in runtime_requirement.specifier
)
if not hardened_floor:
raise SystemExit("template requirement lacks a hardened runtime floor")
if Version("0.3.125") in runtime_requirement.specifier:
raise SystemExit("template requirement permits vulnerable runtime")
PYEOF
- run: python3 .molecule-ci-canonical/scripts/validate-workspace-template.py --static-only
# Audit E1: scripts/molecule-askpass is byte-copied across every workspace
# template (no shared base image yet). Pin its sha256 so an unintentional
# edit — or a hand-copy that drifts from its peers — reds this PR. All
# templates pin the SAME value; a legitimate change must update the file
# AND this pin in every template together (which a reviewer sees). Always
# fires (validate-static has no paths filter / fork guard) and feeds the
# required `validate` aggregate, so it is a hard gate with no secrets.
- name: askpass helper drift guard (audit E1)
run: |
expected=7def3e86a3edf8ff28018081f14a6e390790cb102fd06c27c3f7040b07464303
test -f scripts/molecule-askpass || { echo "::error::scripts/molecule-askpass is missing"; exit 1; }
actual=$(sha256sum scripts/molecule-askpass | cut -d' ' -f1)
if [ "$actual" != "$expected" ]; then
echo "::error::scripts/molecule-askpass drifted from the pinned cross-template sha256."
echo " expected $expected"
echo " actual $actual"
echo " If this change is intentional, apply it identically to EVERY workspace template and bump this pin in each template's ci.yml."
exit 1
fi
echo "askpass pin OK ($actual)"
# pc2-safe: false — needs docker for Docker build smoke; routes to docker-host only
# (task #390 B-lite — PC2 native runners advertise `ubuntu-latest` but cannot
# docker due to npipe→Linux-container bridging gap; AND-match on both labels
# excludes them while keeping the Linux docker-host runners eligible.)
validate-runtime:
name: Template validation (runtime)
runs-on: [ubuntu-latest, docker-host]
timeout-minutes: 15
needs: validate-static
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Skip executable validation for external forks
if: github.event.pull_request.head.repo.fork == true
run: echo "::notice::runtime installation, imports, tests, and Docker builds are disabled for external forks"
- name: Fetch immutable molecule-ci canonical scripts
if: github.event.pull_request.head.repo.fork != true
run: |
set -euo pipefail
CI_ROOT=".molecule-ci-canonical"
mkdir "$CI_ROOT"
git -C "$CI_ROOT" init -q
git -C "$CI_ROOT" remote add origin https://git.moleculesai.app/molecule-ai/molecule-ci.git
fetched=false
for attempt in 1 2 3; do
if GIT_ASKPASS=/bin/false GIT_TERMINAL_PROMPT=0 git -c credential.helper= -c http.userAgent=curl/8.4.0 -C "$CI_ROOT" fetch --no-tags --depth 1 origin "$MOLECULE_CI_REF"; then
fetched=true
break
fi
sleep "$attempt"
done
if [ "$fetched" != true ]; then
echo "::error::immutable molecule-ci validator fetch failed"
exit 1
fi
git -C "$CI_ROOT" checkout -q --detach FETCH_HEAD
ACTUAL_CI_REF="$(git -C "$CI_ROOT" rev-parse HEAD)"
if [ "$ACTUAL_CI_REF" != "$MOLECULE_CI_REF" ]; then
echo "::error::immutable molecule-ci validator ref mismatch"
exit 1
fi
- name: Install canonical validator dependencies
if: github.event.pull_request.head.repo.fork != true
run: python3 -m pip install --isolated --break-system-packages -q pyyaml jsonschema packaging
- name: Install source-pinned runtime and template dependencies
if: github.event.pull_request.head.repo.fork != true
run: python3 .molecule-ci-canonical/scripts/install_workspace_dependencies.py --allow-missing --break-system-packages
- name: Validate workspace-template contract (full)
if: github.event.pull_request.head.repo.fork != true
run: python3 .molecule-ci-canonical/scripts/validate-workspace-template.py
- name: Adapter platform-routing unit tests
if: github.event.pull_request.head.repo.fork != true
# Targeted early feedback for platform routing. The repository-tests
# job below installs the full test dependencies and runs all of tests/.
run: |
pip install -q pytest
python3 -m pytest tests/test_platform_routing.py -q
- name: Docker build smoke test
if: github.event.pull_request.head.repo.fork != true
run: |
set -euo pipefail
: "${GITHUB_RUN_ID:?GITHUB_RUN_ID is required}"
: "${GITHUB_RUN_ATTEMPT:?GITHUB_RUN_ATTEMPT is required}"
SMOKE_TAG="molecule-ai-workspace-openclaw-smoke-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
trap 'docker image rm -f "$SMOKE_TAG" >/dev/null 2>&1 || true' EXIT
if ! docker info >/dev/null 2>&1; then
echo "::warning::docker daemon unreachable — skipping Docker build smoke"
exit 0
fi
if [ ! -s .runtime-version ]; then
echo "::error::.runtime-version is missing or empty"
exit 1
fi
EXPECTED_RUNTIME_VERSION="$(tr -d '[:space:]' < .runtime-version)"
# Redirect, do not pipe: `tail` must not be able to truncate the cause
# or own the exit status. On failure the WHOLE log is printed -- a fixed
# window is what made template-hermes#342 undiagnosable, and buildkit
# interleaves parallel stages so the failing layer can sit far from the end.
BUILD_LOG="$(mktemp)"
rc=0
docker build --build-arg RUNTIME_VERSION="$EXPECTED_RUNTIME_VERSION" -t "$SMOKE_TAG" . --no-cache >"$BUILD_LOG" 2>&1 || rc=$?
if [ "$rc" -ne 0 ]; then
echo "::group::image build output ($(wc -l <"$BUILD_LOG") lines, build failed)"
cat "$BUILD_LOG"
echo "::endgroup::"
echo "::error::image build failed (exit $rc)"
exit "$rc"
fi
tail -5 "$BUILD_LOG"
ACTUAL_RUNTIME_VERSION="$(
docker run --rm --entrypoint python3 "$SMOKE_TAG" \
-c 'from importlib.metadata import version; print(version("molecules-workspace-runtime"))'
)"
if [ "$ACTUAL_RUNTIME_VERSION" != "$EXPECTED_RUNTIME_VERSION" ]; then
echo "::error::image runtime $ACTUAL_RUNTIME_VERSION does not match .runtime-version $EXPECTED_RUNTIME_VERSION"
exit 1
fi
echo "Docker build succeeded with exact runtime $ACTUAL_RUNTIME_VERSION"
# --- Layer-3: real T4 tier-4 conformance gate (RFC internal#456 §11) ---
# NOT a string-match. Builds the actual image, runs it under the EXACT
# flags the controlplane provisioner emits for tier-4
# (userdata_containerized.go @ec2384c: --privileged --pid=host
# -v /:/host -v /var/run/docker.sock:/var/run/docker.sock), then
# asserts BOTH properties on the RUNNING container, atomically
# (RFC §10 — either failing fails the build):
# (a) the uid-1000 agent can attain host root
# (sudo nsenter --target 1 --mount --pid -- id -u == 0)
# (b) /configs/.auth_token is owned by uid 1000 (so the list_peers
# bearer token stays readable after the root->uid-1000 drop —
# this is the OpenClaw "list_peers works by accident (root==root)"
# regression class; the entrypoint chown is the fix).
# The flags are not hard-coded blind: they are the documented
# provisioner contract; drift is caught because the controlplane
# string-match unit test guards the emission side and this gate
# guards the runtime side.
# pc2-safe: false — hard-gate docker (fails closed if daemon unreachable);
# routes to docker-host only (task #390 B-lite).
t4-conformance:
name: T4 tier-4 conformance (live)
runs-on: [ubuntu-latest, docker-host]
timeout-minutes: 15
needs: validate-static
# Runs after static validation. Avoid job-level if: guards here so the
# protected context always reaches a terminal state.
env:
MOLECULE_CI_REF: 11b8598e5c0b3f0b1031733a8d5f6bc238f146a4
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Skip privileged conformance for external forks
if: github.event.pull_request.head.repo.fork == true
run: echo "::notice::privileged T4 validation is disabled for external forks"
- name: Prepare immutable management-MCP attestation
if: github.event.pull_request.head.repo.fork != true
run: |
set -euo pipefail
: "${RUNNER_TEMP:?RUNNER_TEMP is required}"
RUN_SUFFIX="${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}"
MOLECULE_CI_ROOT="$RUNNER_TEMP/molecule-ci-mcp-e2e-$RUN_SUFFIX"
MCP_ATTESTATION="$RUNNER_TEMP/mcp-lockstep-$RUN_SUFFIX.json"
MCP_ATTESTATION_TMP="${MCP_ATTESTATION}.tmp"
MCP_ATTESTATION_SHA256="${MCP_ATTESTATION}.sha256"
RUNTIME_VERSION_FILE="$RUNNER_TEMP/mcp-runtime-version-$RUN_SUFFIX"
mkdir "$MOLECULE_CI_ROOT"
git -C "$MOLECULE_CI_ROOT" init -q
git -C "$MOLECULE_CI_ROOT" remote add origin \
https://git.moleculesai.app/molecule-ai/molecule-ci.git
fetched=false
for attempt in 1 2 3; do
if GIT_ASKPASS=/bin/false GIT_TERMINAL_PROMPT=0 \
git -c credential.helper= -c http.userAgent=curl/8.4.0 \
-C "$MOLECULE_CI_ROOT" \
fetch --no-tags --depth 1 origin "$MOLECULE_CI_REF"; then
fetched=true
break
fi
sleep "$attempt"
done
if [ "$fetched" != true ]; then
echo "::error::immutable molecule-ci verifier fetch failed"
exit 1
fi
git -C "$MOLECULE_CI_ROOT" checkout -q --detach FETCH_HEAD
ACTUAL_CI_REF="$(git -C "$MOLECULE_CI_ROOT" rev-parse HEAD)"
if [ "$ACTUAL_CI_REF" != "$MOLECULE_CI_REF" ]; then
echo "::error::immutable molecule-ci verifier ref mismatch"
exit 1
fi
git -C "$MOLECULE_CI_ROOT" diff --quiet --no-ext-diff --no-textconv "$MOLECULE_CI_REF" -- scripts/mcp_pin_lockstep.py scripts/mcp_built_image_e2e.py
python3 "$MOLECULE_CI_ROOT/scripts/mcp_pin_lockstep.py" \
--repo-root . --json > "$MCP_ATTESTATION_TMP"
mv "$MCP_ATTESTATION_TMP" "$MCP_ATTESTATION"
sha256sum "$MCP_ATTESTATION" > "$MCP_ATTESTATION_SHA256"
git -C "$MOLECULE_CI_ROOT" diff --quiet --no-ext-diff --no-textconv "$MOLECULE_CI_REF" -- scripts/mcp_pin_lockstep.py scripts/mcp_built_image_e2e.py
sha256sum --check "$MCP_ATTESTATION_SHA256"
EXPECTED_RUNTIME_VERSION="$(python3 - "$MOLECULE_CI_ROOT" "$MCP_ATTESTATION" <<'PY'
import sys
from pathlib import Path
sys.path.insert(0, str(Path(sys.argv[1]) / "scripts"))
from mcp_built_image_e2e import load_attestation
with Path(sys.argv[2]).open("rb") as stream:
sys.stdout.write(load_attestation(stream).runtime_version)
PY
)"
printf '%s\n' "$EXPECTED_RUNTIME_VERSION" > "$RUNTIME_VERSION_FILE"
- name: Build the runtime image
id: build
if: github.event.pull_request.head.repo.fork != true
run: |
set -euo pipefail
: "${RUNNER_TEMP:?RUNNER_TEMP is required}"
RUN_SUFFIX="${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}"
MOLECULE_CI_ROOT="$RUNNER_TEMP/molecule-ci-mcp-e2e-$RUN_SUFFIX"
MCP_ATTESTATION="$RUNNER_TEMP/mcp-lockstep-$RUN_SUFFIX.json"
MCP_ATTESTATION_SHA256="${MCP_ATTESTATION}.sha256"
RUNTIME_VERSION_FILE="$RUNNER_TEMP/mcp-runtime-version-$RUN_SUFFIX"
T4_TAG="t4-conformance-test:${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}"
MCP_VERIFY_LOG="$RUNNER_TEMP/mcp-built-image-e2e-$RUN_SUFFIX.log"
MCP_VERIFY_CONTAINER="mcp-built-image-e2e-${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}"
KEEP_T4_IMAGE=0
cleanup_t4_build() {
docker rm -f "$MCP_VERIFY_CONTAINER" >/dev/null 2>&1 || true
rm -rf -- "$MOLECULE_CI_ROOT"
rm -f -- "$MCP_ATTESTATION" "$MCP_ATTESTATION_SHA256" "$RUNTIME_VERSION_FILE" "$MCP_VERIFY_LOG"
if [ "$KEEP_T4_IMAGE" -ne 1 ]; then
docker image rm -f "$T4_TAG" >/dev/null 2>&1 || true
fi
}
trap cleanup_t4_build EXIT
if ! docker info >/dev/null 2>&1; then
echo "::error::docker daemon unreachable — T4 conformance gate CANNOT verify host-root reach. This is a hard gate; failing closed (do NOT treat as skip). Fix runner-config (internal#222) to unblock."
exit 1
fi
if [ ! -s "$MCP_ATTESTATION" ] || [ ! -s "$RUNTIME_VERSION_FILE" ]; then
echo "::error::immutable management-MCP attestation is missing"
exit 1
fi
if [ ! -f "$MOLECULE_CI_ROOT/scripts/mcp_built_image_e2e.py" ]; then
echo "::error::immutable final-image verifier is missing"
exit 1
fi
EXPECTED_RUNTIME_VERSION="$(<"$RUNTIME_VERSION_FILE")"
DECLARED_RUNTIME_VERSION="$(tr -d '[:space:]' < .runtime-version)"
if [ "$EXPECTED_RUNTIME_VERSION" != "$DECLARED_RUNTIME_VERSION" ]; then
echo "::error::attested runtime version disagrees with .runtime-version"
exit 1
fi
# Redirect, do not pipe: `tail` must not be able to truncate the cause
# or own the exit status. On failure the WHOLE log is printed -- a fixed
# window is what made template-hermes#342 undiagnosable, and buildkit
# interleaves parallel stages so the failing layer can sit far from the end.
BUILD_LOG="$(mktemp)"
rc=0
docker build --build-arg RUNTIME_VERSION="$EXPECTED_RUNTIME_VERSION" -t "$T4_TAG" . --no-cache >"$BUILD_LOG" 2>&1 || rc=$?
if [ "$rc" -ne 0 ]; then
echo "::group::image build output ($(wc -l <"$BUILD_LOG") lines, build failed)"
cat "$BUILD_LOG"
echo "::endgroup::"
echo "::error::image build failed (exit $rc)"
exit "$rc"
fi
tail -5 "$BUILD_LOG"
ACTUAL_RUNTIME_VERSION="$(
docker run --rm --entrypoint python3 "$T4_TAG" \
-c 'from importlib.metadata import version; print(version("molecules-workspace-runtime"))'
)"
if [ "$ACTUAL_RUNTIME_VERSION" != "$EXPECTED_RUNTIME_VERSION" ]; then
echo "::error::T4 image runtime $ACTUAL_RUNTIME_VERSION does not match .runtime-version $EXPECTED_RUNTIME_VERSION"
exit 1
fi
rm -f "$MCP_VERIFY_LOG"
docker rm -f "$MCP_VERIFY_CONTAINER" >/dev/null 2>&1 || true
docker create --interactive --name "$MCP_VERIFY_CONTAINER" \
--network none \
--user 1000:1000 --workdir /tmp \
--cap-drop ALL --security-opt no-new-privileges \
--pids-limit 128 --memory 768m --cpus 1 \
--tmpfs /tmp:size=64m \
--entrypoint python3 "$T4_TAG" \
/mcp_built_image_e2e.py >/dev/null
git -C "$MOLECULE_CI_ROOT" diff --quiet --no-ext-diff --no-textconv "$MOLECULE_CI_REF" -- scripts/mcp_pin_lockstep.py scripts/mcp_built_image_e2e.py
docker cp "$MOLECULE_CI_ROOT/scripts/mcp_built_image_e2e.py" \
"$MCP_VERIFY_CONTAINER:/mcp_built_image_e2e.py"
sha256sum --check "$MCP_ATTESTATION_SHA256"
docker start --attach --interactive "$MCP_VERIFY_CONTAINER" \
< "$MCP_ATTESTATION" | tee "$MCP_VERIFY_LOG"
docker rm "$MCP_VERIFY_CONTAINER" >/dev/null
if ! grep -qxF 'mcp-built-image-e2e:sentinel:executed' "$MCP_VERIFY_LOG"; then
echo "::error::final-image management-MCP verifier sentinel is missing"
exit 1
fi
KEEP_T4_IMAGE=1
- name: Run under EXACT tier-4 provisioner flags + assert host-root reach AND token agent-ownership
if: github.event.pull_request.head.repo.fork != true
env:
T4_PROBE_NAME: "t4probe-${{ github.run_id }}-${{ github.run_attempt }}"
MOLECULE_T4_PROBE_ID: "${{ github.run_id }}-${{ github.run_attempt }}"
run: |
set -euo pipefail
T4_TAG="t4-conformance-test:${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}"
T4_PROBE="$T4_PROBE_NAME"
# EXACT flags from controlplane userdata_containerized.go
# (tier-4 emission @ec2384c). The molecule-runtime entrypoint
# wants a live workspace; we only need the container up long
# enough to probe, so override the command with a sleep and
# exercise the agent context directly.
trap 'docker rm -f "$T4_PROBE" >/dev/null 2>&1 || true; docker rmi -f "$T4_TAG" >/dev/null 2>&1 || true' EXIT
docker rm -f "$T4_PROBE" >/dev/null 2>&1 || true
docker run -d \
--name "$T4_PROBE" \
--network host \
--privileged \
--pid=host \
-v /:/host \
-v /var/run/docker.sock:/var/run/docker.sock \
--entrypoint /bin/sh \
"$T4_TAG" -c 'sleep 600' >/dev/null
echo "=== Reproduce the agent-owned-token half of the entrypoint contract ==="
# The real entrypoint chowns /configs to agent before gosu;
# /configs is an unmounted VOLUME in this probe, so reproduce
# the exact contract step the entrypoint performs, then assert.
docker exec "$T4_PROBE" sh -c 'mkdir -p /configs && touch /configs/.auth_token && chown -R agent:agent /configs'
echo "=== (b) token agent-ownership: stat /configs/.auth_token ==="
OWNER_UID=$(docker exec "$T4_PROBE" stat -c '%u' /configs/.auth_token)
echo "owner_uid=$OWNER_UID"
if [ "$OWNER_UID" != "1000" ]; then
echo "::error::T4 contract violated: /configs/.auth_token owner_uid=$OWNER_UID (expected 1000). Escalation leg must NOT regress agent-readable token (RFC internal#456 §10, OpenClaw list_peers root==root accident / Hermes list_peers-401 class)."
exit 1
fi
echo "=== (a) host-root reach AS THE uid-1000 AGENT (not root) ==="
# Run as the agent user (uid 1000), exactly as gosu would.
AGENT_HOSTROOT_UID=$(docker exec -u agent "$T4_PROBE" sudo -n nsenter --target 1 --mount --pid -- id -u)
echo "agent->host-root id -u = $AGENT_HOSTROOT_UID"
if [ "$AGENT_HOSTROOT_UID" != "0" ]; then
echo "::error::T4 contract violated: uid-1000 agent could NOT attain host root via 'sudo nsenter --target 1' (got uid=$AGENT_HOSTROOT_UID). T4 escalation leg ABSENT/broken."
exit 1
fi
# Defense-in-depth: host-filesystem write+readback through /host
# from the agent, proving real host reach (not just a namespace
# trick on an isolated PID 1).
MARKER="t4-conformance-$(date +%s)-$RANDOM"
PROBE_FILE="/host/tmp/.t4-conformance-probe-${MOLECULE_T4_PROBE_ID}"
docker exec -u agent "$T4_PROBE" sudo -n sh -c "echo $MARKER > $PROBE_FILE"
READBACK=$(docker exec -u agent "$T4_PROBE" sudo -n cat "$PROBE_FILE")
docker exec -u agent "$T4_PROBE" sudo -n rm -f "$PROBE_FILE"
if [ "$READBACK" != "$MARKER" ]; then
echo "::error::T4 host-fs write+readback through /host failed (got '$READBACK' expected '$MARKER')."
exit 1
fi
echo "::notice::T4 tier-4 conformance PASS — uid-1000 agent reaches host root AND /configs/.auth_token is agent-owned (both, atomically)."
tests:
name: Adapter unit tests
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Skip executable tests for external forks
if: github.event.pull_request.head.repo.fork == true
run: echo "::notice::dependency installation and repository tests are disabled for external forks"
- name: Fetch immutable molecule-ci canonical scripts
if: github.event.pull_request.head.repo.fork != true
run: |
set -euo pipefail
CI_ROOT=".molecule-ci-canonical"
mkdir "$CI_ROOT"
git -C "$CI_ROOT" init -q
git -C "$CI_ROOT" remote add origin https://git.moleculesai.app/molecule-ai/molecule-ci.git
fetched=false
for attempt in 1 2 3; do
if GIT_ASKPASS=/bin/false GIT_TERMINAL_PROMPT=0 git -c credential.helper= -c http.userAgent=curl/8.4.0 -C "$CI_ROOT" fetch --no-tags --depth 1 origin "$MOLECULE_CI_REF"; then
fetched=true
break
fi
sleep "$attempt"
done
if [ "$fetched" != true ]; then
echo "::error::immutable molecule-ci validator fetch failed"
exit 1
fi
git -C "$CI_ROOT" checkout -q --detach FETCH_HEAD
ACTUAL_CI_REF="$(git -C "$CI_ROOT" rev-parse HEAD)"
if [ "$ACTUAL_CI_REF" != "$MOLECULE_CI_REF" ]; then
echo "::error::immutable molecule-ci validator ref mismatch"
exit 1
fi
# molecule-ai-sdk (molecule_plugin) is the ADR-004 conformance-suite home
# (tests/test_conformance.py inherits AdapterConformance FROM the SDK), so
# it is a test-time dependency alongside the runtime engine. Public test
# tooling is resolved separately, the runtime comes only through the
# canonical private-source installer, and the SDK is content-addressed to
# the reviewed SSOT commit rather than selected from a package index.
- name: Install public test tooling
if: github.event.pull_request.head.repo.fork != true
run: python3 -m pip install --isolated --break-system-packages -q pytest pytest-asyncio pyyaml python-multipart jsonschema packaging
- name: Install source-pinned runtime and template dependencies
if: github.event.pull_request.head.repo.fork != true
run: python3 .molecule-ci-canonical/scripts/install_workspace_dependencies.py --allow-missing --break-system-packages
- name: Install SDK SSOT at reviewed commit
if: github.event.pull_request.head.repo.fork != true
run: python3 -m pip install --isolated --break-system-packages -q "molecule-ai-sdk @ git+https://git.moleculesai.app/molecule-ai/molecule-ai-sdk.git@da42c7f2dae122aaa6f34a74c13e598a87870586"
# Concierge MCP loaded+callable e2e (fail-before/pass-after) — the
# strengthened guard for the mcpServers:NONE / declared-but-dead class of
# regression the mocked test_openclaw_mgmt_mcp.py could not catch. The
# LOGIC layer stands up a REAL stdio MCP server and asserts the required
# privileged tool set (provision_workspace, commit_memory, ...) is actually
# advertised over the wire. The LIVE layer (docker exec into a real
# concierge) auto-skips here and is driven by the deploy gate via
# MOLECULE_LIVE_CONCIERGE_CONTAINER.
- name: Concierge MCP end-to-end tests
if: github.event.pull_request.head.repo.fork != true
run: python3 -m pytest tests/test_openclaw_concierge_mcp_live_e2e.py -v
- name: Repository unit tests
if: github.event.pull_request.head.repo.fork != true
run: python3 -m pytest tests/ -v
# GUARD D (per-template pin<->bake lockstep) REMOVED: the mcp-server bake is now
# the base-runtime SSOT helper (molecule_runtime/scripts/prebake-mgmt-mcp.sh) and
# the pin lives ONCE in the runtime, lockstep-tested against the SDK contract
# (test_mcp_plugin_delivery_contract.py). This template carries no ARG/bake to
# drift, so the per-template gate is obsolete (#54).
validate:
name: validate
runs-on: ubuntu-latest
needs: [validate-static, validate-runtime, t4-conformance, tests]
if: always()
timeout-minutes: 1
steps:
- name: Aggregate
run: |
static="${{ needs.validate-static.result }}"
runtime="${{ needs.validate-runtime.result }}"
t4="${{ needs.t4-conformance.result }}"
tests="${{ needs.tests.result }}"
echo "validate-static: $static"
echo "validate-runtime: $runtime"
echo "t4-conformance: $t4"
echo "tests: $tests"
if [ "$static" != "success" ]; then
echo "::error::validate-static did not succeed: $static"
exit 1
fi
if [ "$runtime" != "success" ] && [ "$runtime" != "skipped" ]; then
echo "::error::validate-runtime did not succeed: $runtime"
exit 1
fi
# T4 conformance is a HARD gate on internal (non-fork) PRs
# and main pushes. `skipped` is only acceptable on fork PRs.
# Any other non-success fails the build: "verified" T4 requires
# this live gate green, never inference.
is_fork_pr="${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}"
if [ "$t4" != "success" ]; then
if [ "$t4" = "skipped" ] && [ "$is_fork_pr" = "true" ]; then
echo "::notice::t4-conformance skipped on fork PR — allowing aggregate to pass."
else
echo "::error::t4-conformance did not succeed: $t4 — T4 host-root reach / token-ownership not verified on a live container. Failing closed (RFC internal#456 §11)."
exit 1
fi
fi
if [ "$tests" != "success" ]; then
echo "::error::tests did not succeed: $tests"
exit 1
fi
echo "::notice::Template validation aggregate passed (static=$static, runtime=$runtime, t4=$t4, tests=$tests)"