Back to feed

sledtools/pika branch #119

pika-cloud-openclaw-startup-simplify

Simplify managed OpenClaw startup plan

Target branch: master

Merge Commit: 9e063aaef26230eddadfd199aa507334dfe43488

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

Continuous Integration

CI: success

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

Open CI Details

Latest run #146 success

2 passed

head c833ba1c2d7c44502b7f4e47a7a3be731ec481ad · queued 2026-03-26 21:54:35 · 2 lane(s)

queued 14s · ran 26s

check-notifications · success check-agent-contracts · success

Summary

This branch simplifies the managed OpenClaw guest startup plan by collapsing a generic, multi-variant service abstraction into a single OpenClaw-specific schema. The previous design modeled the startup plan around polymorphic enums (GuestServiceKind, GuestServiceBackendMode, GuestServiceLaunch, GuestServiceReadinessCheck) that supported multiple service kinds (PikachatDaemon vs OpenclawGateway) and backend modes (Native vs ACP). Since only the OpenClaw gateway path is actually used, this branch removes the unused variants and indirection, replacing them with flat, purpose-built structs (OpenclawStartupPlan, OpenclawLaunchPlan, OpenclawReadinessPlan). The validation logic is correspondingly simplified from complex cross-field match arms to straightforward canonical-value checks, the generated shell script references are updated to the new JSON paths, and tests are rewritten to verify the new shape and constraints.

Tutorial Steps

Remove polymorphic service and backend-mode enums

Intent: Eliminate the unused GuestServiceKind, GuestServiceBackendMode, GuestServiceLaunch, GuestOpenclawDaemonBackend, GuestAcpBackend, and GuestServiceReadinessCheck types that existed to support multiple service variants but were only ever instantiated for the OpenClaw gateway with native backend.

Affected files: crates/pika-server/src/managed_openclaw_guest.rs

Evidence
@@ -15,173 +15,48 @@ pub(crate) const GUEST_OPENCLAW_CONFIG_PATH: &str = "workspace/pika-agent/opencl
-#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
-#[serde(rename_all = "snake_case")]
-enum GuestServiceKind {
-    PikachatDaemon,
-    OpenclawGateway,
-}
-
-#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
-#[serde(rename_all = "snake_case")]
-enum GuestServiceBackendMode {
-    Native,
-    Acp,
-}
@@ -15,173 +15,48 @@
-#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
-#[serde(tag = "kind", rename_all = "snake_case")]
-enum GuestServiceLaunch {
-    PikachatDaemon {
-        #[serde(skip_serializing_if = "Option::is_none")]
-        acp_backend: Option<GuestAcpBackend>,
-    },
-    OpenclawGateway {
-        exec_command: String,
-        state_dir: String,
-        config_path: String,
-        gateway_port: u16,
-        daemon_backend: GuestOpenclawDaemonBackend,
-    },
-}
@@ -15,173 +15,48 @@
-#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
-#[serde(tag = "kind", rename_all = "snake_case")]
-enum GuestServiceReadinessCheck {
-    LogContains {
-        path: String,
-        pattern: String,
-        ready_probe: String,
-        timeout_failure_reason: String,
-    },
-    HttpGetOk {
-        url: String,
-        ready_probe: String,
-        timeout_failure_reason: String,
-    },
-}

The old design defined five enum types to model a generic "guest service" abstraction:

  • GuestServiceKind — discriminated between PikachatDaemon and OpenclawGateway
  • GuestServiceBackendMode — discriminated between Native and Acp
  • GuestServiceLaunch — a tagged union carrying launch config for either service kind, including an optional GuestOpenclawDaemonBackend and GuestAcpBackend
  • GuestServiceReadinessCheck — a tagged union supporting LogContains or HttpGetOk readiness strategies

All of these enums were only ever instantiated with the OpenclawGateway / Native / HttpGetOk variants. This branch deletes them entirely, removing roughly 125 lines of dead abstraction.

Introduce flat OpenClaw-specific startup structs

Intent: Replace the polymorphic model with three focused structs — OpenclawStartupPlan, OpenclawLaunchPlan, and OpenclawReadinessPlan — that directly represent the only configuration shape actually in use.

Affected files: crates/pika-server/src/managed_openclaw_guest.rs

Evidence
@@ -15,173 +15,48 @@
+struct OpenclawStartupPlan {
     daemon_state_dir: String,
-    service: GuestServiceLaunch,
-    readiness_check: GuestServiceReadinessCheck,
+    openclaw: OpenclawLaunchPlan,
+    readiness: OpenclawReadinessPlan,
     #[serde(default)]
     artifacts: GuestStartupArtifacts,
     exit_failure_reason: String,
 }
@@ -15,173 +15,48 @@
+struct OpenclawLaunchPlan {
     exec_command: String,
-    cwd: String,
+    state_dir: String,
+    config_path: String,
+    gateway_port: u16,
 }
@@ -15,173 +15,48 @@
+struct OpenclawReadinessPlan {
+    url: String,
+    ready_probe: String,
+    timeout_failure_reason: String,
 }

The replacement types are plain structs with no serde tag dispatch:

  • OpenclawStartupPlan — top-level plan containing a daemon_state_dir, an OpenclawLaunchPlan (field name openclaw), an OpenclawReadinessPlan (field name readiness), the existing artifacts, and exit_failure_reason.
  • OpenclawLaunchPlan — holds exec_command, state_dir, config_path, and gateway_port. These fields previously lived inside the OpenclawGateway variant of GuestServiceLaunch.
  • OpenclawReadinessPlan — holds url, ready_probe, and timeout_failure_reason, previously inside the HttpGetOk variant of GuestServiceReadinessCheck.

This flattening changes the serialized JSON shape. The service key becomes openclaw and readiness_check becomes readiness; the tagged-union kind discriminators are gone.

Simplify validation from cross-field match arms to canonical-value checks

Intent: Replace the complex validate() method that cross-checked enum discriminants across service_kind, backend_mode, and launch config variants with two simple assertions: the config path must match the canonical constant and the gateway port must match the default.

Affected files: crates/pika-server/src/managed_openclaw_guest.rs

Evidence
@@ -15,173 +15,48 @@
+impl OpenclawStartupPlan {
     fn validate(&self) -> Result<(), String> {
-        if self.service.kind() != self.service_kind {
+        if self.openclaw.config_path != GUEST_OPENCLAW_CONFIG_PATH {
             return Err(format!(
-                "guest startup plan service_kind mismatch: {:?} vs {:?}",
-                self.service_kind,
-                self.service.kind()
+                "openclaw startup plan config_path must use canonical path {:?}, got {:?}",
+                GUEST_OPENCLAW_CONFIG_PATH, self.openclaw.config_path
             ));
         }
@@ -15,173 +15,48 @@
+        if self.openclaw.gateway_port != DEFAULT_OPENCLAW_GATEWAY_PORT {
+            return Err(format!(
+                "openclaw startup plan gateway_port must stay pinned to {:?}, got {:?}",
+                DEFAULT_OPENCLAW_GATEWAY_PORT, self.openclaw.gateway_port
+            ));
+        }
         self.artifacts.validate_canonical_paths()?;
         Ok(())
     }

The previous validate() contained ~50 lines of exhaustive match arms cross-referencing service_kind, backend_mode, and the inner enum variants of GuestServiceLaunch and GuestOpenclawDaemonBackend. Most of these arms existed only to produce error messages for variant combinations that were never constructed.

The new implementation is two simple guard checks:

  1. self.openclaw.config_path must equal GUEST_OPENCLAW_CONFIG_PATH — ensures the plan always points to the canonical config location.
  2. self.openclaw.gateway_port must equal DEFAULT_OPENCLAW_GATEWAY_PORT — prevents accidental port drift.

The existing self.artifacts.validate_canonical_paths() call is retained unchanged.

Update the plan constructor and its call site

Intent: Rename guest_startup_plan() to openclaw_startup_plan() and construct the new flat structs instead of the old enum-based types.

Affected files: crates/pika-server/src/managed_openclaw_guest.rs

Evidence
@@ -299,7 +174,7 @@ pub struct ManagedVmCreateInput<'a> {
-    let startup_plan = guest_startup_plan();
+    let startup_plan = openclaw_startup_plan();
@@ -342,19 +217,16 @@
-fn guest_startup_plan() -> GuestStartupPlan {
-    GuestStartupPlan {
-        service_kind: GuestServiceKind::OpenclawGateway,
-        backend_mode: GuestServiceBackendMode::Native,
+fn openclaw_startup_plan() -> OpenclawStartupPlan {
+    OpenclawStartupPlan {
         daemon_state_dir: DEFAULT_DAEMON_STATE_DIR.to_string(),
-        service: GuestServiceLaunch::OpenclawGateway {
+        openclaw: OpenclawLaunchPlan {
             exec_command: resolved_openclaw_exec_command(),
             state_dir: DEFAULT_OPENCLAW_STATE_DIR.to_string(),
             config_path: GUEST_OPENCLAW_CONFIG_PATH.to_string(),
             gateway_port: DEFAULT_OPENCLAW_GATEWAY_PORT,
-            daemon_backend: GuestOpenclawDaemonBackend::Native,
         },
-        readiness_check: GuestServiceReadinessCheck::HttpGetOk {
+        readiness: OpenclawReadinessPlan {

The constructor is renamed from guest_startup_plan() to openclaw_startup_plan() to match the new type names. The body removes the service_kind and backend_mode fields entirely and constructs OpenclawLaunchPlan and OpenclawReadinessPlan directly. The daemon_backend: GuestOpenclawDaemonBackend::Native field is dropped since there is no longer a backend-mode concept.

The public build_managed_guest_autostart() function is updated to call the renamed constructor. The function signatures of startup_plan_file() and openclaw_gateway_config() are similarly updated to accept &OpenclawStartupPlan instead of &GuestStartupPlan.

Update generated shell script JSON path references

Intent: Change the jq-style plan_value expressions in the generated bash startup script to reference the new flat JSON keys (.openclaw.*, .readiness.*) instead of the old nested tagged-union keys (.service.*, .readiness_check.*).

Affected files: crates/pika-server/src/managed_openclaw_guest.rs

Evidence
@@ -700,9 +572,9 @@
-  local ready_probe="$(plan_value '.readiness_check.ready_probe')"
-  local readiness_url="$(plan_value '.readiness_check.url')"
-  local timeout_failure_reason="$(plan_value '.readiness_check.timeout_failure_reason')"
+  local ready_probe="$(plan_value '.readiness.ready_probe')"
+  local readiness_url="$(plan_value '.readiness.url')"
+  local timeout_failure_reason="$(plan_value '.readiness.timeout_failure_reason')"
@@ -743,12 +615,12 @@
-openclaw_exec="$(plan_value '.service.exec_command')"
-openclaw_state_dir="$(plan_value '.service.state_dir')"
-openclaw_config_path="$(plan_path "$(plan_value '.service.config_path')")"
+openclaw_exec="$(plan_value '.openclaw.exec_command')"
+openclaw_state_dir="$(plan_value '.openclaw.state_dir')"
+openclaw_config_path="$(plan_path "$(plan_value '.openclaw.config_path')")"
 ...
-gateway_port="$(plan_value '.service.gateway_port | tostring')"
+gateway_port="$(plan_value '.openclaw.gateway_port | tostring')"
@@ -783,7 +655,7 @@
-    mark_guest_failed "$(plan_value '.readiness_check.timeout_failure_reason')" 1 false
+    mark_guest_failed "$(plan_value '.readiness.timeout_failure_reason')" 1 false

The managed guest startup script is generated as a Rust string template containing embedded bash. It reads values from the JSON startup plan using a plan_value helper that invokes jq.

All jq path expressions are updated to match the new schema:

Old pathNew path
.service.exec_command.openclaw.exec_command
.service.state_dir.openclaw.state_dir
.service.config_path.openclaw.config_path
.service.gateway_port.openclaw.gateway_port
.readiness_check.url.readiness.url
.readiness_check.ready_probe.readiness.ready_probe
.readiness_check.timeout_failure_reason.readiness.timeout_failure_reason

This is a breaking change to the on-disk plan format — existing serialized plans from the old schema will not work with this code.

Rewrite tests to verify the simplified schema

Intent: Replace tests that validated the old cross-field enum consistency checks with tests that verify the new flat JSON shape and canonical-value constraints.

Affected files: crates/pika-server/src/managed_openclaw_guest.rs

Evidence
@@ -984,7 +856,7 @@
-        let plan = guest_startup_plan();
+        let plan = openclaw_startup_plan();
@@ -1007,51 +879,41 @@
-        let decoded: GuestStartupPlan =
+        let decoded: OpenclawStartupPlan =
             serde_json::from_str(startup_plan).expect("decode startup plan");
@@ -1007,51 +879,41 @@
+    fn guest_startup_plan_serializes_openclaw_only_shape() {
+        let plan = openclaw_startup_plan();
+        let encoded = serde_json::to_value(&plan).expect("encode startup plan");
+
+        assert!(encoded.get("openclaw").is_some());
+        assert!(encoded.get("readiness").is_some());
+        assert!(encoded.get("service_kind").is_none());
+        assert!(encoded.get("backend_mode").is_none());
+        assert!(encoded.get("service").is_none());
+        assert!(encoded.get("readiness_check").is_none());
     }
@@ -1007,51 +879,41 @@
+    fn guest_startup_plan_validate_rejects_non_canonical_openclaw_config_path() {
+        let mut plan = openclaw_startup_plan();
+        plan.openclaw.config_path = "workspace/custom/openclaw.json".to_string();
+
+        let err = plan
+            .validate()
+            .expect_err("plan should reject non-canonical config path");
+
+        assert!(err.contains("config_path"));
+        assert!(err.contains(GUEST_OPENCLAW_CONFIG_PATH));
     }
@@ -1075,6 +937,14 @@
+        assert!(script.contains(".openclaw.exec_command"));
+        assert!(script.contains(".openclaw.state_dir"));
+        assert!(script.contains(".openclaw.config_path"));
+        assert!(script.contains(".readiness.url"));
+        assert!(script.contains(".readiness.ready_probe"));
+        assert!(script.contains(".readiness.timeout_failure_reason"));
+        assert!(!script.contains(".service."));
+        assert!(!script.contains(".readiness_check."));

The test suite is updated in several ways:

  1. Existing tests (guest_startup_plan_uses_shared_lifecycle_artifacts, guest_startup_plan_file_round_trips_through_request_payload) now call openclaw_startup_plan() and deserialize into OpenclawStartupPlan.

  2. Removed testguest_startup_plan_validate_rejects_mismatched_service_kind is deleted since service_kind no longer exists.

  3. Replaced testguest_startup_plan_validate_rejects_openclaw_native_mode_with_acp_daemon_backend is replaced by guest_startup_plan_validate_rejects_non_canonical_openclaw_config_path, which mutates plan.openclaw.config_path and asserts the validation error mentions the canonical path constant.

  4. New testguest_startup_plan_serializes_openclaw_only_shape serializes the plan to serde_json::Value and asserts that the new keys (openclaw, readiness) are present and the old keys (service_kind, backend_mode, service, readiness_check) are absent.

  5. Script content assertions — the script generation test gains positive assertions for the new .openclaw.* and .readiness.* jq paths, plus negative assertions confirming .service. and .readiness_check. no longer appear in the generated bash script.

Diff