feat: add nix manifest profile foundation

This commit is contained in:
2026-05-30 01:41:06 +09:00
parent 1d64c50cf2
commit 5de31a9be2
16 changed files with 850 additions and 23 deletions
+5
View File
@@ -806,6 +806,7 @@ mod tests {
child("child-stale", &stale_socket),
child("child-pending", &pending_socket),
],
resolved_manifest_snapshot: None,
};
store.write(&parent).unwrap();
store
@@ -816,6 +817,7 @@ mod tests {
active_child_segment,
)),
spawned_children: Vec::new(),
resolved_manifest_snapshot: None,
})
.unwrap();
store
@@ -826,6 +828,7 @@ mod tests {
active_child_segment,
)),
spawned_children: Vec::new(),
resolved_manifest_snapshot: None,
})
.unwrap();
store
@@ -833,6 +836,7 @@ mod tests {
pod_name: "child-pending".into(),
active: Some(PodActiveSegmentRef::pending_segment(pending_session_id)),
spawned_children: Vec::new(),
resolved_manifest_snapshot: None,
})
.unwrap();
store
@@ -843,6 +847,7 @@ mod tests {
new_segment_id(),
)),
spawned_children: Vec::new(),
resolved_manifest_snapshot: None,
})
.unwrap();
+79 -1
View File
@@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::Parser;
use manifest::{PodManifest, PodManifestConfig, paths};
use manifest::{NixProfileResolver, PodManifest, PodManifestConfig, ProfileSelector, paths};
use pod::{Pod, PodController, PodFactory, PromptLoader};
use session_store::{FsStore, PodMetadataStore, SegmentId, Store};
@@ -13,6 +13,21 @@ use session_store::{FsStore, PodMetadataStore, SegmentId, Store};
about = "Spawn a Pod process from manifest layers or a single manifest file"
)]
struct Cli {
/// Nix profile to evaluate with `nix eval --json --file <PATH>`.
/// Profiles are resolved artifacts, not manifest-cascade layers.
#[arg(
long,
value_name = "PATH",
conflicts_with_all = ["manifest", "project", "overlay", "pod", "session", "adopt"]
)]
profile: Option<PathBuf>,
/// Pod name override for a freshly-created profile Pod. This does not use
/// `--pod` restore semantics, so it must not attach/restore existing Pod
/// state by re-evaluating the profile source.
#[arg(long, value_name = "NAME", requires = "profile", conflicts_with_all = ["pod", "session", "adopt"])]
profile_pod_name: Option<String>,
/// Manifest TOML to use directly, without loading user, project, or
/// overlay layers.
#[arg(long, value_name = "PATH", conflicts_with_all = ["project", "overlay"])]
@@ -75,6 +90,16 @@ fn resolve_manifest_with_user_manifest_env(
) -> Result<(PodManifest, PromptLoader), String> {
let user_manifest = paths::user_manifest_path_from_env(user_manifest_env);
if let Some(path) = &cli.profile {
if user_manifest.is_some() {
return Err(format!(
"--profile cannot be used when {} is set",
paths::USER_MANIFEST_ENV
));
}
return load_profile(path, cli.profile_pod_name.as_deref());
}
if let Some(path) = &cli.manifest {
if user_manifest.is_some() {
return Err(format!(
@@ -91,6 +116,20 @@ fn resolve_manifest_with_user_manifest_env(
.map_err(|e| format!("failed to resolve manifest cascade: {e}"))
}
fn load_profile(
path: &Path,
pod_name_override: Option<&str>,
) -> Result<(PodManifest, PromptLoader), String> {
let resolver = NixProfileResolver::new();
let mut resolved = resolver
.resolve(&ProfileSelector::path(path.to_path_buf()))
.map_err(|e| format!("failed to resolve profile {}: {e}", path.display()))?;
if let Some(pod_name) = pod_name_override {
resolved.manifest.pod.name = pod_name.to_string();
}
Ok((resolved.manifest, PromptLoader::builtins_only()))
}
fn load_single_manifest(
path: &Path,
pod_name_override: Option<&str>,
@@ -496,6 +535,45 @@ permission = "write"
assert_eq!(manifest.pod.name, "from-flag");
}
#[test]
fn profile_conflicts_with_manifest_and_restore_modes() {
let segment_id = session_store::new_segment_id().to_string();
for args in [
vec!["insomnia-pod", "--profile", "p.nix", "--manifest", "m.toml"],
vec!["insomnia-pod", "--profile", "p.nix", "--pod", "agent"],
vec![
"insomnia-pod",
"--profile",
"p.nix",
"--session",
&segment_id,
],
] {
let err = Cli::try_parse_from(args).unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
}
#[test]
fn profile_pod_name_requires_profile() {
let err = Cli::try_parse_from(["insomnia-pod", "--profile-pod-name", "agent"]).unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
}
#[test]
fn profile_pod_name_is_not_restore_pod_flag() {
let cli = Cli::try_parse_from([
"insomnia-pod",
"--profile",
"p.nix",
"--profile-pod-name",
"agent",
])
.unwrap();
assert_eq!(cli.profile_pod_name.as_deref(), Some("agent"));
assert!(cli.pod.is_none());
}
#[test]
fn manifest_mode_loads_single_file_with_minimal_prompt_loader() {
let tmp = TempDir::new().unwrap();
+31 -11
View File
@@ -917,27 +917,31 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
})
}
fn pod_metadata(&self, active: Option<PodActiveSegmentRef>) -> PodMetadata {
let mut metadata = PodMetadata::new(self.manifest.pod.name.clone(), active);
if self.manifest.profile.is_some() {
metadata.resolved_manifest_snapshot = serde_json::to_value(&self.manifest).ok();
}
metadata
}
fn write_pod_metadata_pending(&self) -> Result<(), StoreError> {
let Some(writer) = &self.pod_metadata_writer else {
return Ok(());
};
writer(PodMetadata::new(
self.manifest.pod.name.clone(),
Some(PodActiveSegmentRef::pending_segment(self.session_id())),
))
writer(self.pod_metadata(Some(PodActiveSegmentRef::pending_segment(
self.session_id(),
))))
}
fn write_pod_metadata_active(&self, loc: SegmentLocation) -> Result<(), StoreError> {
let Some(writer) = &self.pod_metadata_writer else {
return Ok(());
};
writer(PodMetadata::new(
self.manifest.pod.name.clone(),
Some(PodActiveSegmentRef::active_segment(
loc.session_id,
loc.segment_id,
)),
))
writer(self.pod_metadata(Some(PodActiveSegmentRef::active_segment(
loc.session_id,
loc.segment_id,
))))
}
/// Enable name-keyed Pod metadata write-through for Pods built through
@@ -3945,6 +3949,15 @@ where
pod_name: pod_name.to_string(),
session_id: active.session_id,
})?;
let manifest = match metadata.resolved_manifest_snapshot {
Some(snapshot) => serde_json::from_value(snapshot).map_err(|source| {
PodError::PodMetadataManifestSnapshot {
pod_name: pod_name.to_string(),
source,
}
})?,
None => manifest,
};
Self::restore_from_manifest(active.session_id, segment_id, manifest, store, loader).await
}
@@ -4618,6 +4631,13 @@ pub enum PodError {
pod_name: String,
session_id: SessionId,
},
#[error("pod metadata for {pod_name} contains an invalid resolved manifest snapshot: {source}")]
PodMetadataManifestSnapshot {
pod_name: String,
#[source]
source: serde_json::Error,
},
}
/// Bundle of resources that every high-level Pod constructor needs: