Back to feed

sledtools/pika branch #116

pika-cloud-spec-simplify

Simplify runtime spec shape

Target branch: master

Merge Commit: 948993d7ec1e517d553f054f54ecc0ef8681789b

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

7 passed

head 665b36bb789071961dcc3d733897378ba1693eeb · queued 2026-03-26 21:07:01 · 7 lane(s)

queued 7s · ran 2m 06s

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

Summary

This branch simplifies the runtime spec shape in the pika-cloud and pikaci crates by removing the RuntimeBootstrap struct and the lifecycle_root field. The entry_command field is promoted from a nested RuntimeBootstrap member to a top-level Option<String> on both RuntimeSpec and IncusRuntimePlan. The guest_request_path bootstrap field is eliminated entirely since it was redundant with RuntimePaths::guest_request_path. The lifecycle_root field and its associated default_runtime_root() helper are removed, with all path-containment validation now anchoring directly against paths.state_dir. Corresponding error variants (MismatchedStateRoot, MismatchedGuestRequestPath) are deleted, and all tests and downstream consumers are updated to reflect the flattened structure.

Tutorial Steps

Remove the RuntimeBootstrap struct

Intent: Eliminate the intermediate `RuntimeBootstrap` struct that wrapped `guest_request_path` and `entry_command`, since `guest_request_path` was redundant with `RuntimePaths` and `entry_command` can live directly on the spec.

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

Evidence
@@ -27,14 +27,6 @@ pub struct RuntimeResources {
-#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
-pub struct RuntimeBootstrap {
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub guest_request_path: Option<String>,
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub entry_command: Option<String>,
-}
@@ -23,8 +23,7 @@ pub use policy::{
-    IncusRuntimeConfig, RuntimeBootstrap, RuntimeIdentity, RuntimeResources, RuntimeSpec,
-    RuntimeSpecError,
+    IncusRuntimeConfig, RuntimeIdentity, RuntimeResources, RuntimeSpec, RuntimeSpecError,

The RuntimeBootstrap struct carried two optional fields: guest_request_path and entry_command. Analysis showed that guest_request_path duplicated RuntimePaths::guest_request_path and required a cross-validation check (MismatchedGuestRequestPath) to keep them in sync. Rather than maintain this redundancy, the struct is deleted entirely.

In spec.rs, the struct definition and its Default derive are removed. The re-export in lib.rs is updated to drop RuntimeBootstrap from the public API surface.

The entry_command field is preserved but relocated (covered in the next step).

Promote entry_command to a top-level field on RuntimeSpec and IncusRuntimePlan

Intent: Flatten the spec shape by moving `entry_command` from `RuntimeBootstrap` to a direct `Option<String>` field on `RuntimeSpec` and `IncusRuntimePlan`.

Affected files: crates/pika-cloud/src/spec.rs, crates/pika-cloud/src/incus.rs

Evidence
@@ -51,24 +43,18 @@ pub struct RuntimeSpec {
-    #[serde(default)]
-    pub bootstrap: RuntimeBootstrap,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub entry_command: Option<String>,
@@ -38,10 +36,10 @@ pub struct IncusRuntimePlan {
-    pub bootstrap: RuntimeBootstrap,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub entry_command: Option<String>,

Both RuntimeSpec and IncusRuntimePlan replace their bootstrap: RuntimeBootstrap field with entry_command: Option<String>. The serde attributes default and skip_serializing_if = "Option::is_none" ensure backward-compatible deserialization and clean serialization.

The builder method with_bootstrap is renamed to with_entry_command, taking an impl Into<String> argument:

pub fn with_entry_command(mut self, entry_command: impl Into<String>) -> Self {
    self.entry_command = Some(entry_command.into());
    self
}

The new() constructor initializes entry_command: None in place of bootstrap: RuntimeBootstrap::default().

Remove lifecycle_root field and default_runtime_root helper

Intent: Eliminate the `lifecycle_root` field from `RuntimeSpec` and `IncusRuntimePlan`, anchoring path validation directly to `paths.state_dir` instead.

Affected files: crates/pika-cloud/src/spec.rs, crates/pika-cloud/src/incus.rs

Evidence
@@ -51,24 +43,18 @@ pub struct RuntimeSpec {
-    #[serde(default = "default_runtime_root")]
-    pub lifecycle_root: String,
@@ -79,17 +65,9 @@ pub enum RuntimeSpecError {
-    MismatchedStateRoot {
-        lifecycle_root: String,
-        state_dir: String,
-    },
@@ -301,13 +245,13 @@ fn ensure_under_root(
-    lifecycle_root: &str,
+    state_dir: &str,

lifecycle_root previously served as the anchor directory for path-containment checks, with a validation rule that it must equal paths.state_dir. Since these were always required to match, the indirection was unnecessary.

Changes:

  1. The lifecycle_root field is removed from both RuntimeSpec and IncusRuntimePlan.
  2. The default_runtime_root() function is deleted.
  3. The ensure_under_root helper's parameter is renamed from lifecycle_root to state_dir, and all call sites now pass &self.paths.state_dir directly.
  4. The MismatchedStateRoot error variant is removed since the check is no longer needed.
  5. The RUNTIME_ROOT import in spec.rs is dropped (it was only used by default_runtime_root).

Remove redundant validation error variants

Intent: Delete error variants that validated now-removed fields, simplifying the RuntimeSpecError enum and its Display implementation.

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

Evidence
@@ -79,17 +65,9 @@ pub enum RuntimeSpecError {
-    MismatchedStateRoot {
-        lifecycle_root: String,
-        state_dir: String,
-    },
-    MismatchedGuestRequestPath {
-        bootstrap_path: String,
-        runtime_path: String,
-    },
@@ -106,26 +84,12 @@ impl fmt::Display for RuntimeSpecError {
-            Self::MismatchedStateRoot { .. } => write!(
-            Self::MismatchedGuestRequestPath { .. } => write!(

Two error variants are removed from RuntimeSpecError:

  • MismatchedStateRoot — validated that lifecycle_root == paths.state_dir. No longer needed since lifecycle_root is gone.
  • MismatchedGuestRequestPath — validated that bootstrap.guest_request_path == paths.guest_request_path. No longer needed since guest_request_path is no longer part of bootstrap.

Their corresponding Display format arms are also removed. The error message for ensure_under_root is updated from "must stay under lifecycle_root" to "must stay under paths.state_dir".

Update downstream consumers in pikaci

Intent: Adapt the pikaci executor crate to use the simplified spec API, removing references to RuntimeBootstrap and GUEST_REQUEST_PATH.

Affected files: crates/pikaci/src/executor.rs, crates/pikaci/src/executor/incus.rs

Evidence
@@ -11,9 +11,9 @@ use pika_cloud::{
-    CLOUD_GUEST_LOG_PATH, EVENTS_PATH, GUEST_REQUEST_PATH, IncusGuestRunRequest,
+    CLOUD_GUEST_LOG_PATH, EVENTS_PATH, IncusGuestRunRequest, LIFECYCLE_SCHEMA_VERSION, RESULT_PATH,
@@ -92,10 +91,7 @@ fn build_remote_incus_runtime_plan(
-    .with_bootstrap(RuntimeBootstrap {
-        guest_request_path: Some(GUEST_REQUEST_PATH.to_string()),
-        entry_command: Some(REMOTE_LINUX_VM_INCUS_RUN_BINARY.to_string()),
-    })
+    .with_entry_command(REMOTE_LINUX_VM_INCUS_RUN_BINARY)

In crates/pikaci/src/executor.rs, the GUEST_REQUEST_PATH import is removed since it is no longer set via the bootstrap struct.

In crates/pikaci/src/executor/incus.rs:

  • The RuntimeBootstrap import is dropped.
  • The build_remote_incus_runtime_plan function replaces the .with_bootstrap(RuntimeBootstrap { ... }) call with .with_entry_command(REMOTE_LINUX_VM_INCUS_RUN_BINARY), which is simpler and avoids constructing an intermediate struct.

The guest_request_path value is no longer explicitly set during plan construction — it comes from RuntimePaths::default() automatically, eliminating the previous source of potential mismatch.

Update tests to reflect the simplified shape

Intent: Ensure all unit tests validate the new flattened structure and cover the updated validation rules.

Affected files: crates/pika-cloud/src/spec.rs, crates/pika-cloud/src/incus.rs

Evidence
@@ -346,10 +290,7 @@ mod tests {
-        .with_bootstrap(RuntimeBootstrap {
-            guest_request_path: Some("/run/pika-cloud/guest-request.json".to_string()),
-            entry_command: Some("/run/current-system/sw/bin/pikaci-incus-run".to_string()),
-        });
+        .with_entry_command("/run/current-system/sw/bin/pikaci-incus-run");
@@ -492,25 +431,24 @@ mod tests {
-    fn validate_rejects_mismatched_bootstrap_guest_request_path() {
+    fn validate_rejects_empty_entry_command() {
@@ -385,16 +326,15 @@ mod tests {
-        assert_eq!(spec.lifecycle_root, RUNTIME_ROOT);
-        assert_eq!(spec.bootstrap, RuntimeBootstrap::default());
+        assert_eq!(spec.entry_command, None);

Key test changes:

  1. sample_runtime_spec now uses .with_entry_command("/run/current-system/sw/bin/pikaci-incus-run") instead of constructing a RuntimeBootstrap.

  2. Default assertions check spec.entry_command == None instead of comparing against RuntimeBootstrap::default().

  3. validate_rejects_mismatched_bootstrap_guest_request_path is replaced by validate_rejects_empty_entry_command, which sets entry_command = Some(" ") and asserts that validation produces EmptyField { field: "entry_command" }.

  4. Plan equality checks assert plan.entry_command == spec.entry_command instead of plan.bootstrap == spec.bootstrap.

  5. Path validation tests update expected error messages from "must stay under lifecycle_root" to "must stay under paths.state_dir".

  6. The test runtime_spec_defaults_lifecycle_root_to_runtime_root is renamed to runtime_spec_defaults_paths_to_runtime_root and no longer asserts on lifecycle_root.

Diff