feat(openclaw): end-to-end Tier 2 plugin install and runtime - #534
Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the OpenClaw plugin ecosystem by enabling robust end-to-end installation and runtime for Tier 2 TypeScript plugins. It addresses numerous issues across the build pipeline, capsule installer, MCP host engine, and sandbox, ensuring that TypeScript-based plugins can be seamlessly built, deployed, and executed within the Astrid environment. The changes improve compatibility with Node.js, streamline dependency management, and refine security policies, ultimately providing a more stable and functional platform for plugin developers. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Code flows, types stripped, Node.js runs, swift and free, Plugins now take flight. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces support for Tier 2 OpenClaw capsules, which are Node.js-based, by implementing TypeScript transpilation using OXC, refining Node.js and npm binary resolution for correct execution, and dynamically discovering tools from these MCP host engines. It also improves environment variable handling by filling missing fields with empty strings instead of erroring, and refactors directory copying logic to conditionally skip dist folders. However, the changes introduce a significant security trade-off by broadening sandbox file-read* permissions, which should be narrowed or documented. Additionally, there are opportunities to improve performance in tool deduplication, address a portability issue with hardcoded Node.js paths, refactor duplicated code in the TypeScript transpiler, and remove a redundant loop in environment variable resolution.
Fix 14 issues preventing OpenClaw TypeScript plugins from being installed and loaded as Astrid capsules via the Tier 2 (Node.js MCP) pipeline. Tested end-to-end with openclaw-unicity (15 tools, npm deps, Sphere SDK). Build pipeline (astrid-openclaw): - Simplify Tier2Platform to plain string (was TOML map via untagged enum) - Add OXC strip_types() for Tier 2 TS→JS transpilation at build time - Resolve Node binary at build time (prefer Homebrew node@22+) - Use resolved Node's npm with PATH injection for correct ABI - Rewrite .ts entry points to .js in Capsule.toml Capsule installer (astrid-cli): - Fix transpile_and_install to find .capsule archives from astrid-build - Only skip dist/ at top level in copy_capsule_dir (npm packages need it) MCP host engine (astrid-capsule): - Skip path traversal check for absolute system binaries in host_process - Set allow_network from uplink/net capabilities (was defaulting to false) - Don't block capsule loading on missing env — fill with empty defaults Sandbox (astrid-workspace): - Add mach* permission to Seatbelt profile (Node.js needs Mach IPC) - Use unrestricted file-read* (whitelist approach breaks Node.js startup) - Skip hidden path deny when writable_root is inside the hidden path Tool injection (astrid-kernel): - Merge MCP-discovered tools into LLM tool schemas - Ensure all tool schemas have properties field for LLM API compat Closes #533
3f0c98e to
258dfc9
Compare
- Remove dead loop in resolve_env, fill defaults before publishing event - Use HashSet for O(1) tool dedup in inject_tool_schemas - Check both /opt/homebrew and /usr/local for Homebrew Node installs - Extract shared parse_and_transform() to deduplicate transpiler code - Add CHANGELOG entries for OpenClaw Tier 2 support
Move Tier 2 manifest schema types, Node.js binary resolution, source tree copying, and TS transpilation helpers from pipeline.rs (1063 lines) to a new tier2.rs module. pipeline.rs is now 747 lines.
The previous commit used unrestricted (allow file-read*) which exposed ~/.ssh, ~/.aws, and other sensitive paths to sandboxed processes. Root cause: Node.js needs to stat the root directory (/) for path resolution. Adding (literal "/") to the read whitelist is sufficient. The full whitelist is restored — sandboxed processes can only read system paths, their own capsule directory, and /tmp. Verified: sandboxed Node.js cannot read ~/.ssh/id_ed25519 (EPERM).
Tests referenced the removed Tier2Platform enum and unqualified copy_plugin_source/channel_to_platform. Updated to use tier2:: prefix and simplified platform tests to match the new String return type.
Tests hardcoded command = "node" but resolve_node_binary() may return a full path on systems with versioned Homebrew installs. Updated all test assertions to accept either form.
Two compounding bugs prevented OpenClaw Tier 2 capsules from starting on Linux: 1. Mount ordering: --tmpfs for hidden paths (e.g. ~/.astrid) was applied BEFORE writable --bind mounts, causing the tmpfs to erase the capsule directory. In bwrap, later mounts override earlier ones — reordered so hidden tmpfs comes first and writable bind-mounts punch through after. This mirrors the ancestor check already present in the macOS Seatbelt path (PR #534). 2. AppArmor: Ubuntu 24.04+ blocks unprivileged user namespaces by default (kernel.apparmor_restrict_unprivileged_userns=1), silently killing bwrap. Added a cached probe (bwrap --unshare-user -- /bin/true) that detects this at startup and falls back to unsandboxed execution with a clear warning, rather than producing a misleading 'connection closed' error. Closes #648
## Linked Issue Closes #648 ## Summary Two compounding bugs prevented OpenClaw Tier 2 (Node.js) capsules from starting on Linux. The macOS Seatbelt path was already fixed in PR #534 — this ports the fix to the Linux bwrap path and adds AppArmor detection. ## Changes - **Mount ordering fix:** Reordered `build_bwrap_prefix()` so hidden `--tmpfs` overlays come before writable `--bind` mounts. In bwrap, later mounts override earlier ones — capsule bind-mounts now punch through the `~/.astrid` tmpfs overlay instead of being erased by it. - **AppArmor/bwrap probe:** Added `bwrap_available()` with a cached probe (`bwrap --unshare-user --ro-bind / / -- /bin/true`). When user namespaces are blocked (Ubuntu 24.04+ default), `sandbox_prefix()` returns `None` and the existing unsandboxed fallback in `server.rs` runs with a clear warning. - **Distro-aware install hint:** When bwrap is missing, `bwrap_install_hint()` reads `/etc/os-release` and logs the correct install command for the detected package manager (apt/dnf/pacman/apk/zypper/xbps/nix). - **Module split:** `sandbox.rs` (1241 lines) split into `sandbox/mod.rs` (629), `sandbox/bwrap.rs` (385), `sandbox/seatbelt.rs` (189) to stay within the CI line limit. - **Server test resilience:** 4 `astrid-mcp` server tests updated to accept both sandboxed and unsandboxed command shapes, since bwrap may be unavailable in CI containers. - **CHANGELOG.md** updated under `[Unreleased] > Fixed`. ## Test Plan ### Automated - [x] `cargo test --workspace` passes (2 pre-existing socket test failures unrelated to this PR) - [x] No new clippy warnings (`cargo clippy --workspace -- -D warnings`) - [x] `cargo +nightly fmt --all --check` passes New tests added: | Test | File | Covers | |---|---|---| | `test_bwrap_prefix_writable_inside_hidden_path` | `bwrap.rs` | Bug 2: writable root inside hidden path → bind after tmpfs | | `test_seatbelt_prefix_writable_inside_hidden_path` | `seatbelt.rs` | macOS equivalent: ancestor check skips deny rule | | `test_bwrap_probe_success` | `bwrap.rs` | Probe interprets exit 0 as available | | `test_bwrap_probe_namespace_denied` | `bwrap.rs` | Probe interprets AppArmor/namespace failure as unavailable | | `test_bwrap_probe_not_found` | `bwrap.rs` | Probe interprets missing binary as unavailable | | `test_bwrap_install_hint_returns_nonempty` | `bwrap.rs` | Install hint always returns non-empty string mentioning bubblewrap | ### Manual - Built and ran `cargo test -p astrid-workspace -- sandbox` in Docker (`ubuntu:24.04`) — all 28 Linux tests pass. - Verified bwrap probe returns `false` in unprivileged Docker container (simulates AppArmor restriction). - Verified bwrap probe returns `true` in `--privileged` Docker container. ## Checklist - [x] Linked to an issue - [x] CHANGELOG.md updated under `[Unreleased]`
…857) ## Linked Issue Closes #855 ## Summary `SandboxCommand::wrap` — the macOS arm of the `host_process` spawn path — disabled the OS-level subprocess sandbox on **every current Mac** (Darwin >= 24 = macOS 15 Sequoia / macOS 26) and returned the command **completely unsandboxed**, behind: ```rust if seatbelt::darwin_major_version() >= 24 { tracing::warn!("macOS 15+ detected: sandbox-exec is deprecated. Running host process unsandboxed."); return Ok(inner_cmd); } ``` On `host_process`-capable capsules (`astrid-capsule-shell`) every native subprocess then inherited the host user's full filesystem reach — it could read `~/.ssh`, dotfiles, and write anywhere the user can — with a single `tracing::warn!` as the only signal. Containment also depended on which spawn path a capsule happened to use: the MCP path (`ProcessSandboxConfig::sandbox_prefix`) has **no** version guard and sandboxed on the same machine. ### Root cause The premise of the guard — that `sandbox-exec` is deprecated and therefore non-enforcing on macOS 15+ — is **false**. `sandbox-exec` is deprecated but still enforces on current macOS. The SIGABRT that motivated the guard (introduced in #603, no reproducer, merged without review) was a fail-closed **profile defect**: `wrap`'s inline profile was a stale duplicate that omitted `(allow file-read* (literal "/"))`. A dynamically-linked binary like `node` stats the filesystem root `/` during `dyld` init; the profile denied it, Seatbelt correctly aborted the process, and the guard converted that fail-closed signal into a silent fail-open passthrough. The correct profile already lives in `build_seatbelt_prefix` (it carries `(literal "/")` + `(allow mach*)`, added for Node.js in #534) and runs on macOS 15+ via the MCP path. ## Changes - Remove the `darwin_major_version() >= 24` guard and the stale duplicate inline profile in `wrap`. - Route `wrap`'s macOS arm through `build_seatbelt_prefix` — a single Seatbelt profile now backs **both** the `wrap` and MCP spawn paths. - Drop the now-dead `darwin_major_version` helper. - Fail-closed by construction: if `sandbox-exec` genuinely can't run, the spawn errors (`ENOENT`) and the subprocess never launches — never a silent unsandboxed launch. (Matches the `SandboxPolicy::Required` posture from #655.) - `wrap` no longer has a panic path (the builder returns `io::Result`); its `# Panics` doc is removed. ## Test Plan ### Automated - [x] `cargo test -p astrid-workspace` green (56 passed), including: - `seatbelt_root_read_is_load_bearing_for_real_binary` — spawns a **real `node`** under the generated profile and asserts it runs (T1), then strips `(literal "/")` and asserts the same profile **fails closed** (T3). Skips cleanly when `node` or a usable `sandbox-exec` is absent (e.g. nested-sandbox CI). - `wrap_always_sandboxes_via_shared_profile` — `wrap` now always returns a `sandbox-exec`-wrapped command carrying the root-read rule (never a passthrough). - [x] No new clippy warnings — `cargo clippy --workspace --all-features -- -D warnings` clean for the changed crate; `astrid-capsule` (the consumer) compiles. ### Manual - [x] Verified on **Darwin 25.2**: T1 exits 0, T3 fails closed — matches the issue's repro table. ## External verification The deprecation claim and SBPL mechanism were independently verified against documentation: - `sandbox-exec` is **deprecated but still enforcing** on macOS 15 / macOS 26 (Apple DTS confirmation that it remains functional with no supported replacement for non-App-Store process sandboxing; third-party reports debugging live Seatbelt denials on Darwin 25 in Dec 2025; Chrome/Firefox/Homebrew/SwiftPM still depend on it). - `(literal "/")` grants only a `stat` on the **root inode**, not the recursive access `(subpath "/")` would — so the fix is minimal-aperture, not a widening. - App Sandbox / entitlements require a signed `.app` bundle and **cannot** wrap arbitrary spawned binaries like `node`, so `sandbox-exec` is the only mechanism for this use case. ## Notes / follow-ups - `(allow mach*)` in the shared profile is a deliberately broad wildcard — pragmatic for running general-purpose binaries (e.g. `node`'s `os.cpus()` needs `mach-host*`), a conscious trade-off. - If Apple ever removes `sandbox-exec`, the correct response is a real replacement, **not** another silent-fallback version guard (fail-secure). - Kernel-internal sandbox wiring: no WIT/IPC/capability-contract change, so no RFC. - Security fix that should land in **0.8.0** — once merged I'll rebase the release PR (#854) so this entry folds into `[0.8.0]`. ## Checklist - [x] Linked to an issue - [x] CHANGELOG.md updated under `[Unreleased]`
Linked Issue
Closes #533
Summary
Fixes 14 issues preventing OpenClaw TypeScript plugins from being installed and loaded as Astrid capsules via the Tier 2 (Node.js MCP subprocess) pipeline. Tested end-to-end with
openclaw-unicity— a real-world plugin with 15 tools, npm dependencies, and Sphere SDK integration.Changes
Build pipeline (
astrid-openclaw)Tier2Platformto plain string (was serializing as TOML map via#[serde(untagged)]enum)strip_types()OXC transpiler for Tier 2 TS→JS at build time (preserves ESM, unlike Tier 1's CJS conversion)transpile_ts_tree()to walk source and transpile all.ts/.tsxfilesnode@22+), validates each candidate runs#!/usr/bin/env nodepicks up correct version.tsentry points to.jsin generatedCapsule.tomlCapsule installer (
astrid-cli)transpile_and_installto find and unpack.capsulearchives fromastrid-buildoutput (was passing raw dir toinstall_from_local_path_inner)dist/at top level incopy_capsule_dir— npm packages insidenode_modulesstore compiled code indist/MCP host engine (
astrid-capsule)host_processcapabilityallow_networkfrom capsule capabilities (uplink or net domains) instead ofDefault::default()(false)Sandbox (
astrid-workspace)(allow mach*)to macOS Seatbelt profile — Node.js requires Mach IPC(allow file-read*)— whitelist approach breaks Node.js startup (V8 snapshots, ICU data, dylib resolution)writable_rootis inside the hidden path (capsule dir is inside~/.astrid/)Tool injection (
astrid-kernel)inject_tool_schemasalongside WASM capsule toolspropertiesfield for LLM API compatibilityTest Plan
Automated
cargo test --workspacepassesManual
brew install node@22astrid capsule install /path/to/openclaw-unicityastrid capsule list(16 capsules total)astrid— confirm no boot errors in~/.astrid/logs/MCP connection established server="capsule:openclaw-unicity" tool_count=18Checklist
[Unreleased]