Provision bindgen dependencies in the default Nix dev shell
Intent: The nightly CI lane runs inside `nix develop .#default`, which lacked `linuxHeaders` and the `BINDGEN_EXTRA_CLANG_ARGS` environment variable. When `pika-desktop` depends on `nokhwa` (a camera crate using bindgen), the build fails because bindgen cannot locate kernel and libc headers. This step adds those inputs and exports to the default Linux dev shell.
Affected files: flake.nix
Evidence
@@ -520,6 +520,11 @@
+ linuxBindgenIncludeArgs =
+ if pkgs.stdenv.isLinux then
+ "-I${pkgs.linuxHeaders}/include -I${pkgs.stdenv.cc.libc.dev}/include"
+ else
+ "";
@@ -738,6 +743,7 @@
+ pkgs.linuxHeaders
@@ -770,6 +787,7 @@
+ export BINDGEN_EXTRA_CLANG_ARGS="${linuxBindgenIncludeArgs}''${BINDGEN_EXTRA_CLANG_ARGS:+ $BINDGEN_EXTRA_CLANG_ARGS}"
The nokhwa camera crate (a dependency of pika-desktop) uses bindgen at build time to generate Rust FFI bindings for V4L2 headers. On NixOS/Nix-shell environments the standard system include paths are absent, so bindgen cannot find <linux/videodev2.h> or libc headers.
Three changes are made to flake.nix:
- A new
let binding linuxBindgenIncludeArgs constructs the -I flags pointing at pkgs.linuxHeaders and pkgs.stdenv.cc.libc.dev (Linux-only; empty string on macOS).
pkgs.linuxHeaders is added to the buildInputs list alongside the existing pkgs.llvmPackages.libclang so the headers are physically available in the shell.
BINDGEN_EXTRA_CLANG_ARGS is exported in the shell hook, prepending the computed include args while preserving any pre-existing value.
These three pieces work together: libclang provides the clang library that bindgen links against, linuxHeaders provides the kernel UAPI headers, and the environment variable tells bindgen where to find them.
Export the Android AAPT2 override for nightly Gradle builds
Intent: Nightly CI runs Android integration tests inside the same default dev shell. The Nix-provided Android SDK ships `aapt2` at a path that Gradle does not discover automatically, causing Android resource compilation to fail. This step detects the installed `aapt2` binary and exports `PIKACI_ANDROID_AAPT2_OVERRIDE` plus the corresponding Gradle system property.
Affected files: flake.nix
Evidence
@@ -759,6 +765,17 @@
+ unset PIKACI_ANDROID_AAPT2_OVERRIDE
+ for candidate in \
+ "$ANDROID_HOME/build-tools/35.0.0/aapt2" \
+ "$ANDROID_HOME/build-tools/34.0.0/aapt2"
+ do
+ if [ -x "$candidate" ]; then
+ export PIKACI_ANDROID_AAPT2_OVERRIDE="$candidate"
+ export GRADLE_OPTS="''${GRADLE_OPTS:+$GRADLE_OPTS }-Dorg.gradle.project.android.aapt2FromMavenOverride=$PIKACI_ANDROID_AAPT2_OVERRIDE"
+ break
+ fi
+ done
When the Nix-managed Android SDK is present, Gradle cannot resolve aapt2 from Maven because the Nix sandbox blocks network fetches. The fix adds a candidate loop in the shell hook that checks for aapt2 under build-tools 35.0.0 and 34.0.0 (in preference order). When found, it:
- Exports
PIKACI_ANDROID_AAPT2_OVERRIDE for any scripts that reference it directly.
- Appends
-Dorg.gradle.project.android.aapt2FromMavenOverride=... to GRADLE_OPTS, which tells the Android Gradle Plugin to use the local binary instead of downloading one from Google's Maven repository.
The unset at the top ensures the variable is clean if the shell hook runs more than once.
Add guardrail tests for the Nix dev shell invariants
Intent: To prevent these Nix regressions from silently reappearing, two new unit tests are added to `jerichoci::model::tests`. They parse `flake.nix` and assert the presence of the critical strings, giving a clear failure message that links cause to fix.
Affected files: crates/jerichoci/src/model.rs
Evidence
@@ -1545,6 +1545,34 @@
+ #[test]
+ fn default_linux_dev_shell_provisions_bindgen_for_nightly_camera_dependencies() {
+ let flake = fs::read_to_string(workspace_root().join("flake.nix")).expect("read flake.nix");
+ let desktop_manifest =
+ fs::read_to_string(workspace_root().join("crates/pika-desktop/Cargo.toml"))
+ .expect("read pika-desktop Cargo.toml");
+
+ if desktop_manifest.contains("nokhwa") {
+ assert!(
+ flake.contains("pkgs.llvmPackages.libclang")
+ && flake.contains("pkgs.linuxHeaders")
+ && flake.contains("export BINDGEN_EXTRA_CLANG_ARGS="),
@@ -1545,6 +1545,34 @@
+ #[test]
+ fn default_linux_dev_shell_exports_android_aapt2_override_for_nightly() {
+ let flake = fs::read_to_string(workspace_root().join("flake.nix")).expect("read flake.nix");
+
+ assert!(
+ flake.contains("PIKACI_ANDROID_AAPT2_OVERRIDE")
+ && flake.contains("android.aapt2FromMavenOverride"),
Two new #[test] functions in crates/jerichoci/src/model.rs act as living documentation and regression detectors:
default_linux_dev_shell_provisions_bindgen_for_nightly_camera_dependencies
This test is conditional: it only asserts when pika-desktop/Cargo.toml still lists nokhwa as a dependency. When active, it checks that flake.nix contains:
pkgs.llvmPackages.libclang (the clang library for bindgen)
pkgs.linuxHeaders (kernel UAPI headers)
export BINDGEN_EXTRA_CLANG_ARGS= (the environment variable that passes include paths)
default_linux_dev_shell_exports_android_aapt2_override_for_nightly
This test unconditionally verifies that flake.nix references both PIKACI_ANDROID_AAPT2_OVERRIDE and android.aapt2FromMavenOverride, ensuring the Gradle override path remains wired up.
Both tests produce descriptive failure messages explaining why the invariant matters and which CI lanes depend on it.
Add `cargo build -p pikachat` to the nightly-pika-e2e recipe
Intent: The `call_with_pikachat_daemon_boundary` integration test expects a pre-built `pikachat` binary at `target/debug/pikachat`. The Just recipe was missing the explicit build step, causing the test to fail with a 'binary not found' error on clean CI runners.
Affected files: just/checks.just
Evidence
@@ -254,6 +254,7 @@
nightly-pika-e2e:
+ cargo build -p pikachat
cargo test -p pikahut --test integration_deterministic call_over_local_moq_relay_boundary -- --ignored --nocapture
The nightly-pika-e2e recipe in just/checks.just runs several --ignored integration tests that exercise heavy call-path regression boundaries. The call_with_pikachat_daemon_boundary test spawns pikachat as a child process, expecting the binary to already exist at target/debug/pikachat.
On a clean CI runner (or after cargo clean), the binary does not exist, and the test fails with a confusing "No such file or directory" error rather than a meaningful build failure.
The fix adds cargo build -p pikachat as the first line of the recipe, ensuring the binary is compiled before any test attempts to spawn it. This is intentionally a separate build step (not cargo test --build) because the daemon boundary test locates the binary by path, not through the test harness.
Add a guardrail test for the pikachat build step
Intent: A new test in `pikahut/tests/guardrails.rs` parses the Just recipe and asserts that `cargo build -p pikachat` appears before the daemon boundary test commands, preventing the build step from being accidentally removed in the future.
Affected files: crates/pikahut/tests/guardrails.rs
Evidence
@@ -416,6 +416,26 @@
+#[test]
+fn nightly_pika_lane_builds_pikachat_binary_before_daemon_boundary() -> Result<()> {
+ let root = workspace_root();
+ let checks = fs::read_to_string(root.join("just/checks.just"))?;
+ let recipe = extract_just_recipe_body(&checks, "nightly-pika-e2e");
+
+ assert!(
+ !recipe.is_empty(),
+ "checks.just must keep a checked-in nightly-pika-e2e recipe body"
+ );
+ assert!(
+ recipe
+ .iter()
+ .any(|line| line.contains("cargo build -p pikachat")),
+ "nightly-pika-e2e must build pikachat before the daemon-boundary selector expects target/debug/pikachat"
+ );
This guardrail test uses the existing extract_just_recipe_body helper to parse the nightly-pika-e2e recipe out of just/checks.just and verify two invariants:
- The recipe body is non-empty (the recipe hasn't been accidentally deleted).
- At least one line contains
cargo build -p pikachat.
The assertion message explicitly states why the build step is needed ("the daemon-boundary selector expects target/debug/pikachat"), making future debugging straightforward if someone removes the line.
Adapt pikachat-openclaw imports to upstream SDK changes
Intent: The upstream `openclaw/plugin-sdk` moved `DEFAULT_ACCOUNT_ID` to `openclaw/plugin-sdk/account-id`, moved `ChannelThreadingToolContext` to `openclaw/plugin-sdk/channel-contract`, removed the `formatPairingApproveHint` export, and renamed `listActions` to `describeMessageTool` with a new return shape. These import and API changes broke compilation of the pikachat-openclaw extension.
Affected files: pikachat-openclaw/openclaw/extensions/pikachat-openclaw/src/channel.ts, pikachat-openclaw/openclaw/extensions/pikachat-openclaw/src/types.ts
Evidence
@@ -1,12 +1,11 @@
import {
- DEFAULT_ACCOUNT_ID,
- formatPairingApproveHint,
type AnyAgentTool,
type ChannelMessageActionContext,
type ChannelMessageActionName,
type ChannelPlugin,
- type ChannelThreadingToolContext,
} from "openclaw/plugin-sdk";
+import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
+import type { ChannelThreadingToolContext } from "openclaw/plugin-sdk/channel-contract";
@@ -118,6 +117,10 @@
+function formatPairingApproveHint(channelId: string): string {
+ return `Approve via: \`openclaw pairing list ${channelId}\` / \`openclaw pairing approve ${channelId} <code>\``;
+}
@@ -740,7 +743,9 @@
actions: {
- listActions: (): ChannelMessageActionName[] => ["react"],
+ describeMessageTool: () => ({
+ actions: ["react"] as const,
+ }),
@@ -1,4 +1,5 @@
-import { DEFAULT_ACCOUNT_ID, type OpenClawConfig } from "openclaw/plugin-sdk";
+import type { OpenClawConfig } from "openclaw/plugin-sdk";
+import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
The upstream openclaw/plugin-sdk package introduced breaking changes that affected the pikachat-openclaw extension in three ways:
Import path relocations
DEFAULT_ACCOUNT_ID moved from openclaw/plugin-sdk to openclaw/plugin-sdk/account-id. Both channel.ts and types.ts are updated to import from the new path. Similarly, ChannelThreadingToolContext moved to openclaw/plugin-sdk/channel-contract.
Removed export: formatPairingApproveHint
The SDK no longer exports this helper. Since the function is small and specific to the pikachat pairing flow, it is inlined directly in channel.ts as a local function with the same signature and behavior.
Renamed action method: listActions → describeMessageTool
The ChannelPlugin.actions contract changed from a listActions() method returning ChannelMessageActionName[] to a describeMessageTool() method returning an object with an actions array. The implementation is updated accordingly, using as const for type narrowing on the string literal array.