Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
033febe
fix(messaging): lock WeChat runtime graph
HOYALIM Jul 13, 2026
c233f51
fix(build): stage WeChat runtime lock
ericksoa Jul 13, 2026
d19deb6
Merge branch 'main' into codex/issue-5896-wechat-lock
ericksoa Jul 13, 2026
5adda46
test(build): verify runtime manifest staging
ericksoa Jul 13, 2026
399a314
Merge branch 'main' into codex/issue-5896-wechat-lock
ericksoa Jul 13, 2026
1525f89
fix(messaging): harden WeChat locked install
ericksoa Jul 13, 2026
2270db9
docs(security): record WeChat audit gate
ericksoa Jul 13, 2026
5625e0b
test(build): cover writable WeChat cache layer
ericksoa Jul 13, 2026
f07d3ad
Merge branch 'main' into codex/issue-5896-wechat-lock
ericksoa Jul 13, 2026
850bc79
Merge branch 'main' into codex/issue-5896-wechat-lock
ericksoa Jul 13, 2026
485b3c6
ci(security): add trusted WeChat audit action
ericksoa Jul 13, 2026
c17fd35
fix(ci): harden WeChat audit gate
ericksoa Jul 13, 2026
0377c39
docs(security): clarify trusted WeChat audit
ericksoa Jul 13, 2026
8acc869
fix(openclaw): isolate WeChat archive retrieval
ericksoa Jul 13, 2026
e5285cd
fix(ci): pin trusted npm registry for WeChat audit
HOYALIM Jul 13, 2026
7e7b50d
ci(security): advance trusted WeChat audit pin
ericksoa Jul 13, 2026
0d2256d
ci(security): fail closed on WeChat metadata validation
ericksoa Jul 13, 2026
76c9636
Merge main into codex/issue-5896-wechat-lock
ericksoa Jul 13, 2026
e1d056d
ci(security): execute fail-closed WeChat audit
ericksoa Jul 13, 2026
7bb99bb
Merge main into codex/issue-5896-wechat-lock
ericksoa Jul 13, 2026
869dc33
Merge main into codex/issue-5896-wechat-lock
ericksoa Jul 13, 2026
455b63f
fix(ci): verify Ubuntu fdfind binary name
ericksoa Jul 13, 2026
818febd
fix(ci): install advisor yaml dependency
ericksoa Jul 13, 2026
b7a19aa
fix(ci): install advisor vitest runtime
ericksoa Jul 13, 2026
81125fd
fix(messaging): remove stale archive imports
ericksoa Jul 13, 2026
5521f8f
test(packaging): align reviewed archive contracts
ericksoa Jul 13, 2026
03286ed
test(packaging): keep optional plugin fixture linear
ericksoa Jul 13, 2026
e93923b
Merge remote-tracking branch 'origin/main' into review/pr-6739
ericksoa Jul 13, 2026
3338232
Merge remote-tracking branch 'origin/main' into review/pr-6739
ericksoa Jul 13, 2026
fbf969e
fix(messaging): canonicalize WeChat cache boundary
ericksoa Jul 13, 2026
c160492
Merge remote-tracking branch 'origin/main' into review/pr-6739
ericksoa Jul 13, 2026
fa09c85
test(messaging): remove stale cache fixture
ericksoa Jul 13, 2026
a0f2a43
Merge remote-tracking branch 'origin/main' into review/pr-6739
ericksoa Jul 13, 2026
e70c922
docs(messaging): define audit bootstrap retirement
ericksoa Jul 13, 2026
6a15263
fix(messaging): verify final WeChat cache graph
ericksoa Jul 13, 2026
bad41be
Merge remote-tracking branch 'origin/main' into review/pr-6739
ericksoa Jul 13, 2026
721a2a9
test(messaging): keep cache proof branch-free
ericksoa Jul 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/actions/ci-wechat-runtime-audit/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

name: ci-wechat-runtime-audit
description: Audit a PR-provided WeChat runtime graph from trusted CI code.

inputs:
target-root:
description: Absolute path to the checked-out target tree whose lock inputs are audited.
required: true
report-dir:
description: Report directory, absolute or relative to target-root.
required: true

runs:
using: composite
steps:
- name: Setup production-compatible Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: "22.19.0"

- name: Pin production npm
shell: bash
run: >-
cd "$RUNNER_TEMP" &&
npm install --global npm@10.9.4
--userconfig /dev/null
--registry https://registry.npmjs.org/
--ignore-scripts --no-audit --no-fund

- name: Audit locked WeChat runtime graph
shell: bash
env:
NEMOCLAW_WECHAT_AUDIT_TARGET_ROOT: ${{ inputs.target-root }}
NEMOCLAW_WECHAT_AUDIT_REPORT_DIR: ${{ inputs.report-dir }}
run: bash "$GITHUB_ACTION_PATH/audit.sh"
213 changes: 213 additions & 0 deletions .github/actions/ci-wechat-runtime-audit/audit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

set -euo pipefail

target_root="${NEMOCLAW_WECHAT_AUDIT_TARGET_ROOT:?target root is required}"
report_dir="${NEMOCLAW_WECHAT_AUDIT_REPORT_DIR:?report directory is required}"
target_root="$(cd "$target_root" && pwd -P)"
if [[ "$report_dir" != /* ]]; then
report_dir="$target_root/$report_dir"
fi
runtime_dir="$target_root/agents/openclaw/wechat-runtime"
package_json="$runtime_dir/package.json"
package_lock="$runtime_dir/package-lock.json"
npm_registry="https://registry.npmjs.org/"

for input in "$runtime_dir" "$package_json" "$package_lock"; do
if [[ -L "$input" ]]; then
echo "WeChat runtime audit refuses symbolic-link input: $input" >&2
exit 1
fi
done

# The audited checkout must not influence npm's trust or registry configuration.
for npm_config in "$target_root/.npmrc" "$runtime_dir/.npmrc"; do
if [[ -e "$npm_config" || -L "$npm_config" ]]; then
echo "WeChat runtime audit refuses target-controlled npm config: $npm_config" >&2
exit 1
fi
done
runtime_dir="$(cd "$runtime_dir" && pwd -P)"
case "$runtime_dir/" in
"$target_root"/*) ;;
*)
echo "WeChat runtime directory escaped the target checkout: $runtime_dir" >&2
exit 1
;;
esac

trusted_cache="$(mktemp -d "${TMPDIR:-/tmp}/nemoclaw-wechat-trusted-cache.XXXXXX")"
install_cache="$(mktemp -d "${TMPDIR:-/tmp}/nemoclaw-wechat-install-cache.XXXXXX")"
pack_dir="$(mktemp -d "${TMPDIR:-/tmp}/nemoclaw-wechat-pack.XXXXXX")"
trusted_npmrc="$(mktemp "${TMPDIR:-/tmp}/nemoclaw-wechat-npmrc.XXXXXX")"
printf 'registry=%s\n' "$npm_registry" >"$trusted_npmrc"
chmod 600 "$trusted_npmrc"

cleanup() {
chmod -R u+w "$trusted_cache" "$install_cache" "$pack_dir" 2>/dev/null || true
rm -rf "$trusted_cache" "$install_cache" "$pack_dir" "$runtime_dir/node_modules"
rm -f "$trusted_npmrc"
}
trap cleanup EXIT

mkdir -p "$report_dir"

package_identity_output="$(
PACKAGE_JSON="$package_json" PACKAGE_LOCK="$package_lock" NPM_REGISTRY_ORIGIN="${npm_registry%/}" node <<'NODE'
const fs = require("node:fs");
const packageJson = JSON.parse(fs.readFileSync(process.env.PACKAGE_JSON, "utf8"));
const packageLock = JSON.parse(fs.readFileSync(process.env.PACKAGE_LOCK, "utf8"));
const registryOrigin = process.env.NPM_REGISTRY_ORIGIN;

function requireRegistryUrl(resolved, location) {
if (typeof resolved !== "string") {
throw new Error(`locked package lacks a resolved URL: ${location}`);
}
let parsed;
try {
parsed = new URL(resolved);
} catch {
throw new Error(`locked package has an invalid resolved URL: ${location}`);
}
if (parsed.origin !== registryOrigin || parsed.username || parsed.password) {
throw new Error(`locked package must resolve from the reviewed npm registry origin: ${location}`);
}
}

const dependencyNames = Object.keys(packageJson.dependencies ?? {});
if (dependencyNames.length !== 1 || dependencyNames[0] !== "@tencent-weixin/openclaw-weixin") {
throw new Error("WeChat runtime package.json must contain exactly the reviewed plugin dependency");
}
const version = packageJson.dependencies[dependencyNames[0]];
if (typeof version !== "string" || !/^\d+\.\d+\.\d+$/.test(version)) {
throw new Error("WeChat runtime dependency must use an exact numeric version");
}
if (packageLock.lockfileVersion !== 3) {
throw new Error(`WeChat runtime lockfileVersion must be 3, got ${packageLock.lockfileVersion}`);
}
const rootDependency = packageLock.packages?.[""]?.dependencies?.[dependencyNames[0]];
const plugin = packageLock.packages?.[`node_modules/${dependencyNames[0]}`];
if (rootDependency !== version || plugin?.version !== version) {
throw new Error("WeChat runtime package and lock identities do not match");
}
if (typeof plugin.integrity !== "string" || !plugin.integrity.startsWith("sha512-")) {
throw new Error("WeChat runtime plugin lock entry must carry sha512 integrity");
}
requireRegistryUrl(plugin.resolved, `node_modules/${dependencyNames[0]}`);
const peerRange = plugin.peerDependencies?.openclaw;
if (typeof peerRange !== "string" || peerRange.length === 0) {
throw new Error("WeChat runtime plugin lock entry must declare its OpenClaw peer range");
}
for (const [location, record] of Object.entries(packageLock.packages ?? {})) {
if (!location.startsWith("node_modules/")) continue;
if (typeof record.version !== "string" || typeof record.integrity !== "string") {
throw new Error(`locked package lacks version or integrity: ${location}`);
}
requireRegistryUrl(record.resolved, location);
}
process.stdout.write(`${dependencyNames[0]}@${version}\n${plugin.resolved}\n${plugin.integrity}\n`);
NODE
)"
readarray -t package_identity <<<"$package_identity_output"
if [[ ${#package_identity[@]} -ne 3 ]] \
|| [[ -z "${package_identity[0]}" || -z "${package_identity[1]}" || -z "${package_identity[2]}" ]]; then
echo "ERROR: WeChat runtime package validation returned incomplete identity metadata" >&2
exit 1
fi
wechat_spec="${package_identity[0]}"
wechat_tarball="${package_identity[1]}"
wechat_integrity="${package_identity[2]}"

# Materialize the exact PR-provided graph without executing dependency scripts.
npm --prefix "$runtime_dir" ci \
--userconfig "$trusted_npmrc" \
--registry "$npm_registry" \
--ignore-scripts \
--omit=dev \
--legacy-peer-deps \
--no-audit \
--no-fund \
--cache "$trusted_cache"
for package_spec in "$wechat_spec" "qrcode-terminal@0.12.0" "zod@4.4.3"; do
npm cache add "$package_spec" \
--userconfig "$trusted_npmrc" \
--registry "$npm_registry" \
--cache "$trusted_cache"
done

audit_status=0
npm --prefix "$runtime_dir" audit \
--userconfig "$trusted_npmrc" \
--registry "$npm_registry" \
--omit=dev \
--audit-level=low \
--json >"$report_dir/npm-audit.json" || audit_status=$?

signature_status=0
npm --prefix "$runtime_dir" audit signatures \
--userconfig "$trusted_npmrc" \
--registry "$npm_registry" \
>"$report_dir/npm-audit-signatures.txt" 2>&1 || signature_status=$?

# Reproduce the sandbox-user npm-pack boundary with the exact reviewed archive.
# The trusted source cache is read-only; only its short-lived copy is writable.
chmod -R a-w "$trusted_cache"
cp -R "$trusted_cache"/. "$install_cache"/
chmod -R u+rwX,go-w "$install_cache"
npm pack "$wechat_tarball" \
--userconfig "$trusted_npmrc" \
--registry "$npm_registry" \
--offline \
--cache "$install_cache" \
--pack-destination "$pack_dir" \
--json >"$report_dir/npm-pack.json"

TRUSTED_CACHE="$trusted_cache" \
PACK_DIR="$pack_dir" \
PACK_REPORT="$report_dir/npm-pack.json" \
EXPECTED_INTEGRITY="$wechat_integrity" \
node <<'NODE'
const fs = require("node:fs");
const path = require("node:path");

const trustedCache = process.env.TRUSTED_CACHE;
const packDir = path.resolve(process.env.PACK_DIR);
const report = JSON.parse(fs.readFileSync(process.env.PACK_REPORT, "utf8"));
const entry = report[0] ?? {};
if (entry.integrity !== process.env.EXPECTED_INTEGRITY) {
throw new Error(`reviewed WeChat archive integrity mismatch: ${entry.integrity ?? "missing"}`);
}
const filename = String(entry.filename ?? "");
if (!filename || path.basename(filename) !== filename) {
throw new Error(`npm pack reported an unsafe filename: ${filename || "missing"}`);
}
const archive = path.resolve(packDir, filename);
if (!archive.startsWith(`${packDir}${path.sep}`) || !fs.statSync(archive).isFile()) {
throw new Error(`npm pack archive escaped its destination: ${filename}`);
}

const pending = [trustedCache];
while (pending.length > 0) {
const current = pending.pop();
const stats = fs.lstatSync(current);
if ((stats.mode & 0o222) !== 0) {
throw new Error(`trusted WeChat cache entry remained writable: ${current}`);
}
if (stats.isDirectory()) {
for (const child of fs.readdirSync(current)) pending.push(path.join(current, child));
}
}
NODE

if ((audit_status != 0)); then
echo "WeChat runtime npm audit failed at audit-level=low; see $report_dir/npm-audit.json" >&2
exit "$audit_status"
fi
if ((signature_status != 0)); then
echo "WeChat runtime npm signature audit failed; see $report_dir/npm-audit-signatures.txt" >&2
exit "$signature_status"
fi

echo "WeChat runtime graph, audit, signatures, and writable install-cache boundary passed."
27 changes: 27 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,30 @@ jobs:
- name: Run installer integration tests
uses: ./.github/actions/ci-installer-integration

wechat-runtime-audit:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Audit locked WeChat runtime graph
uses: ./.github/actions/ci-wechat-runtime-audit
with:
target-root: ${{ github.workspace }}
report-dir: artifacts/wechat-runtime-audit

- name: Upload WeChat runtime audit evidence
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: wechat-runtime-audit-${{ github.run_id }}-${{ github.run_attempt }}
path: artifacts/wechat-runtime-audit
if-no-files-found: error
retention-days: 14

reviewed-npm-audit:
runs-on: ubuntu-latest
timeout-minutes: 15
Expand Down Expand Up @@ -225,6 +249,7 @@ jobs:
- static-checks
- build-typecheck
- installer-integration
- wechat-runtime-audit
- reviewed-npm-audit
- real-openclaw-dist-harness
- cli-tests
Expand All @@ -240,6 +265,7 @@ jobs:
STATIC_RESULT: ${{ needs['static-checks'].result }}
BUILD_TYPECHECK_RESULT: ${{ needs['build-typecheck'].result }}
INSTALLER_INTEGRATION_RESULT: ${{ needs['installer-integration'].result }}
WECHAT_RUNTIME_AUDIT_RESULT: ${{ needs['wechat-runtime-audit'].result }}
REVIEWED_NPM_AUDIT_RESULT: ${{ needs['reviewed-npm-audit'].result }}
REAL_OPENCLAW_DIST_HARNESS_RESULT: ${{ needs['real-openclaw-dist-harness'].result }}
CLI_TESTS_RESULT: ${{ needs['cli-tests'].result }}
Expand All @@ -261,6 +287,7 @@ jobs:
require_success "static-checks" "$STATIC_RESULT"
require_success "build-typecheck" "$BUILD_TYPECHECK_RESULT"
require_success "installer-integration" "$INSTALLER_INTEGRATION_RESULT"
require_success "wechat-runtime-audit" "$WECHAT_RUNTIME_AUDIT_RESULT"
require_success "reviewed-npm-audit" "$REVIEWED_NPM_AUDIT_RESULT"
require_success "real-openclaw-dist-harness" "$REAL_OPENCLAW_DIST_HARNESS_RESULT"
require_success "cli-tests" "$CLI_TESTS_RESULT"
Expand Down
Loading
Loading