This branch introduces a first-class IncusGuestRole enum to the Pika codebase, replacing hard-coded image aliases and duplicated Nix image-build logic with a role-driven abstraction. A new pika-incus-guest-role crate defines two roles — managed-openclaw (the server-managed OpenClaw agent) and pikaci-runner (the CI runner) — each carrying its own default image alias and Nix flake package attribute. The role propagates through the managed-agent contract, the CI executor, and the pika-server agent API so that image selection, instance config, and provisioning parameters are all derived from the role rather than from scattered string constants. On the Nix side, duplicated image-packaging expressions are collapsed into a reusable guest-image-package.nix builder driven by a roles.nix registry, and the shell scripts are unified into a single incus-role-image.sh dispatcher. Legacy image-alias package names are preserved as aliases for backward compatibility.
Tutorial Steps
Create the `pika-incus-guest-role` crate
Intent: Define a shared enum that every crate can use to identify Incus guest roles, with serde support, `FromStr` parsing, and methods that map each role to its canonical image alias and Nix flake package attribute.
default_image_alias() — the Incus alias each role resolves to when no override is given (pika-agent/dev, pikaci/dev).
uses_default_image_alias(alias) — returns true only when the active alias matches the role default (used later to decide whether to record the role in metadata).
flake_package_attr() — the Nix flake package name for the image.
FromStr — strict parse that rejects unknown strings.
Unit tests pin serde round-trip, alias mapping, and whitespace tolerance.
Wire the guest role into `pika-managed-agent-contract`
Intent: Expose the role through the managed-agent provisioning contract so callers (CLI, server) can request a specific guest role when provisioning an agent.
The pika-managed-agent-contract crate re-exports IncusGuestRole and adds an optional guest_role field to IncusProvisionParams. The field is Option<IncusGuestRole> and serializes only when Some, preserving backward compatibility with existing JSON payloads.
The existing round-trip test is updated to set guest_role: Some(IncusGuestRole::ManagedOpenclaw).
Integrate the guest role into the `jerichoci` CI executor
Intent: Let the CI executor derive image aliases from the guest role instead of a hard-coded default, and record the role in execution metadata for traceability.
Several interconnected changes propagate the role through CI pipelines:
IncusRuntimeConfig gains a guest_role: IncusGuestRole field, set to PikaciRunner for all CI targets (crates/pikaci/src/targets.rs).
RemoteIncusContext stores the role and exposes recorded_guest_role(), which returns Some(role) only when the image alias hasn't been overridden (custom aliases suppress the role to avoid misattribution).
remote_linux_vm_incus_guest_role() resolves the role from either the PIKACI_REMOTE_LINUX_VM_INCUS_GUEST_ROLE env var or the job's JobRuntimeConfig::Incus config. An invalid env value produces a clear error.
remote_linux_vm_incus_image_alias() now accepts a guest_role parameter and falls back to guest_role.default_image_alias() instead of the old REMOTE_LINUX_VM_INCUS_IMAGE_ALIAS_DEFAULT constant (which is deleted).
RemoteLinuxVmImageRecord gains an optional guest_role field with #[serde(alias = "role")] for forward compatibility.
load_remote_incus_image_record() populates guest_role via recorded_guest_role().
New tests cover: canonical vs. custom alias recording, invalid env rejection, and legacy JSON deserialization with/without the guest_role key (including the role alias).
Propagate the guest role through pika-server's agent API
Intent: Let the server resolve, validate, merge, and materialize the guest role when provisioning managed Incus agents, and stamp it into the instance config.
@@ -3358,7 +3396,20 @@ fn resolved_incus_params ... + let guest_role = params.guest_role.unwrap_or(IncusGuestRole::ManagedOpenclaw); + anyhow::ensure!( guest_role == IncusGuestRole::ManagedOpenclaw, "managed runtime currently only supports incus.guest_role = managed-openclaw" );
@@ -1610,6 +1639,12 @@ ... + if let Some(guest_role) = materialized_guest_role(&self.resolved) { instance_config.insert( INCUS_GUEST_ROLE_CONFIG_KEY ... guest_role.as_str() ... ); }
On the server side:
ResolvedIncusParams adds a non-optional guest_role: IncusGuestRole field.
default_incus_params_from_env() now returns anyhow::Result and reads PIKA_AGENT_INCUS_GUEST_ROLE via the new incus_guest_role_from_env() helper. Invalid values produce an actionable error message.
resolved_incus_params() merges the guest role from env defaults and the caller's request, then asserts guest_role == ManagedOpenclaw (the only role the managed runtime currently supports). The image alias falls back to guest_role.default_image_alias() instead of requiring it from the env.
materialized_guest_role() mirrors the CI pattern — the role is included in materialized params only when the image alias matches the role's default.
merge_incus_provision_params() and incus_params_provided() now account for the guest_role field.
Instance creation stamps user.pika.guest_role into the Incus instance config when a role is materialized.
agent_api_healthcheck() propagates the ? from the now-fallible default_incus_params_from_env().
New tests verify: role preservation in materialized params, role omission for custom aliases, and rejection of invalid env values.
Update the CLI to pass `guest_role: None`
Intent: Keep the CLI compiling after `IncusProvisionParams` gained the new field, without changing CLI behavior.
The CLI does not yet expose a way to select a guest role, so guest_role is set to None in both the production path and the test fixture. This means the server will apply its own default (currently ManagedOpenclaw).
Refactor Nix image builds into a role-driven registry
Intent: Eliminate duplicated NixOS image-build expressions by extracting a reusable builder function and a declarative role registry.
Previously, flake.nix contained two nearly identical 40-line blocks to build the managed-agent and CI runner Incus images. This branch replaces them with:
nix/incus/guest-image-package.nix — A function that takes { system, name, packageName, description, variant, diskSize, modules, specialArgs, buildInstructions, importInstructions } and produces a derivation containing metadata.tar.xz, disk.qcow2, and a README.txt.
nix/incus/roles.nix — A declarative attrset mapping role IDs to their image parameters (system, package attr, default alias, disk size, NixOS modules, etc.).
flake.nix — Defines mkIncusGuestRoleImagePkg which composes the two, then exports:
managed-openclaw-incus-image and pikaci-runner-incus-image as the canonical package names.
pika-agent-incus-dev-image and pikaci-incus-dev-image as backward-compatible aliases pointing to the same derivations.
Unify shell scripts into `incus-role-image.sh`
Intent: Replace two nearly identical image-management shell scripts with a single role-aware dispatcher, and turn the old scripts into thin wrappers.
A new scripts/incus-role-image.sh accepts --role ROLE before the subcommand and resolves the alias and Nix package attribute from the role name. It supports build, import, and build-import subcommands, same as before.
Notable improvement: build-import now uses git ls-files -co --exclude-standard -z | tar instead of git archive, which includes untracked (but non-ignored) files — useful when building from a dirty working tree.
The legacy scripts incus-dev-image.sh and pikaci-incus-image.sh become one-liner exec wrappers that delegate to the new script with the appropriate --role flag.
Update NixOS server deployment config
Intent: Switch the production pika-server host from a hard-coded image alias to a guest role, and make `incusImageAlias` optional in the module.
The NixOS module pika-server.nix gains an optional incusGuestRole parameter. When set, it emits PIKA_AGENT_INCUS_GUEST_ROLE into the systemd environment. incusImageAlias becomes optional and is emitted only when explicitly provided — the server's Rust code now derives the alias from the guest role.
The assertion that previously required incusImageAlias is relaxed accordingly.
The production host config (pika-server.nix) switches from incusImageAlias = "pika-agent/dev" to incusGuestRole = "managed-openclaw", which resolves to the same alias via IncusGuestRole::default_image_alias().
Update downstream test fixtures
Intent: Keep tests in pika-git and other crates compiling after the struct changes.
pika-git web tests add guest_role: None to the RemoteLinuxVmImageRecord fixture.
pikaci adds the pika-incus-guest-role dependency and sets guest_role: IncusGuestRole::PikaciRunner on all CI target runtime configs, since CI jobs always run in the pikaci-runner role.