Back to feed

sledtools/pika branch #118

pika-cloud-managed-startup-cut

Move managed startup contract into pika-server

Target branch: master

Merge Commit: 961f34c5d9557d00f4e9eb37d4288c84fd2b6c08

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 #145 success

5 passed

head f324f97c2e5f2371b9dfaace6c7d2ef1a7bd0c49 · queued 2026-03-26 21:48:26 · 5 lane(s)

queued 8s · ran 3m 13s

check-notifications · success check-agent-contracts · success check-pikachat · success check-apple-host-sanity · success check-fixture · success

Summary

This branch relocates the managed VM guest startup contract—including all guest path constants, the GuestStartupPlan type hierarchy, validation logic, the ManagedVmGuestAutostartRequest/ManagedVmCreateRequest envelope types, and their associated tests—out of the shared pika-cloud crate and into pika-server's managed_openclaw_guest module. The public API surface of pika-cloud shrinks significantly: types and constants that were only consumed by the server no longer leak into the shared crate. The server-side module now owns the full startup contract privately (pub(crate)), the intermediate ManagedVmCreateRequest wrapper is eliminated in favour of a flattened ManagedGuestAutostart struct, and the relocated tests are rewritten to derive plans from the canonical guest_startup_plan() builder instead of constructing standalone literals.

Tutorial Steps

Remove guest startup constants from `pika-cloud`

Intent: Eliminate the eight `GUEST_*` path constants and the unused `std::collections::BTreeMap` import from the shared crate so they are no longer part of the public API.

Affected files: crates/pika-cloud/src/lib.rs

Evidence
@@ -26,8 +26,6 @@
-use std::collections::BTreeMap;
-
@@ -117,286 +115,8 @@
-pub const GUEST_AUTOSTART_COMMAND: &str = "bash /workspace/pika-agent/start-agent.sh";
-pub const GUEST_AUTOSTART_SCRIPT_PATH: &str = "workspace/pika-agent/start-agent.sh";
-pub const GUEST_STARTUP_PLAN_PATH: &str = "workspace/pika-agent/startup-plan.json";
-pub const GUEST_AUTOSTART_IDENTITY_PATH: &str = "workspace/pika-agent/state/identity.json";
-pub const GUEST_LOG_PATH: &str = "workspace/pika-agent/agent.log";
-pub const GUEST_PID_PATH: &str = "workspace/pika-agent/agent.pid";
-pub const GUEST_OPENCLAW_CONFIG_PATH: &str = "workspace/pika-agent/openclaw/openclaw.json";
-pub const GUEST_OPENCLAW_EXTENSION_ROOT: &str =
-    "workspace/pika-agent/openclaw/extensions/pikachat-openclaw";

The branch removes all eight GUEST_* path constants (GUEST_AUTOSTART_COMMAND, GUEST_AUTOSTART_SCRIPT_PATH, GUEST_STARTUP_PLAN_PATH, GUEST_AUTOSTART_IDENTITY_PATH, GUEST_LOG_PATH, GUEST_PID_PATH, GUEST_OPENCLAW_CONFIG_PATH, GUEST_OPENCLAW_EXTENSION_ROOT) from crates/pika-cloud/src/lib.rs. These constants defined guest-internal filesystem paths that only the server's VM bootstrap logic ever referenced. Their presence in the shared pika-cloud crate leaked implementation details to every downstream consumer.

The now-unnecessary use std::collections::BTreeMap import is also removed because it was only needed by the ManagedVmGuestAutostartRequest type being deleted in the same change.

Remove guest startup type hierarchy from `pika-cloud`

Intent: Delete the full `GuestStartupPlan` type graph—`GuestServiceKind`, `GuestServiceBackendMode`, `GuestStartupPlan`, `GuestServiceLaunch`, `GuestAcpBackend`, `GuestOpenclawDaemonBackend`, `GuestServiceReadinessCheck`, `GuestStartupArtifacts`, `ManagedGuestServiceArtifacts`—plus their `Default` and `validate` impls from the shared crate.

Affected files: crates/pika-cloud/src/lib.rs

Evidence
@@ -117,286 +115,8 @@
-#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
-#[serde(rename_all = "snake_case")]
-pub enum GuestServiceKind {
-    PikachatDaemon,
-    OpenclawGateway,
-}
-...approximately 270 lines of types, impls and validation deleted...

The entire type hierarchy that models the guest startup contract is deleted from pika-cloud::lib. This includes:

  • GuestServiceKind (enum: PikachatDaemon, OpenclawGateway)
  • GuestServiceBackendMode (enum: Native, Acp)
  • GuestStartupPlan (struct + validate method)
  • GuestServiceLaunch (tagged enum + kind() helper)
  • GuestAcpBackend (struct)
  • GuestOpenclawDaemonBackend (tagged enum)
  • GuestServiceReadinessCheck (tagged enum)
  • GuestStartupArtifacts (struct + Default + validate_canonical_paths)
  • ManagedGuestServiceArtifacts (struct + Default + validate_canonical_paths)

All of these were pub and derived Serialize/Deserialize, making them part of pika-cloud's serialization contract. Since only pika-server ever constructed or consumed them, exposing them from the shared crate was unnecessary coupling.

Remove `ManagedVmCreateRequest` and `ManagedVmGuestAutostartRequest` from `pika-cloud`

Intent: Delete the request envelope types that wrapped the guest autostart payload, since the server will now use a simpler, locally-defined struct.

Affected files: crates/pika-cloud/src/lib.rs

Evidence
@@ -412,21 +132,6 @@
-#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
-pub struct ManagedVmCreateRequest {
-    pub guest_autostart: ManagedVmGuestAutostartRequest,
-}
-
-#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
-pub struct ManagedVmGuestAutostartRequest {
-    pub command: String,
-    #[serde(default)]
-    pub env: BTreeMap<String, String>,
-    #[serde(default)]
-    pub files: BTreeMap<String, String>,
-    pub startup_plan: GuestStartupPlan,
-}

ManagedVmCreateRequest was a single-field wrapper holding ManagedVmGuestAutostartRequest. The inner struct carried the guest autostart command, environment, files map, and a GuestStartupPlan. Both are deleted.

The wrapper added an unnecessary layer of indirection—the calling code in agent_api.rs always immediately destructured it to access guest_autostart. The replacement ManagedGuestAutostart struct (introduced in the server crate) drops the startup_plan field entirely because the plan is now serialised directly into the files map by the builder function, eliminating the typed dependency on GuestStartupPlan at the request boundary.

Remove guest startup tests from `pika-cloud`

Intent: Delete all tests that exercised the now-removed types and constants, since the tests will be relocated alongside the types.

Affected files: crates/pika-cloud/src/lib.rs

Evidence
@@ -908,12 +613,6 @@
-    #[test]
-    fn managed_vm_create_request_requires_guest_autostart() {
-        ...
-    }
@@ -960,156 +659,6 @@
-    #[test]
-    fn guest_startup_artifacts_default_to_shared_paths() { ... }
-    #[test]
-    fn guest_startup_plan_round_trips_through_guest_autostart_request() { ... }
-    #[test]
-    fn guest_autostart_request_rejects_missing_startup_plan() { ... }
-    #[test]
-    fn guest_startup_plan_validate_rejects_mismatched_service_kind() { ... }
-    #[test]
-    fn guest_startup_plan_validate_rejects_openclaw_native_mode_with_acp_daemon_backend() { ... }
-    #[test]
-    fn guest_startup_plan_validate_rejects_non_canonical_artifact_paths() { ... }

Seven tests are removed from pika-cloud:

  1. managed_vm_create_request_requires_guest_autostart — validated deserialization of the deleted wrapper.
  2. guest_startup_artifacts_default_to_shared_paths — checked Default impl of GuestStartupArtifacts.
  3. guest_startup_plan_round_trips_through_guest_autostart_request — serialization round-trip for ManagedVmGuestAutostartRequest.
  4. guest_autostart_request_rejects_missing_startup_plan — negative deserialization test.
  5. guest_startup_plan_validate_rejects_mismatched_service_kind — validation logic test.
  6. guest_startup_plan_validate_rejects_openclaw_native_mode_with_acp_daemon_backend — validation logic test.
  7. guest_startup_plan_validate_rejects_non_canonical_artifact_paths — validation logic test.

All seven are recreated in pika-server in the next steps.

Introduce private guest startup constants and type hierarchy in `pika-server`

Intent: Re-establish the full startup contract as `pub(crate)` definitions inside the server's `managed_openclaw_guest` module, reducing the visibility to crate-internal only.

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

Evidence
@@ -1,15 +298,7 @@
+pub(crate) const GUEST_AUTOSTART_COMMAND: &str = "bash /workspace/pika-agent/start-agent.sh";
+pub(crate) const GUEST_AUTOSTART_SCRIPT_PATH: &str = "workspace/pika-agent/start-agent.sh";
+...
@@ +30,0 @@
+#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "snake_case")]
+enum GuestServiceKind {
+    PikachatDaemon,
+    OpenclawGateway,
+}
+...full type hierarchy recreated with private visibility...

The eight GUEST_* constants are redeclared as pub(crate) const in managed_openclaw_guest.rs. The full type hierarchy—GuestServiceKind, GuestServiceBackendMode, GuestStartupPlan, GuestServiceLaunch, GuestAcpBackend, GuestOpenclawDaemonBackend, GuestServiceReadinessCheck, GuestStartupArtifacts, and ManagedGuestServiceArtifacts—is recreated with private (non-pub) visibility on types and fields.

Key design decisions:

  • All types lose their pub qualifier; they are file-private, used only by the builder function and tests.
  • GuestStartupArtifacts still references pika_cloud::RuntimeArtifactPaths via #[serde(flatten)], preserving the shared lifecycle path contract.
  • Validation logic (validate, validate_canonical_paths) is reproduced verbatim but with fn instead of pub fn.
  • serde derives remain identical, so the JSON wire format is unchanged.

Replace `ManagedVmCreateRequest` with `ManagedGuestAutostart`

Intent: Introduce a simpler `pub(crate)` autostart payload struct that carries only command, env, and files—without a typed `startup_plan` field—and rename the builder function accordingly.

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

Evidence
@@ +237,0 @@
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+pub(crate) struct ManagedGuestAutostart {
+    pub command: String,
+    pub env: BTreeMap<String, String>,
+    pub files: BTreeMap<String, String>,
+}
@@ -26,7 +298,7 @@
-pub fn build_managed_vm_create_request(input: ManagedVmCreateInput<'_>) -> ManagedVmCreateRequest {
+pub fn build_managed_guest_autostart(input: ManagedVmCreateInput<'_>) -> ManagedGuestAutostart {
@@ -63,13 +335,10 @@
-    ManagedVmCreateRequest {
-        guest_autostart: ManagedVmGuestAutostartRequest {
-            command: GUEST_AUTOSTART_COMMAND.to_string(),
-            env,
-            files,
-            startup_plan,
-        },
+    ManagedGuestAutostart {
+        command: GUEST_AUTOSTART_COMMAND.to_string(),
+        env,
+        files,
     }

The two-level nesting of ManagedVmCreateRequestManagedVmGuestAutostartRequest is replaced by a single flat ManagedGuestAutostart struct with three fields: command, env, and files.

The startup_plan field is intentionally absent from the new struct. The builder function build_managed_guest_autostart still constructs a GuestStartupPlan internally, validates it, serialises it to JSON, and inserts it into the files map under GUEST_STARTUP_PLAN_PATH. This means the startup plan travels as an opaque file payload rather than a typed field, which is exactly how the VM guest consumes it.

The function rename from build_managed_vm_create_request to build_managed_guest_autostart reflects the eliminated wrapper.

Update `agent_api.rs` to use the new builder and imports

Intent: Rewire the call-site in the Incus managed runtime provider to consume the renamed function and locally-sourced constants.

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

Evidence
@@ -23,8 +23,8 @@
-use crate::managed_openclaw_guest::{
-    build_managed_vm_create_request, ManagedVmCreateInput as ManagedRuntimeCreateInput,
-    DEFAULT_OPENCLAW_GATEWAY_PORT,
+use crate::managed_openclaw_guest::{
+    build_managed_guest_autostart, ManagedVmCreateInput as ManagedRuntimeCreateInput,
+    DEFAULT_OPENCLAW_GATEWAY_PORT, GUEST_AUTOSTART_SCRIPT_PATH, GUEST_OPENCLAW_CONFIG_PATH,
@@ -1825,8 +1825,7 @@
-        let bootstrap_request = build_managed_vm_create_request(*input);
-        let guest_autostart = bootstrap_request.guest_autostart;
+        let guest_autostart = build_managed_guest_autostart(*input);
@@ -876,7 +876,7 @@
-        let path = format!("/{}", pika_cloud::GUEST_OPENCLAW_CONFIG_PATH);
+        let path = format!("/{}", GUEST_OPENCLAW_CONFIG_PATH);
@@ -2469,7 +2468,7 @@
-    if path == pika_cloud::GUEST_AUTOSTART_SCRIPT_PATH {
+    if path == GUEST_AUTOSTART_SCRIPT_PATH {

Three changes in agent_api.rs:

  1. Import updatebuild_managed_vm_create_request becomes build_managed_guest_autostart; GUEST_AUTOSTART_SCRIPT_PATH and GUEST_OPENCLAW_CONFIG_PATH are now imported from crate::managed_openclaw_guest instead of pika_cloud.

  2. Call-site simplification — The two-step pattern build_managed_vm_create_request(*input)bootstrap_request.guest_autostart collapses to a single build_managed_guest_autostart(*input) call, directly yielding the autostart payload.

  3. Constant referencespika_cloud::GUEST_OPENCLAW_CONFIG_PATH and pika_cloud::GUEST_AUTOSTART_SCRIPT_PATH are replaced with the crate-local imports in read_openclaw_config_from_instance and bootstrap_file_permissions.

Relocate and improve tests in `managed_openclaw_guest`

Intent: Re-establish full test coverage for the startup contract in the server crate, rewriting tests to derive plans from the canonical builder rather than constructing standalone literals.

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

Evidence
@@ +969,19 @@
+    #[test]
+    fn guest_startup_artifacts_default_to_shared_paths() {
+        let artifacts = GuestStartupArtifacts::default();
+        assert_eq!(artifacts.startup_plan_path, GUEST_STARTUP_PLAN_PATH);
+        ...
@@ +991,83 @@
+    #[test]
+    fn guest_startup_plan_file_round_trips_through_request_payload() { ... }
+    #[test]
+    fn guest_startup_plan_validate_rejects_mismatched_service_kind() { ... }
+    #[test]
+    fn guest_startup_plan_validate_rejects_openclaw_native_mode_with_acp_daemon_backend() { ... }
+    #[test]
+    fn guest_startup_plan_validate_rejects_non_canonical_artifact_paths() { ... }

Five tests are added to the managed_openclaw_guest::tests module:

  1. guest_startup_artifacts_default_to_shared_paths — Direct port from pika-cloud; verifies Default for GuestStartupArtifacts uses the canonical constants.

  2. guest_startup_plan_file_round_trips_through_request_payload — Replaces the old guest_startup_plan_round_trips_through_guest_autostart_request. Instead of constructing a ManagedVmGuestAutostartRequest with a typed startup_plan field, it builds a ManagedGuestAutostart whose files map contains the serialised plan, then deserialises from the map entry and asserts equality. This reflects the new contract where the plan lives as an opaque file.

  3. guest_startup_plan_validate_rejects_mismatched_service_kind — Starts from guest_startup_plan() and mutates service_kind to PikachatDaemon, avoiding the need to construct a full plan literal. Simpler and less brittle.

  4. guest_startup_plan_validate_rejects_openclaw_native_mode_with_acp_daemon_backend — Starts from guest_startup_plan() and replaces the service field with an ACP-backed OpenClaw variant while leaving backend_mode as Native.

  5. guest_startup_plan_validate_rejects_non_canonical_artifact_paths — Starts from guest_startup_plan() and overrides artifacts.lifecycle_artifacts.status_path to a non-canonical value.

Notably absent is the old managed_vm_create_request_requires_guest_autostart test (which validated ManagedVmCreateRequest deserialization) and guest_autostart_request_rejects_missing_startup_plan — both are obsolete because the ManagedGuestAutostart struct no longer carries a typed startup_plan field. The old guest_startup_plan_round_trips_through_guest_autostart_request test that constructed a standalone OpenClaw plan literal is replaced by the file-based round-trip test that uses the canonical builder output.

Diff