Repository navigation
docs(npm): ship real READMEs for core + skill-nanoclaw, fix client qu… #1588
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| branches: | |
| - main | |
| jobs: | |
| # Path filter — lets the heavy JS workspace jobs (client, mcp, app) run ONLY | |
| # when their own code changes. Keeps the onnxruntime-fetch-prone client/mcp | |
| # jobs off unrelated PRs (docs, contracts, relay, python) entirely, so a | |
| # transient onnxruntime/nuget blip can't stall an unrelated PR's auto-merge. | |
| # Skipped jobs report a "skipped" conclusion, which poseidon/wait-for-status-checks | |
| # (auto-merge-l1.yml) treats as complete — so this does not block merges. | |
| changes: | |
| name: Detect changed areas | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| # paths-filter lists a PR's files through the API; the graphprotocol org's | |
| # default token is contents-only, so the job fails without this. | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| outputs: | |
| client: ${{ steps.filter.outputs.client }} | |
| mcp: ${{ steps.filter.outputs.mcp }} | |
| app: ${{ steps.filter.outputs.app }} | |
| plugin: ${{ steps.filter.outputs.plugin }} | |
| docs: ${{ steps.filter.outputs.docs }} | |
| enclave: ${{ steps.filter.outputs.enclave }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dorny/paths-filter@v3 | |
| id: filter | |
| with: | |
| filters: | | |
| client: | |
| - 'client/**' | |
| mcp: | |
| - 'mcp/**' | |
| app: | |
| - 'app/**' | |
| plugin: | |
| - 'skill/**' | |
| docs: | |
| - '**/*.md' | |
| - 'skill/plugin/skill.json' | |
| - 'scripts/check-docs.sh' | |
| # The enclave imports the in-repo Python client and core, so | |
| # changes there must re-run its tests too. | |
| enclave: | |
| - 'enclave/**' | |
| - 'python/**' | |
| - 'rust/totalreclaw-core/**' | |
| - '.github/workflows/ci.yml' | |
| docs-check: | |
| # Guards visitor-facing docs against retired-product drift: retired | |
| # tokens (Base Sepolia-as-current, deleted tools, "unlimited" tiers, | |
| # removed env vars, unfilled placeholders) + broken/root-absolute | |
| # relative links. The 2026-07-15 docs audit found ~40 instances this | |
| # would have caught at PR time. See scripts/check-docs.sh. | |
| name: Docs Check | |
| needs: changes | |
| if: ${{ needs.changes.outputs.docs == 'true' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Run docs guardrail (retired tokens + link check) | |
| run: bash scripts/check-docs.sh | |
| phrase-safety: | |
| # Static guard against accidental "display the recovery phrase" copy | |
| # leaking into a SKILL.md / README / docs page. Caught regressing | |
| # twice (rc.20 doc rewrite + an earlier slip); fast-fails the PR if | |
| # any human-facing artifact contains a phrase-display pattern that | |
| # isn't paired with a safety qualifier ("NEVER display", "do not | |
| # show", etc) on the same line. | |
| # | |
| # The dist scan (added 2026-04-26 with the @totalreclaw/mcp-server@3.2.1 | |
| # SECURITY hotfix) is the source-code companion: it scans the COMPILED | |
| # JavaScript shipped to npm for any tool-response payload that emits | |
| # `recovery_phrase` or `mnemonic` as an object key. The deleted | |
| # `totalreclaw_setup` MCP tool used to do exactly that. | |
| # | |
| # See: scripts/check-phrase-safety.sh + scripts/check-phrase-safety-dist.sh | |
| # + tests/phrase-safety/. | |
| name: Phrase Safety | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: '22' | |
| - name: Run phrase-safety guard (markdown / SKILL.md) | |
| run: bash scripts/check-phrase-safety.sh | |
| - name: Run phrase-safety regression suite | |
| run: bash tests/phrase-safety/run-regression.sh | |
| - name: Build mcp/dist (required for dist scan) | |
| run: cd mcp && rm -f package-lock.json && npm install --legacy-peer-deps && npm run build | |
| - name: Run phrase-safety dist scan (compiled JS payloads) | |
| run: bash scripts/check-phrase-safety-dist.sh | |
| client-tests: | |
| name: Client Tests (TypeScript) | |
| needs: changes | |
| if: ${{ needs.changes.outputs.client == 'true' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| strategy: | |
| # The crypto/fingerprint golden-vector suites run production KDF params | |
| # (~11 min locally, slower on CI runners) — shard so each job fits. | |
| matrix: | |
| shard: ["1/2", "2/2"] | |
| # Skip the onnxruntime-node CUDA/GPU binary fetch from api.nuget.org — it | |
| # hung CI 25-30 min repeatedly (ETIMEDOUT) and blocked auto-merge on PRs | |
| # that didn't even touch client/. CPU inference is all CI needs. | |
| env: | |
| ONNXRUNTIME_NODE_INSTALL_CUDA: skip | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: '22' | |
| - name: Install and test | |
| run: cd client && rm -f package-lock.json && npm install --legacy-peer-deps && npm test -- --forceExit --shard=${{ matrix.shard }} | |
| app-tests: | |
| name: Web App Tests (Vitest) | |
| needs: changes | |
| if: ${{ needs.changes.outputs.app == 'true' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: '22' | |
| - name: Install and test | |
| run: cd app && npm ci && npm test | |
| plugin-build: | |
| name: Plugin Build Check | |
| # Path-scoped (reliability #2 follow-up): the plugin install pulls | |
| # onnxruntime-node, whose postinstall fetches from api.nuget.org and | |
| # routinely ETIMEDOUTs on CI. client/mcp/app were gated in #295 but this | |
| # job was missed, so it ran — and flaked — on python/docs-only PRs. Gate | |
| # it on skill/** so unrelated PRs don't eat the onnxruntime network flake. | |
| needs: changes | |
| if: ${{ needs.changes.outputs.plugin == 'true' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| # Same as the client-tests job: skip onnxruntime-node's CUDA/GPU binary | |
| # fetch from api.nuget.org. Since onnxruntime-node 1.31.1 that fetch fails | |
| # outright ("Failed to find specific package versions for | |
| # Microsoft.ML.OnnxRuntime.Gpu.Linux"), breaking npm install. CPU | |
| # inference is all this job needs. | |
| env: | |
| ONNXRUNTIME_NODE_INSTALL_CUDA: skip | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: '20' | |
| # We delete the lockfile because it was last regenerated against a local | |
| # `file:` dep for `@totalreclaw/core`; the current `package.json` uses | |
| # `^2.0.0` from npm and a fresh install resolves against the registry. | |
| # Once the lockfile is regenerated after `@totalreclaw/core@2.x` is | |
| # published, this can switch back to `npm ci`. | |
| - name: Install dependencies | |
| run: cd skill/plugin && rm -f package-lock.json && npm install --legacy-peer-deps | |
| # Simulate OpenClaw's `env-harvesting` scanner rule so a JSDoc comment | |
| # with "fetch" in a file that reads process.env never ships again. | |
| # (The rule also gates `openclaw plugins install` from npm, so this | |
| # stayed load-bearing after the ClawHub channel was retired.) | |
| # See docs/notes/INVESTIGATION-OPENCLAW-SCANNER-*. | |
| - name: Scanner-sim (env-harvesting false-positive check) | |
| run: node skill/scripts/check-scanner.mjs | |
| # Manifest shape regression guard (3.3.0-rc.6): asserts that | |
| # openclaw.plugin.json does NOT have "kind":"memory" (startup registry fix) | |
| # AND that index.ts still declares kind:'memory' as const (memory-slot | |
| # matching preserved). Uses npx tsx — available without a devDependency. | |
| # Includes import-time-smoke.test.ts (issue #110 fix 2) which asserts | |
| # plugin module load completes in <5s with no eager network I/O. | |
| - name: Plugin manifest shape + smoke tests | |
| run: cd skill/plugin && npm test | |
| # Issue #110 fix 1: build the plugin's TS to dist/.js, and verify the | |
| # tarball would ship a complete set of `.js` artifacts. PR-time check | |
| # so a broken build (e.g. missing import target, syntax error) doesn't | |
| # reach the publish workflow. | |
| - name: Build plugin (.ts -> dist/.js) | |
| run: cd skill/plugin && npm run build | |
| - name: Verify tarball ships built `.js` artifacts (issue #110) | |
| run: cd skill/plugin && npm run verify-tarball | |
| # Issue #124 (rc.20 ship-stopper): the published `dist/index.js` is | |
| # ESM-only (`"type":"module"`) but had three runtime `require()` calls | |
| # left over from an unfinished migration — every plugin entry point | |
| # died with `require is not defined`. The pre-publish gate above only | |
| # runs `node --check` (syntax-only) and missed it. This step actually | |
| # imports the built dist in a clean Node ESM process AND grep-guards | |
| # the dist files for any bare `require(` outside comments. Catches | |
| # the regression at PR time, never publish-time. | |
| - name: ESM-shape smoke (issue #124 — bare require regression) | |
| run: cd skill/plugin && npm run smoke:dist | |
| mcp-tests: | |
| name: MCP Server Tests | |
| needs: changes | |
| if: ${{ needs.changes.outputs.mcp == 'true' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| # Same onnxruntime-node CUDA-fetch skip as client-tests (mcp depends on | |
| # @huggingface/transformers → onnxruntime-node). | |
| env: | |
| ONNXRUNTIME_NODE_INSTALL_CUDA: skip | |
| steps: | |
| - uses: actions/checkout@v5 | |
| # `mcp` depends on `@totalreclaw/core@^2.5.x` from npm — `npm install` | |
| # resolves it from the registry, so no local WASM build is needed here. | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: '22' | |
| - name: Install, build, and test | |
| run: cd mcp && rm -f package-lock.json && npm install --legacy-peer-deps && npm run build && npm test | |
| rust-tests: | |
| name: Rust Core Tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| # Source-level guard (issue #503): a #[pyfunction] split into | |
| # src/python/bind_*.rs is invisible at runtime unless it is also | |
| # wrap_pyfunction!-registered in python.rs's #[pymodule] (or a module's | |
| # register_python_functions). A missed registration silently drops the | |
| # function from the PyPI wheel -- same failure class as the Hermes rc10 | |
| # incident (#427: tool shipped with schema+handler but never | |
| # register()-ed). Cheap source scan over the python that's already | |
| # installed for cargo's pyo3 build -- no extra toolchain download | |
| # (see #489/#491: no flaky toolchain fetches in test jobs). | |
| - name: PyO3 pyfunction <-> registration parity | |
| run: python scripts/check-pyfunction-parity.py | |
| - name: Run tests | |
| run: cd rust/totalreclaw-core && cargo test | |
| python-tests: | |
| name: Python Client Tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| - name: Build totalreclaw_core wheel from local source | |
| uses: PyO3/maturin-action@v1 | |
| with: | |
| working-directory: rust/totalreclaw-core | |
| args: --release --features python-extension --out dist -i python3.12 | |
| - name: Install local totalreclaw_core wheel | |
| run: pip install --force-reinstall rust/totalreclaw-core/dist/totalreclaw_core-*.whl | |
| - name: Install and test | |
| run: | | |
| cd python | |
| pip install -e ".[dev]" | |
| python -m pytest tests/ -v --ignore=tests/test_staging_integration.py --ignore=tests/cross_client_e2e.py --ignore=tests/test_cross_client.py -x | |
| - name: Outer protobuf fields 9/11 absent — Python calldata parity (PRD-04 F8 / DEP-6) | |
| run: cd python && python -m pytest ../tests/parity/test_outer_protobuf_parity.py -v -p no:cacheprovider | |
| # Hermes runs its own bundled Python 3.14. Install the client there with | |
| # --only-binary :all:, reusing the abi3 core wheel built above, so any | |
| # dependency (core included) that would need a compiler on 3.14 fails | |
| # this PR instead of a user's install. | |
| - uses: actions/setup-python@v5 | |
| id: py314 | |
| with: | |
| python-version: '3.14' | |
| - name: Install client binary-only on Python 3.14 (no compiler) | |
| run: | | |
| set -e | |
| pip wheel --no-deps --wheel-dir /tmp/client-wheel ./python | |
| "${{ steps.py314.outputs.python-path }}" -m venv /tmp/venv314 | |
| /tmp/venv314/bin/pip install --only-binary :all: \ | |
| rust/totalreclaw-core/dist/totalreclaw_core-*.whl /tmp/client-wheel/totalreclaw-*.whl | |
| /tmp/venv314/bin/python -c "import sys, totalreclaw, totalreclaw_core; print('OK: totalreclaw imports on', sys.version.split()[0])" | |
| enclave-tests: | |
| # TotalReclaw Enclave service (enclave/, Python 3.12). Path-scoped like | |
| # client/mcp/app: runs when enclave/, python/ or core change. Installs | |
| # the in-repo core wheel and Python client first so the enclave is tested | |
| # against the code in this PR, not the PyPI releases its floors allow. | |
| name: Enclave Tests (Python) | |
| needs: changes | |
| if: ${{ needs.changes.outputs.enclave == 'true' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| - name: Build totalreclaw_core wheel from local source | |
| uses: PyO3/maturin-action@v1 | |
| with: | |
| working-directory: rust/totalreclaw-core | |
| args: --release --features python-extension --out dist -i python3.12 | |
| - name: Install local totalreclaw_core wheel | |
| run: pip install --force-reinstall rust/totalreclaw-core/dist/totalreclaw_core-*.whl | |
| - name: Install the in-repo Python client, then the enclave | |
| run: | | |
| pip install -e ./python | |
| pip install -e "./enclave[dev]" | |
| - name: Lint (ruff check + format check) | |
| run: | | |
| cd enclave | |
| ruff check src tests | |
| ruff format --check src tests | |
| - name: Test | |
| run: cd enclave && python -m pytest tests/ -v | |
| cross-language-parity: | |
| name: Cross-Language Parity Tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: jetli/wasm-pack-action@v0.4.0 | |
| with: | |
| version: 'v0.14.0' | |
| - name: Build @totalreclaw/core (WASM) | |
| run: | | |
| cd rust/totalreclaw-core | |
| wasm-pack build --target nodejs --out-dir pkg --features wasm | |
| node -e "const p=require('./pkg/package.json'); p.name='@totalreclaw/core'; require('fs').writeFileSync('./pkg/package.json', JSON.stringify(p,null,2)+'\n')" | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: '22' | |
| - name: Install parity-test deps | |
| run: cd tests/parity && npm install --no-audit --no-fund | |
| - name: Cross-language executeBatch calldata parity (TS/WASM side) | |
| run: cd tests/parity && npx tsx userop-batch-parity.test.ts | |
| - name: Session-key EIP-712 grant digest parity (TS/viem side, cred-9, #584) | |
| run: cd tests/parity && npx tsx session-key-grant-roundtrip.ts | |
| - name: Outer protobuf fields 9/11 absent — MCP + WASM calldata parity (PRD-04 F8 / DEP-6) | |
| run: cd tests/parity && npx tsx outer-protobuf-parity.test.ts | |
| # NOTE: Relay unit tests run in the private totalreclaw-relay repo CI. | |
| # This job runs the E2E smoke test against a real relay + PostgreSQL | |
| # using Docker Compose. Requires access to totalreclaw-relay repo. | |
| # Uncomment when relay repo is accessible from CI via RELAY_REPO_TOKEN secret. | |
| # | |
| # e2e-relay: | |
| # name: E2E Relay Smoke Tests | |
| # runs-on: ubuntu-latest | |
| # steps: | |
| # - uses: actions/checkout@v5 | |
| # - uses: actions/checkout@v5 | |
| # with: | |
| # repository: graphprotocol/totalreclaw-relay | |
| # path: totalreclaw-relay | |
| # token: ${{ secrets.RELAY_REPO_TOKEN }} | |
| # - name: Set up Node.js | |
| # uses: actions/setup-node@v5 | |
| # with: | |
| # node-version: '20' | |
| # - name: Start relay + PostgreSQL | |
| # run: cd tests/e2e-relay && docker compose up -d --wait | |
| # - name: Run smoke tests | |
| # run: cd tests/e2e-relay && npx tsx smoke-test.ts | |
| # - name: Tear down | |
| # if: always() | |
| # run: cd tests/e2e-relay && docker compose down -v |