Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat(openclaw): end-to-end Tier 2 plugin install and runtime - #534

Merged
joshuajbouw merged 7 commits into
mainfrom
fix/openclaw-capsule-install
Mar 19, 2026
Merged

joshuajbouw merged 7 commits into
mainfrom
fix/openclaw-capsule-install

Conversation

@joshuajbouw

@joshuajbouw joshuajbouw commented Mar 19, 2026

Copy link
Copy Markdown
Member

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)

  • Simplify Tier2Platform to plain string (was serializing as TOML map via #[serde(untagged)] enum)
  • Add strip_types() OXC transpiler for Tier 2 TS→JS at build time (preserves ESM, unlike Tier 1's CJS conversion)
  • Add transpile_ts_tree() to walk source and transpile all .ts/.tsx files
  • Resolve Node.js binary at build time — prefers versioned Homebrew installs (node@22+), validates each candidate runs
  • Use resolved Node's sibling npm with PATH injection so #!/usr/bin/env node picks up correct version
  • Rewrite .ts entry points to .js in generated Capsule.toml

Capsule installer (astrid-cli)

  • Fix transpile_and_install to find and unpack .capsule archives from astrid-build output (was passing raw dir to install_from_local_path_inner)
  • Only skip dist/ at top level in copy_capsule_dir — npm packages inside node_modules store compiled code in dist/

MCP host engine (astrid-capsule)

  • Skip path traversal check for absolute system binaries already validated in host_process capability
  • Derive allow_network from capsule capabilities (uplink or net domains) instead of Default::default() (false)
  • Don't block capsule loading on missing env fields — fill with empty defaults and log. Uplink capsules load at boot before clients connect, so the onboarding event would be lost

Sandbox (astrid-workspace)

  • Add (allow mach*) to macOS Seatbelt profile — Node.js requires Mach IPC
  • Use unrestricted (allow file-read*) — whitelist approach breaks Node.js startup (V8 snapshots, ICU data, dylib resolution)
  • Skip hidden path deny rules when writable_root is inside the hidden path (capsule dir is inside ~/.astrid/)

Tool injection (astrid-kernel)

  • Merge MCP-discovered tools into inject_tool_schemas alongside WASM capsule tools
  • Ensure all tool schemas have properties field for LLM API compatibility

Test Plan

Automated

  • cargo test --workspace passes
  • No new clippy warnings

Manual

  1. Install Node.js 22+ via Homebrew: brew install node@22
  2. Install the OpenClaw plugin: astrid capsule install /path/to/openclaw-unicity
  3. Fill in env prompts (nametag, owner, network, etc.)
  4. Verify capsule appears in astrid capsule list (16 capsules total)
  5. Boot TUI: astrid — confirm no boot errors in ~/.astrid/logs/
  6. Ask "what tools do you have?" — agent should list Unicity tools (messaging, wallet, groups, payments)
  7. Verify MCP bridge loads plugin: logs show MCP connection established server="capsule:openclaw-unicity" tool_count=18

Checklist

  • Linked to an issue
  • CHANGELOG.md updated under [Unreleased]

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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

  • OpenClaw Build Pipeline Enhancements: Introduced TypeScript-to-JavaScript transpilation for Tier 2 plugins, ensuring ESM compatibility and proper Node.js binary resolution, including PATH injection for npm. Simplified Tier2Platform serialization.
  • Capsule Installation Refinements: Corrected the installer to properly locate and unpack .capsule archives after compilation and adjusted directory copying to selectively skip dist/ folders.
  • MCP Host Engine Robustness: Improved handling of absolute system binaries by skipping unnecessary path traversal checks, dynamically configured network access based on capsule capabilities, and prevented capsule loading failures due to missing environment variables by providing empty defaults.
  • Sandbox Security Adjustments: Updated macOS Seatbelt profiles to allow Mach IPC and broad file-read access, and refined hidden path deny rules to prevent conflicts with capsule writable roots.
  • Tool Discovery and Schema Consistency: Integrated MCP-discovered tools into the kernel's tool injection mechanism and enforced the presence of a 'properties' field in all tool schemas for LLM API compatibility.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread crates/astrid-workspace/src/sandbox.rs Outdated
Comment thread crates/astrid-capsule/src/engine/mod.rs Outdated
Comment thread crates/astrid-kernel/src/lib.rs
Comment thread crates/astrid-openclaw/src/pipeline.rs Outdated
Comment thread crates/astrid-openclaw/src/transpiler.rs Outdated
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
@joshuajbouw
joshuajbouw force-pushed the fix/openclaw-capsule-install branch from 3f0c98e to 258dfc9 Compare March 19, 2026 02:18
- 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.
@joshuajbouw
joshuajbouw merged commit 2225436 into main Mar 19, 2026
13 checks passed
@joshuajbouw
joshuajbouw deleted the fix/openclaw-capsule-install branch March 19, 2026 14:10
joshuajbouw added a commit that referenced this pull request Apr 16, 2026
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
joshuajbouw added a commit that referenced this pull request Apr 16, 2026
## 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]`
joshuajbouw added a commit that referenced this pull request Jun 6, 2026
…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]`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(openclaw): End-to-end Tier 2 OpenClaw plugin installation and runtime

1 participant