0a05b27aaa
publish-image / Resolve runtime version (push) Successful in 3s
CI / Template validation (static) (push) Successful in 6s
Secret scan / secret-scan (push) Successful in 12s
verify-providers-projection / Regenerate projection, fail on drift, assert registry ⊆ template (push) Successful in 25s
CI / Adapter unit tests (push) Successful in 36s
CI / Template validation (runtime) (push) Successful in 1m31s
CI / T4 tier-4 conformance (live) (push) Successful in 1m32s
CI / validate (push) Successful in 2s
publish-image / Build & push workspace-template-openclaw image (push) Failing after 11s
publish-image / Promote runtime_image_pins (CP admin) (staging-api.moleculesai.app, staging) (push) Has been skipped
Co-authored-by: hongming-ceo-delegated <ceo-delegated-bot@moleculesai.app> Co-committed-by: hongming-ceo-delegated <ceo-delegated-bot@moleculesai.app>
314 lines
16 KiB
YAML
314 lines
16 KiB
YAML
name: CI
|
|
|
|
# Ported from .github/workflows/ci.yml per feedback_gitea_cross_repo_uses_blocked.
|
|
# The original `uses: Molecule-AI/molecule-ci/.github/...` reference hits the
|
|
# suspended GitHub org (2026-05-06) and fails in 1s. Inlined here per the
|
|
# hermes migration pattern (molecule-ai-workspace-template-hermes).
|
|
#
|
|
# Gitea 1.22.6 hostile-shape checklist applied:
|
|
# - 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
|
|
# HERMETIC dep policy (CI-robustness): do NOT set PIP_INDEX_URL=private —
|
|
# it force-routes EVERY pip install (pytest, pyyaml, jsonschema, …) at the
|
|
# private index, which serves only molecules-workspace-runtime → 404s all
|
|
# public tooling (the Adapter-unit-tests / Template-validation reds). Public
|
|
# tooling resolves from the default (PyPI) index; the private runtime — which
|
|
# is ABSENT from public PyPI, so no dependency-confusion — is pulled via an
|
|
# EXPLICIT --extra-index-url on the runtime install only. The legacy name
|
|
# molecule-ai-workspace-runtime IS squatted on public PyPI (0.1.999999); we
|
|
# install the CURRENT dist name molecules-workspace-runtime (private-only).
|
|
MOLECULE_PRIVATE_INDEX: https://git.moleculesai.app/api/packages/molecule-ai/pypi/simple/
|
|
|
|
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
|
|
- name: Fetch molecule-ci canonical scripts
|
|
run: git clone --depth 1 https://git.moleculesai.app/molecule-ai/molecule-ci.git .molecule-ci-canonical
|
|
- 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', '.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 -q
|
|
- run: python3 .molecule-ci-canonical/scripts/validate-workspace-template.py --static-only
|
|
|
|
# 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 operator-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
|
|
- name: Fetch molecule-ci canonical scripts
|
|
run: git clone --depth 1 https://git.moleculesai.app/molecule-ai/molecule-ci.git .molecule-ci-canonical
|
|
- run: pip install pyyaml -q
|
|
- if: hashFiles('requirements.txt') != ''
|
|
run: pip install -q --extra-index-url "$MOLECULE_PRIVATE_INDEX" -r requirements.txt
|
|
- if: hashFiles('requirements.txt') == ''
|
|
run: pip install -q --extra-index-url "$MOLECULE_PRIVATE_INDEX" molecules-workspace-runtime
|
|
- run: python3 .molecule-ci-canonical/scripts/validate-workspace-template.py
|
|
- name: Adapter platform-routing unit tests
|
|
# Scoped to the platform-routing test this PR adds. The rest of tests/
|
|
# (test_model_fallback, test_session_id_derivation) is NOT wired to CI
|
|
# yet and has pre-existing failures against the published runtime —
|
|
# fixing/enabling those is a separate cleanup, out of scope here.
|
|
run: |
|
|
pip install -q pytest
|
|
python3 -m pytest tests/test_platform_routing.py -q
|
|
- name: Docker build smoke test
|
|
if: hashFiles('Dockerfile') != ''
|
|
run: |
|
|
if ! docker info >/dev/null 2>&1; then
|
|
echo "::warning::docker daemon unreachable — skipping Docker build smoke"
|
|
exit 0
|
|
fi
|
|
docker build -t template-test . --no-cache 2>&1 | tail -5 && echo "Docker build succeeded"
|
|
|
|
# --- 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 because
|
|
# Gitea 1.22.6 can leave the protected context pending instead of skipped.
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
- name: Build the runtime image
|
|
id: build
|
|
run: |
|
|
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
|
|
T4_TAG="t4-conformance-test:${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}"
|
|
docker build -t "$T4_TAG" . --no-cache 2>&1 | tail -5
|
|
- name: Run under EXACT tier-4 provisioner flags + assert host-root reach AND token agent-ownership
|
|
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.
|
|
docker rm -f "$T4_PROBE" >/dev/null 2>&1 || true
|
|
CID=$(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')
|
|
trap 'docker rm -f "$T4_PROBE" >/dev/null 2>&1 || true; docker rmi -f "$T4_TAG" >/dev/null 2>&1 || true' EXIT
|
|
|
|
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
|
|
# 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 dep alongside the runtime engine. Both live in the
|
|
# private registry; jsonschema is pulled because the SDK's [test] extra
|
|
# (adapter_conformance's transitive deps) expects it.
|
|
- run: pip install -q pytest pytest-asyncio pyyaml python-multipart jsonschema && pip install -q --extra-index-url "$MOLECULE_PRIVATE_INDEX" molecules-workspace-runtime molecule-ai-sdk
|
|
# 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.
|
|
- run: python3 -m pytest tests/test_openclaw_concierge_mcp_live_e2e.py -v
|
|
- 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]
|
|
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)"
|