Back to feed

sledtools/pika branch #29

incus-ci

Finish staged Linux Incus cutover

Target branch: master

Merge Commit: 81f5c993cea5002fd9f33fcf2fcf475609128522

branch: merged tutorial: ready ci: failed
Open CI Details

Continuous Integration

CI: failed

Compact status on the review page, with full logs on the CI page.

Open CI Details

Latest run #35 failed

9 passed 1 failed

head 43a8543bee5cb1fa3a8add9ec92d05f0650d9aed · queued 2026-03-24 13:50:03 · 10 lane(s)

queued 2m 11s · ran 24s

check-pika-rust · success check-pika-followup · success check-notifications · success check-agent-contracts · success check-rmp · success check-pikachat · success check-pikachat-typescript · success check-apple-host-sanity · failed check-pikachat-openclaw-e2e · success check-fixture · success

Summary

This branch completes the staged Linux Incus cutover for the pika CI system by removing the last two automatic Incus-default exclusions — pre-merge-agent-contracts and pre-merge-pikachat-openclaw-e2e — so that all staged pre-merge Linux jobs on pika-build now default to Incus with no remaining microVM fallbacks. The key changes are: (1) removing the stale host-side deterministic HTTP agent test job and its associated StagedLinuxRustLane variant, (2) eliminating the automatic_incus_default_exclusion gate that kept agent-contracts and OpenClaw E2E targets off the Incus path, (3) overhauling the OpenClaw E2E scenario to copy plugins into a runtime-local directory instead of symlinking into the mounted package root (which caused duplicate plugin discovery and gateway health stalls), (4) adding a bounded /health probe for the OpenClaw gateway with rich diagnostic artifacts on failure, (5) introducing a Unix socket path resolver that handles the platform's 108-byte limit, (6) switching the gateway config from inline sidecarCmd/sidecarArgs to a daemonCmd/daemonBackend contract, (7) trimming the forge-lanes.toml workflow filter to remove paths that only existed for the now-removed host-side selectors, and (8) updating all documentation, guardrail tests, and job descriptions to reflect the completed Incus migration state.

Tutorial Steps

Remove the AgentContractsDeterministicHttp lane variant and its job spec

Intent: The host-side deterministic HTTP agent tests encode legacy vm-spawner-era assumptions that no longer apply under Incus. Rather than treat them as an Incus parity problem, the branch removes them from provider-contract CI entirely, unblocking the Incus default for the agent-contracts target.

Affected files: crates/pikaci/src/main.rs, crates/pikaci/src/model.rs

Evidence
@@ -1192,21 +1192,6 @@ fn agent_contract_jobs() -> Vec<JobSpec> {
-        JobSpec {
-            id: "agent-http-deterministic-tests",
-            description: "Run the deterministic pikahut agent HTTP tests in a remote Linux microVM",
-            ...
-            staged_linux_rust_lane: Some(StagedLinuxRustLane::AgentContractsDeterministicHttp),
-        },
@@ -187,7 +187,6 @@ pub enum StagedLinuxRustLane {
-    AgentContractsDeterministicHttp,
@@ -221,7 +220,6 @@ impl StagedLinuxRustLane {
-            Self::AgentContractsDeterministicHttp => "agent_contracts_deterministic_http",
@@ -337,9 +332,6 @@ impl StagedLinuxRustLane {
-            Self::AgentContractsDeterministicHttp => {
-                "/staged/linux-rust/workspace-build/bin/run-agent-http-deterministic-tests"
-            }

The AgentContractsDeterministicHttp variant is deleted from StagedLinuxRustLane along with its string key (agent_contracts_deterministic_http), its execute wrapper path, and the JobSpec entry in agent_contract_jobs(). The match arm that mapped this variant to StagedLinuxRustTarget::PreMergeAgentContracts is consolidated so the remaining four agent-contract lanes share a single arm.

The pre-merge job count assertion in the test suite drops from 5 to 4, and a new negative assertion confirms that the removed job ID no longer appears:

assert!(pre_merge.jobs.iter().all(|job| job.id != "agent-http-deterministic-tests"));

The test that previously validated workspace output paths via AgentContractsDeterministicHttp is rewritten to use AgentContractsMicrovmTests instead, ensuring the agent-contracts workspace output contract still has coverage.

Remove the automatic Incus default exclusion gate

Intent: With the stale agent HTTP tests removed and the OpenClaw E2E scenario fixed (in a later step), there is no longer a reason to exclude any staged target from the Incus default on pika-build. This step removes the exclusion function so all remote Linux VM jobs follow the same Incus default path.

Affected files: crates/pikaci/src/model.rs

Evidence
@@ -599,24 +591,13 @@ fn forced_remote_linux_vm_backend() -> Option<RemoteLinuxVmBackend> {
-fn should_default_remote_linux_vm_to_incus(job: &JobSpec) -> bool {
+fn should_default_remote_linux_vm_to_incus(_job: &JobSpec) -> bool {
     ...
-    ) && !automatic_incus_default_exclusion(job)
+    )
 }
-
-fn automatic_incus_default_exclusion(job: &JobSpec) -> bool {
-    matches!(
-        job.staged_linux_rust_lane()
-            .map(StagedLinuxRustLane::target),
-        Some(StagedLinuxRustTarget::PreMergeAgentContracts)
-            | Some(StagedLinuxRustTarget::PreMergePikachatOpenclawE2e)
-    )
-}

The automatic_incus_default_exclusion function is deleted entirely. The should_default_remote_linux_vm_to_incus function's job parameter becomes _job since the function no longer inspects the job spec — it only checks whether the SSH host is a known local builder.

Two existing unit tests are renamed to reflect the new behavior:

  • agent_contracts_target_does_not_default_to_incus_on_pika_buildagent_contracts_target_defaults_to_incus_on_pika_build (now asserts RemoteLinuxVmBackend::Incus)
  • pikachat_openclaw_target_does_not_default_to_incus_on_pika_buildpikachat_openclaw_target_defaults_to_incus_on_pika_build (same)

A third test (agent_contracts_target_can_still_opt_into_incus_explicitly) is replaced with pikachat_openclaw_target_selector_still_opt_in_matches_away_from_pika_build, which validates that explicit lane selectors still work for non-pika-build hosts.

Trim forge-lanes.toml workflow filter paths

Intent: The agent-contracts workflow filter previously included paths for the CLI, pikahut sources/tests, pika-desktop, and pikachat-sidecar because those were transitive dependencies of the now-removed host-side deterministic HTTP selectors. With those selectors gone, the filter is trimmed to avoid false-positive CI triggers.

Affected files: ci/forge-lanes.toml

Evidence
@@ -109,7 +109,6 @@ paths = [
-  "cli/**",
@@ -132,11 +131,6 @@ paths = [
-  "crates/pika-desktop/**",
-  "crates/pikahut/Cargo.toml",
-  "crates/pikahut/src/**",
-  "crates/pikahut/tests/**",
-  "crates/pikachat-sidecar/**",

Six path entries are removed from the check-agent-contracts lane filter in ci/forge-lanes.toml:

  • cli/** — no longer needed since agent HTTP CLI selectors are gone
  • crates/pika-desktop/** — was a transitive dependency of pikahut selectors
  • crates/pikahut/Cargo.toml, crates/pikahut/src/**, crates/pikahut/tests/** — the lane no longer owns any pikahut test selectors
  • crates/pikachat-sidecar/** — was needed for the CLI shell-out tests

The guardrail test in crates/pikahut/tests/guardrails.rs is updated in lockstep to assert the absence of these paths rather than their presence, ensuring the filter stays trimmed.

Overhaul OpenClaw E2E plugin materialization for Incus compatibility

Intent: Under Incus, symlinks that resolve back to the mounted workspace-build path caused OpenClaw's plugin safety checks to reject the plugin. The fix copies the plugin tree into a runtime-local directory for packaged runtimes and introduces helper functions for safe path operations.

Affected files: crates/pikahut/src/testing/scenarios/openclaw.rs

Evidence
@@ -44,17 +48,6 @@ impl OpenclawRuntime {
-    fn plugin_source_root(&self) -> &Path {
-        match self {
-            Self::Checkout { plugin_source_root, .. }
-            | Self::Packaged { plugin_source_root, .. } => plugin_source_root,
-        }
-    }
@@ +136,19 +192,69 @@ fn reset_symlink(path: &Path, target: &Path) -> Result<()> {
+fn remove_path(path: &Path) -> Result<()> { ... }
+fn copy_tree(source: &Path, destination: &Path) -> Result<()> { ... }
+fn materialize_plugin_runtime_root(runtime: &OpenclawRuntime, destination: &Path) -> Result<()> { ... }

The unified plugin_source_root() accessor is removed from OpenclawRuntime because checkout and packaged runtimes now need different materialization strategies.

Three new functions are introduced:

  1. remove_path — extracted from reset_symlink, handles both files and directories including symlinks
  2. copy_tree — recursively copies a directory tree, resolving symlinks to their canonical targets before copying (critical for Incus where the mount point would otherwise leak into the copy)
  3. materialize_plugin_runtime_root — dispatches based on runtime variant: checkout mode symlinks as before, packaged mode does remove_path + copy_tree

The call site in run_openclaw_e2e switches from reset_symlink(&plugin_runtime_path, openclaw_runtime.plugin_source_root()) to materialize_plugin_runtime_root(&openclaw_runtime, &plugin_runtime_path). A new test (packaged_plugin_runtime_is_materialized_as_local_copy) verifies that the packaged path produces a real directory (not a symlink) with the expected file structure.

Add workspace-level node_modules symlink and cwd fix for packaged gateway

Intent: The packaged OpenClaw gateway resolves its configuration relative to a workspace root directory. The scenario was incorrectly using the state directory as the cwd, causing module resolution failures. Additionally, a workspace-level node_modules symlink is needed for the gateway's require chain.

Affected files: crates/pikahut/src/testing/scenarios/openclaw.rs

Evidence
@@ -438,14 +689,19 @@ pub async fn run_openclaw_e2e(args: OpenclawE2eRequest) -> Result<ScenarioRunOut>
+    let openclaw_workspace_root = openclaw_config_path
+        .parent()
+        .ok_or_else(|| anyhow!("OpenClaw config path missing parent directory"))?;
     ...
+    let openclaw_workspace_node_modules_dir = openclaw_workspace_root.join("node_modules");
@@ -567,7 +808,7 @@ pub async fn run_openclaw_e2e
-                .cwd(&openclaw_state_dir)
+                .cwd(openclaw_workspace_root)

The scenario now computes openclaw_workspace_root as the parent directory of the config file path. A second node_modules symlink is created at the workspace root level (in addition to the existing state-dir level one), both pointing to the OpenClaw package root.

The packaged gateway's working directory is changed from openclaw_state_dir to openclaw_workspace_root, which is where the gateway expects to resolve its configuration and plugin paths from. Several new environment variables are passed to the gateway process: NODE_PATH, PIKACHAT_DAEMON_CMD, PIKACHAT_SIDECAR_CMD, PIKA_OPENCLAW_GATEWAY_PORT, and OPENCLAW_DISABLE_BONJOUR.

Switch gateway config from sidecarCmd/sidecarArgs to daemonCmd/daemonBackend

Intent: The OpenClaw gateway now uses a managed daemon contract where the gateway itself manages the pikachat daemon lifecycle, rather than the old sidecar-args-based approach. This aligns with the Incus runtime where the daemon must be launched as a native subprocess.

Affected files: crates/pikahut/src/testing/scenarios/openclaw.rs

Evidence
@@ +376,101 +376,101 @@
+fn openclaw_gateway_channel_config(
+    relay_url: &str,
+    daemon_state_dir: &Path,
+    daemon_cmd: &str,
+) -> Value {
+    json!({
+      "relays": [relay_url],
+      "groupPolicy": "open",
+      "autoAcceptWelcomes": true,
+      "stateDir": daemon_state_dir,
+      "daemonCmd": daemon_cmd,
+      "daemonBackend": "native",
+    })
@@ -513,45 +773,13 @@
-    let sidecar_args = vec![
-        "daemon".to_string(),
-        "--relay".to_string(),
-        ...
-    ];
-    let config_json = json!({ ... "sidecarCmd": sidecar_cmd, "sidecarArgs": sidecar_args ... });
+    let config_json = openclaw_gateway_config(
+        &relay_url, &sidecar_state_dir, &sidecar_cmd, gw_port, &plugin_runtime_path,
+    );

The inline gateway configuration JSON is replaced with two extracted functions:

  • openclaw_gateway_channel_config — produces the per-channel config with daemonCmd and daemonBackend: "native" instead of sidecarCmd/sidecarArgs
  • openclaw_gateway_config — produces the full gateway config including gateway.mode, gateway.bind, gateway.port, plugin loading, and channels, with the channel config shared between plugins.entries and channels

The old sidecar_args vector (which manually assembled ["daemon", "--relay", ...]) is removed entirely. The new contract delegates daemon lifecycle management to the gateway via daemonBackend: "native".

A test (staged_gateway_config_uses_managed_daemon_contract_shape) validates:

  • The config shape includes daemonCmd and daemonBackend
  • sidecarCmd and sidecarArgs are absent (null)
  • Plugin and channel configs are symmetric

Add bounded /health probe for the OpenClaw gateway

Intent: Under Incus, the packaged OpenClaw gateway has a measurably longer cold start (~66s for Jiti compilation). A bounded health probe prevents the scenario from racing ahead to keypackage publication before the gateway is ready, and provides rich diagnostics on timeout.

Affected files: crates/pikahut/src/testing/scenarios/openclaw.rs

Evidence
@@ +24,8 +24,8 @@
+const OPENCLAW_GATEWAY_HEALTH_TIMEOUT: Duration = Duration::from_secs(120);
+const OPENCLAW_GATEWAY_HEALTH_REQUEST_TIMEOUT: Duration = Duration::from_secs(2);
@@ +606,61 +606,61 @@
+async fn wait_for_openclaw_gateway_health(
+    gateway_port: u16,
+    sidecar_state_dir: &Path,
+    gateway: &mut SpawnHandle,
+) -> Result<()> { ... }

A new wait_for_openclaw_gateway_health async function polls http://127.0.0.1:{port}/health every 250ms with a 2-second per-request timeout and a 120-second overall deadline. On each iteration it checks whether the gateway process has exited early.

The error messages include diagnostic state: daemon socket presence, identity file presence, and the last health error string. This is called in the main run_openclaw_e2e flow immediately after spawning the gateway and before attempting keypackage publication.

On health probe failure, the scenario writes three directory listing artifacts before returning the error:

  • openclaw-workspace-tree.txt
  • openclaw-state-tree.txt
  • sidecar-state-tree.txt

Add Unix socket path resolver with platform length limit handling

Intent: Unix domain socket paths have a platform-imposed ~108-byte limit. Deep state directory paths (common in CI) can exceed this, causing silent bind failures. The resolver falls back to a hashed path in /tmp when the preferred path is too long.

Affected files: crates/pikahut/src/testing/scenarios/openclaw.rs

Evidence
@@ +64,69 +64,69 @@
+fn resolve_sidecar_daemon_socket_path(state_dir: &Path) -> PathBuf {
+    const MAX_UNIX_SOCKET_PATH_BYTES: usize = 100;
+    let preferred = state_dir.join("daemon.sock");
+    if preferred.as_os_str().to_string_lossy().len() <= MAX_UNIX_SOCKET_PATH_BYTES {
+        return preferred;
+    }
+    let mut hasher = std::collections::hash_map::DefaultHasher::new();
+    state_dir.hash(&mut hasher);
+    std::env::temp_dir().join(format!("pikachat-daemon-{:016x}.sock", hasher.finish()))
+}

The resolve_sidecar_daemon_socket_path function uses a conservative 100-byte limit (below the typical 108-byte kernel limit to leave margin). When the preferred {state_dir}/daemon.sock path fits, it is used directly. Otherwise, the state directory is hashed with DefaultHasher and a deterministic /tmp/pikachat-daemon-{hash}.sock path is generated.

This path is used consistently throughout the scenario: in error messages for wait_for_sidecar_keypackage, in the health probe, and in the final scenario output metadata. A unit test (sidecar_daemon_socket_uses_exists_style_path_contract) verifies the short-path case returns the expected join.

Add diagnostic artifacts and enriched error context

Intent: Incus-specific failures were difficult to diagnose because the scenario lacked visibility into the runtime filesystem state and daemon socket presence. This step adds directory tree listings, runtime environment dumps, and socket/identity metadata to both error paths and successful run outputs.

Affected files: crates/pikahut/src/testing/scenarios/openclaw.rs

Evidence
@@ +64,69 +64,69 @@
+fn write_tree_listing(path: &Path, output: &mut String) -> Result<()> { ... }
+fn write_directory_listing_artifact(path: &Path, artifact_path: &Path) -> Result<()> { ... }
@@ +376,101 +376,101 @@
+struct OpenclawRuntimeEnvArtifact<'a> { ... }
+fn write_openclaw_runtime_env_artifact(env: OpenclawRuntimeEnvArtifact<'_>) -> Result<()> { ... }
@@ +661,941 +941,32 @@
+        .with_metadata("daemon_socket_path", ...)
+        .with_metadata("daemon_socket_present", ...)
+        .with_metadata("identity_json_path", ...)
+        .with_metadata("identity_json_present", ...)

Several diagnostic utilities are added:

  1. write_tree_listing — recursively formats a directory tree with entry types (file, dir, symlink with target, socket, other) using FileTypeExt for Unix socket detection
  2. write_directory_listing_artifact — writes a tree listing to an artifact file, handling the missing-directory case
  3. OpenclawRuntimeEnvArtifact + write_openclaw_runtime_env_artifact — dumps the full runtime environment (state dirs, config paths, socket paths, daemon command, gateway port) to a text artifact

These are used in three places:

  • After the gateway health probe fails
  • After keypackage publication fails
  • As a standard artifact during normal scenario setup (openclaw-runtime-env.txt)

The successful scenario output is enriched with four new metadata fields: daemon_socket_path, daemon_socket_present, identity_json_path, and identity_json_present.

Fix run plan test to use explicit backend environment

Intent: With the Incus default now applying universally, the existing run plan test would produce different results depending on the host environment. Wrapping it in an explicit backend env ensures deterministic test behavior.

Affected files: crates/pikaci/src/run.rs

Evidence
@@ -4412,6 +4412,43 @@ mod tests {
+    fn with_remote_linux_vm_backend_env<T>(value: Option<&str>, action: impl FnOnce() -> T) -> T { ... }
@@ -4704,7 +4741,9 @@ mod tests {
-        let plan = build_run_plan(&jobs, &prepared, &snapshot, &metadata).expect("build plan");
+        let plan = with_remote_linux_vm_backend_env(Some("microvm"), || {
+            build_run_plan(&jobs, &prepared, &snapshot, &metadata).expect("build plan")
+        });

A new test helper with_remote_linux_vm_backend_env is added that safely sets/unsets the PIKACI_REMOTE_LINUX_VM_BACKEND, PIKACI_REMOTE_LINUX_VM_INCUS_LANES, and SSH host environment variables around a closure, with proper save/restore semantics.

The build_run_plan test is wrapped in with_remote_linux_vm_backend_env(Some("microvm"), ...) to force the microvm backend, preventing the test from accidentally using the Incus default and producing a different plan structure than expected.

Update job descriptions from microVM to Linux VM terminology

Intent: With the Incus cutover complete, the job descriptions should no longer reference the legacy 'microVM' terminology. This is a cosmetic but important change for operator clarity.

Affected files: crates/pikaci/src/main.rs

Evidence
@@ -1365,7 +1350,7 @@ fn pika_followup_jobs() -> Vec<JobSpec> {
-            description: "Compile Android instrumentation test Kotlin for the Pika app in a remote microVM guest",
+            description: "Compile Android instrumentation test Kotlin for the Pika app in a remote Linux VM guest",
@@ -1429,7 +1414,7 @@ fn pikachat_openclaw_e2e_jobs() -> Vec<JobSpec> {
-        description: "Run the heavy OpenClaw gateway end-to-end scenario in a remote Linux microVM",
+        description: "Run the heavy OpenClaw gateway end-to-end scenario in a remote Linux VM",

Eleven job description strings across pika_followup_jobs, pikachat_openclaw_e2e_jobs, pikachat_typescript_jobs, fixture_rust_jobs, and rmp_jobs are updated to replace "microVM" or "Linux microVM" with "Linux VM". This reflects that the jobs now run on Incus containers rather than Firecracker/vfkit micro-VMs.

Update guardrail tests to match the new lane surface

Intent: The guardrail tests enforce that CI lane configurations match checked-in source artifacts. With the host-side agent selectors removed and new Incus-specific staged wrapper requirements added, the guardrails must be updated to validate the new contract.

Affected files: crates/pikahut/tests/guardrails.rs

Evidence
@@ -854,6 +854,33 @@ fn pre_merge_pikachat_filter_tracks_checked_in_lane_surface() -> Result<()> {
+        assert!(
+            linux_rust.contains("openclaw_stage_dir=\"$root/share/pikaci/openclaw-gateway-e2e\""),
+            "staged OpenClaw gateway wrapper must use the root-owned packaged OpenClaw e2e tree..."
+        );
@@ -977,74 +1004,27 @@
-    let expected_host_selectors = [
-        "integration_deterministic::agent_http_ensure_local",
-        ...
-    ];
+    assert!(
+        !selector_refs.iter().any(|selector| selector.starts_with("integration_deterministic::agent_")),
+        "pre-merge-agent-contracts should not keep stale host-side deterministic agent selectors"
+    );

The guardrail changes fall into two categories:

pikachat lane guardrails (new positive assertions):

  • The staged OpenClaw wrapper must use a root-owned packaged e2e tree (openclaw_stage_dir)
  • The packaged tree must have its bundled extensions directory cleared (rm -rf + mkdir -p)
  • The plugin must be sourced from a standalone share tree, not from the mounted workspace-build path
  • The OpenClaw launcher binary must be copied into the dedicated e2e tree
  • The wrapper must NOT restage under /tmp (would fail plugin safety checks on Incus)
  • The wrapper must NOT restage the full extension tree (would cause gateway stalls)

agent-contracts lane guardrails (flipped from positive to negative):

  • Host-side integration_deterministic::agent_* selectors must be absent
  • crates/pikahut/src/**, crates/pikahut/tests/**, crates/pikahut/Cargo.toml, and cli/** must be absent from the workflow filter
  • All the transitive dependency checks (pika-desktop, pikachat-sidecar, etc.) and the CLI shell-out body inspection are removed

Update documentation to reflect completed Incus migration

Intent: The documentation must accurately describe the current CI state: what the agent-contracts lane covers, why the old selectors were removed, and the fact that there are no remaining automatic Incus exclusions.

Affected files: docs/agent-ci.md, docs/incus-migration-plan.md, docs/testing/ci-selectors.md, docs/testing/integration-matrix.md, just/checks.just, nix/ci/linux-rust.nix, todos/faster-ci.md

Evidence
@@ -14,8 +14,9 @@ These lanes are defined canonically in `ci/forge-lanes.toml`
- `check-agent-contracts`:
-  - Runs mocked HTTP control-plane contracts for MicroVM...
-  - Covers: `pika-agent-microvm` tests, `pika-server` agent API tests, the lower-level `pikahut` deterministic HTTP integration probes...
+  - Runs the checked-in staged Linux agent provider contract surface on `pikaci`.
+  - Covers: `pika-agent-control-plane` unit tests, `pika-agent-microvm` tests, `pika-server` `agent_api::tests`, and the `pika_core` NIP-98 signing contract test.
+  - Intentionally does not cover the old host-side `pikahut` deterministic HTTP / CLI selectors anymore.
@@ -905,26 +914,30 @@ Current `pika-build` proof status:
-  staged Linux on `pika-build` now defaults to Incus for the green branch targets, with two
-  explicit automatic-default exclusions still kept off the Incus path
+  staged pre-merge Linux on `pika-build` now defaults to Incus with no
+  remaining automatic Incus exclusions in the path discussed in this plan

agent-ci.md: The check-agent-contracts lane description is rewritten to list only the surviving staged coverage (pika-agent-control-plane, pika-agent-microvm, pika-server agent_api::tests, NIP-98). The removed selectors are explicitly documented as intentionally excluded with a rationale. The PR-change patterns section removes entries for pika-desktop, pikahut/Cargo.toml, and pikahut/tests, and notes that touching cli/src/main.rs no longer triggers agent-contracts.

incus-migration-plan.md: A new note is added about the OpenClaw packaged gateway requirements (empty extensions directory, runtime-local plugin copy, standalone share tree sourcing). The proof status section is rewritten: both previous blockers are now resolved, and the summary states there are no remaining automatic Incus exclusions.

ci-selectors.md and integration-matrix.md: Updated to remove references to the deleted selectors and reflect the new lane surface.

just/checks.just: The local pre-merge-agent-contracts recipe is updated to include pika-agent-control-plane to match the staged lane.

nix/ci/linux-rust.nix: The staged OpenClaw gateway wrapper is updated to use the root-owned packaged tree, clear bundled extensions, and source the plugin from the standalone share path.

Diff