feat: add nix manifest profile foundation
This commit is contained in:
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user