Back to feed

sledtools/pika branch #111

pika-cloud-artifact-boundary

Clarify managed guest startup artifacts

Target branch: master

Merge Commit: e53729bad954876253ebf4ed8eb79e922b0654c9

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

5 passed

head 4e62c49dd9667ba82275c482990955ebeb88d3d8 · queued 2026-03-26 17:33:31 · 5 lane(s)

queued 6s · ran 2m 06s

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

Summary

This branch refactors the GuestStartupArtifacts struct in the pika-cloud crate to draw a clear boundary between lifecycle artifacts (status, events, result) and service artifacts (log file, PID file). The previously flat fields log_path and pid_path are extracted into a new ManagedGuestServiceArtifacts struct with its own Default implementation and validate_canonical_paths method, while the existing runtime_artifacts field is renamed to lifecycle_artifacts to better communicate intent. All call-sites, validations, and tests across pika-cloud and pika-server are updated to match the new structure.

Tutorial Steps

Rename `runtime_artifacts` to `lifecycle_artifacts`

Intent: Make the field name explicitly convey that these paths (status, events, result) track the guest's lifecycle rather than being generic runtime paths.

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

Evidence
@@ -315,19 +315,59 @@
-    pub runtime_artifacts: RuntimeArtifactPaths,
+    pub lifecycle_artifacts: RuntimeArtifactPaths,
@@ -337,8 +377,8 @@
-            runtime_artifacts: RuntimeArtifactPaths::default(),
+            lifecycle_artifacts: RuntimeArtifactPaths::default(),
@@ -363,7 +393,9 @@
-        self.runtime_artifacts
+        self.lifecycle_artifacts
             .validate_canonical_paths("artifacts")?;

The runtime_artifacts field on GuestStartupArtifacts is renamed to lifecycle_artifacts. This field holds a RuntimeArtifactPaths value containing status_path, events_path, and result_path — all of which describe the guest's lifecycle state machine. The rename clarifies that these paths are specifically about lifecycle tracking, distinguishing them from the newly introduced service artifacts.

Because #[serde(flatten)] is used, the serialized JSON representation is unchanged; only the Rust field name changes. The validation call in GuestStartupArtifacts::validate_canonical_paths is updated accordingly:

self.lifecycle_artifacts
    .validate_canonical_paths("artifacts")?;

Extract `ManagedGuestServiceArtifacts` struct

Intent: Group the service-level paths (`log_path`, `pid_path`) into a dedicated struct so they can be reasoned about, defaulted, and validated independently from lifecycle artifacts.

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

Evidence
@@ -315,19 +315,59 @@
+    #[serde(flatten)]
+    pub service_artifacts: ManagedGuestServiceArtifacts,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+pub struct ManagedGuestServiceArtifacts {
+    #[serde(rename = "log_path")]
+    pub service_log_path: String,
+    pub pid_path: String,
+}

A new ManagedGuestServiceArtifacts struct is introduced to hold paths that belong to the managed guest service process rather than to the guest lifecycle state machine:

pub struct ManagedGuestServiceArtifacts {
    #[serde(rename = "log_path")]
    pub service_log_path: String,
    pub pid_path: String,
}

Key design decisions:

  • #[serde(flatten)] on the parent field preserves wire-format compatibility — log_path and pid_path still appear at the top level of the serialized JSON.
  • #[serde(rename = "log_path")] keeps the JSON key as log_path while giving the Rust field the more descriptive name service_log_path, avoiding ambiguity with other log paths in the system.
  • The struct is added to GuestStartupArtifacts as service_artifacts, replacing the two previously inlined fields (log_path and pid_path).

Add `Default` and `validate_canonical_paths` to `ManagedGuestServiceArtifacts`

Intent: Give the new struct the same self-contained default and validation behavior that `RuntimeArtifactPaths` already has, keeping the validation logic close to the data it protects.

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

Evidence
@@ -315,19 +315,59 @@
+impl Default for ManagedGuestServiceArtifacts {
+    fn default() -> Self {
+        Self {
+            service_log_path: GUEST_LOG_PATH.to_string(),
+            pid_path: GUEST_PID_PATH.to_string(),
+        }
+    }
+}
+
+impl ManagedGuestServiceArtifacts {
+    pub fn validate_canonical_paths(&self, field_prefix: &str) -> Result<(), String> {
@@ -363,7 +393,9 @@
+        self.service_artifacts
+            .validate_canonical_paths("artifacts")?;

ManagedGuestServiceArtifacts receives its own Default impl sourcing values from the existing constants GUEST_LOG_PATH and GUEST_PID_PATH, and a validate_canonical_paths method that mirrors the pattern already established by RuntimeArtifactPaths.

The validation iterates over each field, comparing the current value against the canonical default:

pub fn validate_canonical_paths(&self, field_prefix: &str) -> Result<(), String> {
    let canonical = Self::default();
    for (field, actual, expected) in [
        ("log_path", self.service_log_path.as_str(), canonical.service_log_path.as_str()),
        ("pid_path", self.pid_path.as_str(), canonical.pid_path.as_str()),
    ] {
        if actual != expected {
            return Err(format!(
                "{field_prefix}.{field} must use canonical path {expected:?}, got {actual:?}"
            ));
        }
    }
    Ok(())
}

The parent GuestStartupArtifacts::validate_canonical_paths method is simplified: the log_path and pid_path checks are removed from its inline loop and are now delegated to self.service_artifacts.validate_canonical_paths("artifacts").

Simplify `GuestStartupArtifacts::Default` and validation

Intent: Remove the now-redundant inline field defaults and validation entries from the parent struct, delegating to the two sub-structs instead.

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

Evidence
@@ -337,8 +377,8 @@
-            runtime_artifacts: RuntimeArtifactPaths::default(),
-            log_path: GUEST_LOG_PATH.to_string(),
-            pid_path: GUEST_PID_PATH.to_string(),
+            lifecycle_artifacts: RuntimeArtifactPaths::default(),
+            service_artifacts: ManagedGuestServiceArtifacts::default(),
@@ -346,16 +386,6 @@
-            (
-                "log_path",
-                self.log_path.as_str(),
-                canonical.log_path.as_str(),
-            ),
-            (
-                "pid_path",
-                self.pid_path.as_str(),
-                canonical.pid_path.as_str(),
-            ),

The Default impl for GuestStartupArtifacts now delegates to the two sub-struct defaults:

Self {
    startup_plan_path: GUEST_STARTUP_PLAN_PATH.to_string(),
    identity_seed_path: GUEST_AUTOSTART_IDENTITY_PATH.to_string(),
    lifecycle_artifacts: RuntimeArtifactPaths::default(),
    service_artifacts: ManagedGuestServiceArtifacts::default(),
}

The validate_canonical_paths method loses the six-line inline check for log_path and pid_path, and instead chains two delegated validation calls:

self.lifecycle_artifacts.validate_canonical_paths("artifacts")?;
self.service_artifacts.validate_canonical_paths("artifacts")?;

This keeps the parent struct thin and ensures each sub-struct owns its own invariants.

Update tests across both crates

Intent: Ensure all existing tests compile and assert against the new field paths, confirming no behavioral regression.

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

Evidence
@@ -937,11 +969,11 @@
-        assert_eq!(artifacts.runtime_artifacts.status_path, STATUS_PATH);
-        assert_eq!(artifacts.runtime_artifacts.events_path, EVENTS_PATH);
-        assert_eq!(artifacts.runtime_artifacts.result_path, RESULT_PATH);
-        assert_eq!(artifacts.log_path, GUEST_LOG_PATH);
-        assert_eq!(artifacts.pid_path, GUEST_PID_PATH);
+        assert_eq!(artifacts.lifecycle_artifacts.status_path, STATUS_PATH);
+        assert_eq!(artifacts.lifecycle_artifacts.events_path, EVENTS_PATH);
+        assert_eq!(artifacts.lifecycle_artifacts.result_path, RESULT_PATH);
+        assert_eq!(artifacts.service_artifacts.service_log_path, GUEST_LOG_PATH);
+        assert_eq!(artifacts.service_artifacts.pid_path, GUEST_PID_PATH);
@@ -1068,7 +1100,7 @@
-                runtime_artifacts: RuntimeArtifactPaths {
+                lifecycle_artifacts: RuntimeArtifactPaths {
@@ -704,9 +704,9 @@
-        assert_eq!(plan.artifacts.runtime_artifacts.status_path, STATUS_PATH);
-        assert_eq!(plan.artifacts.runtime_artifacts.events_path, EVENTS_PATH);
-        assert_eq!(plan.artifacts.runtime_artifacts.result_path, RESULT_PATH);
+        assert_eq!(plan.artifacts.lifecycle_artifacts.status_path, STATUS_PATH);
+        assert_eq!(plan.artifacts.lifecycle_artifacts.events_path, EVENTS_PATH);
+        assert_eq!(plan.artifacts.lifecycle_artifacts.result_path, RESULT_PATH);

All test assertions are mechanically updated to use the new field paths:

  • In pika-cloud/src/lib.rs, the default-artifacts test now reads lifecycle_artifacts.status_path, service_artifacts.service_log_path, etc.
  • A struct literal in the validation-error test switches from runtime_artifacts to lifecycle_artifacts.
  • In pika-server/src/managed_openclaw_guest.rs, the guest_startup_plan_uses_shared_lifecycle_artifacts test updates all three assertions from runtime_artifacts to lifecycle_artifacts.

No new tests are added — the existing coverage already exercises the default values and canonical-path validation, and the refactoring preserves those contracts.

Diff