merge: spawn through insomnia pod
This commit is contained in:
commit
cfbafbd67f
6
Cargo.lock
generated
6
Cargo.lock
generated
|
|
@ -333,6 +333,7 @@ name = "client"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"manifest",
|
||||
"pod-command",
|
||||
"protocol",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
|
|
@ -2338,6 +2339,7 @@ dependencies = [
|
|||
"manifest",
|
||||
"memory",
|
||||
"minijinja",
|
||||
"pod-command",
|
||||
"pod-registry",
|
||||
"pod-store",
|
||||
"protocol",
|
||||
|
|
@ -2357,6 +2359,10 @@ dependencies = [
|
|||
"workflow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pod-command"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "pod-registry"
|
||||
version = "0.1.0"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ members = [
|
|||
"crates/session-store",
|
||||
"crates/manifest",
|
||||
"crates/pod",
|
||||
"crates/pod-command",
|
||||
"crates/pod-store",
|
||||
"crates/protocol",
|
||||
"crates/provider",
|
||||
|
|
@ -33,6 +34,7 @@ manifest = { path = "crates/manifest" }
|
|||
lint-common = { path = "crates/lint-common" }
|
||||
memory = { path = "crates/memory" }
|
||||
pod = { path = "crates/pod" }
|
||||
pod-command = { path = "crates/pod-command" }
|
||||
pod-registry = { path = "crates/pod-registry" }
|
||||
pod-store = { path = "crates/pod-store" }
|
||||
protocol = { path = "crates/protocol" }
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ license.workspace = true
|
|||
[dependencies]
|
||||
protocol = { workspace = true }
|
||||
manifest = { workspace = true }
|
||||
pod-command = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt", "macros", "net", "io-util", "sync", "time", "process", "fs"] }
|
||||
uuid = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! `insomnia-pod` バイナリをサブプロセスとして立ち上げ、`INSOMNIA-READY` を待つ
|
||||
//! Pod runtime command をサブプロセスとして立ち上げ、`INSOMNIA-READY` を待つ
|
||||
//! ハンドシェイク。
|
||||
//!
|
||||
//! - 親プロセス (TUI / GUI / E2E) は profile/default/typed restore flags を
|
||||
|
|
@ -15,6 +15,7 @@ use std::path::{Path, PathBuf};
|
|||
use std::process::Stdio;
|
||||
use std::time::Duration;
|
||||
|
||||
use pod_command::PodRuntimeCommand;
|
||||
use tokio::process::Command;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
|
@ -99,7 +100,7 @@ pub async fn spawn_pod<F>(config: SpawnConfig, mut progress: F) -> Result<SpawnR
|
|||
where
|
||||
F: FnMut(&str),
|
||||
{
|
||||
let pod_bin = resolve_pod_command();
|
||||
let pod_command = PodRuntimeCommand::resolve().map_err(SpawnError::Io)?;
|
||||
|
||||
let pod_runtime_dir = manifest::paths::pod_runtime_dir(&config.pod_name)
|
||||
.ok_or(SpawnError::RuntimeDirUnavailable)?;
|
||||
|
|
@ -107,8 +108,9 @@ where
|
|||
let stderr_path = pod_runtime_dir.join("stderr.log");
|
||||
let stderr_file = std::fs::File::create(&stderr_path).map_err(SpawnError::Io)?;
|
||||
|
||||
let mut command = Command::new(&pod_bin);
|
||||
let mut command = Command::new(pod_command.program());
|
||||
command
|
||||
.args(pod_command.prefix_args())
|
||||
.current_dir(&config.cwd)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
|
|
@ -268,23 +270,6 @@ async fn drain_stderr_into_tail(stderr_path: &Path, tail: &mut StderrTail, offse
|
|||
*offset = content.len();
|
||||
}
|
||||
|
||||
/// Resolves the binary used to launch a child Pod. Must point at a
|
||||
/// `insomnia-pod`-compatible executable — the parent reads the child's stderr
|
||||
/// directly looking for `INSOMNIA-READY`, so any wrapper that emits
|
||||
/// extra lines on stderr will pollute that handshake.
|
||||
///
|
||||
/// `INSOMNIA_POD_COMMAND` overrides the lookup (used by tests to inject
|
||||
/// a mock binary). Otherwise we defer to `PATH` — missing binary
|
||||
/// surfaces as the spawn `io::Error`.
|
||||
fn resolve_pod_command() -> PathBuf {
|
||||
if let Ok(cmd) = std::env::var("INSOMNIA_POD_COMMAND")
|
||||
&& !cmd.is_empty()
|
||||
{
|
||||
return PathBuf::from(cmd);
|
||||
}
|
||||
PathBuf::from("insomnia-pod")
|
||||
}
|
||||
|
||||
struct StderrTail {
|
||||
lines: std::collections::VecDeque<String>,
|
||||
}
|
||||
|
|
|
|||
7
crates/pod-command/Cargo.toml
Normal file
7
crates/pod-command/Cargo.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "pod-command"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
170
crates/pod-command/src/lib.rs
Normal file
170
crates/pod-command/src/lib.rs
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
use std::ffi::{OsStr, OsString};
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub const POD_COMMAND_OVERRIDE_ENV: &str = "INSOMNIA_POD_COMMAND";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PodRuntimeCommand {
|
||||
pub program: PathBuf,
|
||||
pub prefix_args: Vec<OsString>,
|
||||
}
|
||||
|
||||
impl PodRuntimeCommand {
|
||||
pub fn new(program: impl Into<PathBuf>, prefix_args: Vec<OsString>) -> Self {
|
||||
Self {
|
||||
program: program.into(),
|
||||
prefix_args,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn executable_only(program: impl Into<PathBuf>) -> Self {
|
||||
Self::new(program, Vec::new())
|
||||
}
|
||||
|
||||
pub fn for_current_exe() -> io::Result<Self> {
|
||||
Ok(Self::for_executable(std::env::current_exe()?))
|
||||
}
|
||||
|
||||
pub fn for_executable(program: impl Into<PathBuf>) -> Self {
|
||||
let program = program.into();
|
||||
let prefix_args = if is_legacy_pod_binary(&program) {
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![OsString::from("pod")]
|
||||
};
|
||||
Self::new(program, prefix_args)
|
||||
}
|
||||
|
||||
/// Resolve the Pod runtime command used for subprocess launches.
|
||||
///
|
||||
/// `INSOMNIA_POD_COMMAND` is intentionally executable-only: its value is
|
||||
/// used as the program path without shell parsing and without the unified
|
||||
/// `pod` prefix arg. That keeps existing development/test overrides safe
|
||||
/// while the default path moves to `current_exe() + ["pod"]`.
|
||||
pub fn resolve() -> io::Result<Self> {
|
||||
if let Some(command) = Self::from_override_env() {
|
||||
return Ok(command);
|
||||
}
|
||||
Self::for_current_exe()
|
||||
}
|
||||
|
||||
pub fn from_override_env() -> Option<Self> {
|
||||
let raw = std::env::var_os(POD_COMMAND_OVERRIDE_ENV)?;
|
||||
if raw.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(Self::executable_only(raw))
|
||||
}
|
||||
|
||||
pub fn program(&self) -> &Path {
|
||||
&self.program
|
||||
}
|
||||
|
||||
pub fn prefix_args(&self) -> &[OsString] {
|
||||
&self.prefix_args
|
||||
}
|
||||
|
||||
pub fn argv_with<I, S>(&self, args: I) -> Vec<OsString>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: Into<OsString>,
|
||||
{
|
||||
let mut argv = self.prefix_args.clone();
|
||||
argv.extend(args.into_iter().map(Into::into));
|
||||
argv
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PodRuntimeCommand {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.program.display())?;
|
||||
for arg in &self.prefix_args {
|
||||
write!(f, " {}", arg.to_string_lossy())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn is_legacy_pod_binary(program: &Path) -> bool {
|
||||
let Some(file_name) = program.file_name().and_then(OsStr::to_str) else {
|
||||
return false;
|
||||
};
|
||||
let stem = file_name.strip_suffix(".exe").unwrap_or(file_name);
|
||||
stem == "insomnia-pod"
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Mutex;
|
||||
|
||||
static ENV_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
struct EnvRestore(Option<OsString>);
|
||||
|
||||
impl EnvRestore {
|
||||
fn capture() -> Self {
|
||||
Self(std::env::var_os(POD_COMMAND_OVERRIDE_ENV))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EnvRestore {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
match &self.0 {
|
||||
Some(value) => std::env::set_var(POD_COMMAND_OVERRIDE_ENV, value),
|
||||
None => std::env::remove_var(POD_COMMAND_OVERRIDE_ENV),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insomnia_binary_defaults_to_pod_prefix() {
|
||||
let command = PodRuntimeCommand::for_executable("/opt/insomnia/bin/insomnia");
|
||||
|
||||
assert_eq!(command.program(), Path::new("/opt/insomnia/bin/insomnia"));
|
||||
assert_eq!(command.prefix_args(), [OsString::from("pod")]);
|
||||
assert_eq!(
|
||||
command.argv_with(["--pod", "agent"]),
|
||||
vec!["pod", "--pod", "agent"]
|
||||
.into_iter()
|
||||
.map(OsString::from)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_wrapper_keeps_executable_only_command() {
|
||||
let command = PodRuntimeCommand::for_executable("/opt/insomnia/bin/insomnia-pod");
|
||||
|
||||
assert_eq!(
|
||||
command.program(),
|
||||
Path::new("/opt/insomnia/bin/insomnia-pod")
|
||||
);
|
||||
assert!(command.prefix_args().is_empty());
|
||||
assert_eq!(
|
||||
command.argv_with(["--pod", "agent"]),
|
||||
vec!["--pod", "agent"]
|
||||
.into_iter()
|
||||
.map(OsString::from)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_override_is_executable_only_and_not_shell_parsed() {
|
||||
let _guard = ENV_LOCK.lock().unwrap();
|
||||
let _restore = EnvRestore::capture();
|
||||
unsafe {
|
||||
std::env::set_var(POD_COMMAND_OVERRIDE_ENV, "/tmp/mock pod --flag");
|
||||
}
|
||||
|
||||
let command = PodRuntimeCommand::resolve().unwrap();
|
||||
|
||||
assert_eq!(command.program(), Path::new("/tmp/mock pod --flag"));
|
||||
assert!(command.prefix_args().is_empty());
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ pod-store = { workspace = true }
|
|||
manifest = { workspace = true }
|
||||
protocol = { workspace = true }
|
||||
provider = { workspace = true }
|
||||
pod-command = { workspace = true }
|
||||
pod-registry = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ use std::time::Duration;
|
|||
use async_trait::async_trait;
|
||||
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
||||
use manifest::{Permission, ScopeRule};
|
||||
use pod_command::PodRuntimeCommand;
|
||||
use pod_store::{PodActiveSegmentRef, PodMetadata, PodMetadataStore};
|
||||
use protocol::stream::JsonLineReader;
|
||||
use protocol::{Event, PodStatus};
|
||||
|
|
@ -328,8 +329,10 @@ where
|
|||
pod_name: &str,
|
||||
socket_path: &Path,
|
||||
) -> Result<(), PodDiscoveryError> {
|
||||
let mut command = Command::new(resolve_pod_command());
|
||||
let pod_command = PodRuntimeCommand::resolve().map_err(PodDiscoveryError::RestoreSpawn)?;
|
||||
let mut command = Command::new(pod_command.program());
|
||||
command
|
||||
.args(pod_command.prefix_args())
|
||||
.arg("--pod")
|
||||
.arg(pod_name)
|
||||
.arg("--require-pod-state")
|
||||
|
|
@ -661,15 +664,6 @@ fn lookup_segment_lock(
|
|||
pod_registry::lookup_segment(segment_id)
|
||||
}
|
||||
|
||||
fn resolve_pod_command() -> PathBuf {
|
||||
if let Ok(cmd) = std::env::var("INSOMNIA_POD_COMMAND")
|
||||
&& !cmd.is_empty()
|
||||
{
|
||||
return PathBuf::from(cmd);
|
||||
}
|
||||
PathBuf::from("insomnia-pod")
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
struct PodNameInput {
|
||||
/// Pod name to restore.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//!
|
||||
//! Wires pod-registry delegation, child manifest-config construction, subprocess
|
||||
//! launch, and socket handoff into a single `Tool` implementation. When
|
||||
//! the LLM calls `SpawnPod`, a fresh `insomnia-pod` binary is exec'd in its own
|
||||
//! the LLM calls `SpawnPod`, a fresh Pod runtime command is exec'd in its own
|
||||
//! process group, the pod-registry is updated atomically, and the child's
|
||||
//! first turn is kicked off by handing its socket a `Method::Run`.
|
||||
|
||||
|
|
@ -19,6 +19,7 @@ use manifest::{
|
|||
ProfileRegistrySource, ProfileResolveOptions, ProfileResolver, ProfileSelector, ScopeConfig,
|
||||
ScopeRule, SessionConfigPartial, SharedScope, ToolOutputLimitsPartial, WorkerManifestConfig,
|
||||
};
|
||||
use pod_command::PodRuntimeCommand;
|
||||
use serde::Deserialize;
|
||||
use tokio::net::UnixStream;
|
||||
use tokio::process::Command;
|
||||
|
|
@ -408,8 +409,9 @@ impl SpawnPodTool {
|
|||
spawn_config_json: &str,
|
||||
predicted_socket: &Path,
|
||||
) -> Result<(), ToolError> {
|
||||
let pod_command =
|
||||
std::env::var("INSOMNIA_POD_COMMAND").unwrap_or_else(|_| "insomnia-pod".into());
|
||||
let pod_command = PodRuntimeCommand::resolve().map_err(|error| {
|
||||
ToolError::ExecutionFailed(format!("failed to resolve Pod runtime command: {error}"))
|
||||
})?;
|
||||
|
||||
// Pre-create the child's runtime dir so we have a stable place to
|
||||
// capture its stderr before it has had a chance to bind anything.
|
||||
|
|
@ -430,8 +432,9 @@ impl SpawnPodTool {
|
|||
ToolError::ExecutionFailed(format!("open {}: {e}", stderr_path.display()))
|
||||
})?;
|
||||
|
||||
let mut cmd = Command::new(&pod_command);
|
||||
cmd.arg("--adopt")
|
||||
let mut cmd = Command::new(pod_command.program());
|
||||
cmd.args(pod_command.prefix_args())
|
||||
.arg("--adopt")
|
||||
.arg("--callback")
|
||||
.arg(&self.callback_socket)
|
||||
.arg("--spawn-config-json")
|
||||
|
|
@ -488,7 +491,7 @@ fn parse_scope(rules: &[ScopeRuleInput]) -> Result<Vec<ScopeRule>, ToolError> {
|
|||
}
|
||||
|
||||
/// Serialise the internal manifest config that gets handed to the child
|
||||
/// `insomnia-pod` binary via the hidden `--spawn-config-json` flag.
|
||||
/// Pod runtime process via the hidden `--spawn-config-json` flag.
|
||||
/// `PodManifestConfig`'s `Serialize` impl is the single source of truth for the
|
||||
/// internal handoff shape.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ enum Mode {
|
|||
profile: Option<String>,
|
||||
},
|
||||
/// `insomnia <name>` / `insomnia --pod <name>`: attach to a live Pod by name if
|
||||
/// possible; otherwise launch `insomnia-pod --pod <name>` so the pod process
|
||||
/// possible; otherwise launch the Pod runtime command with `--pod <name>` so it
|
||||
/// resumes from name-keyed state or creates a fresh same-name Pod.
|
||||
PodName {
|
||||
pod_name: String,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//!
|
||||
//! Reads live Pod allocations from the runtime registry and stopped Pod state
|
||||
//! from the pod-store name-keyed metadata. Picking a live row attaches to
|
||||
//! its socket; picking a stopped row restores via `insomnia-pod --pod <name>`.
|
||||
//! its socket; picking a stopped row restores via the Pod runtime command.
|
||||
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -65,7 +65,7 @@ impl From<session_store::StoreError> for PickerError {
|
|||
pub enum PickerOutcome {
|
||||
/// User picked a Pod. `socket_override` is set for live rows when the
|
||||
/// runtime registry knows the exact socket path; stopped rows leave it
|
||||
/// empty so the caller restores with `insomnia-pod --pod <name>`.
|
||||
/// empty so the caller restores by spawning the Pod runtime command.
|
||||
Picked {
|
||||
pod_name: String,
|
||||
socket_override: Option<PathBuf>,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
//! Rendered at the user's current cursor position when `insomnia` is invoked
|
||||
//! with no positional argument. Discovers `.insomnia/profiles.toml` profile
|
||||
//! choices plus bundled profiles, defaults to the builtin profile, prompts for
|
||||
//! the Pod's name, and on confirmation launches the `insomnia-pod` binary as an
|
||||
//! the Pod's name, and on confirmation launches the Pod runtime command as an
|
||||
//! independent process. Once the process reports its socket via the
|
||||
//! `INSOMNIA-READY` stderr line, the dialog hands control back so main can
|
||||
//! switch the terminal to alternate-screen mode.
|
||||
|
|
@ -72,7 +72,7 @@ type InlineTerminal = Terminal<CrosstermBackend<io::Stdout>>;
|
|||
|
||||
/// Source session for a resume run. `None` = fresh spawn (current
|
||||
/// behaviour); `Some(id)` swaps the dialog into "Resume Pod" mode and
|
||||
/// passes `--session <id>` to the spawned `insomnia-pod` child.
|
||||
/// passes `--session <id>` to the spawned Pod runtime child.
|
||||
pub async fn run(
|
||||
resume_from: Option<SegmentId>,
|
||||
profile: Option<String>,
|
||||
|
|
@ -162,7 +162,7 @@ pub async fn run(
|
|||
}
|
||||
}
|
||||
|
||||
/// Launch `insomnia-pod --pod <name>` without opening the name dialog. The child Pod
|
||||
/// Launch a Pod runtime command with `--pod <name>` without opening the name dialog. The child Pod
|
||||
/// resolves persisted Pod metadata if present, or creates a fresh same-name Pod
|
||||
/// from the default profile.
|
||||
pub async fn run_pod_name(pod_name: String) -> Result<SpawnOutcome, SpawnError> {
|
||||
|
|
@ -415,7 +415,7 @@ struct Form {
|
|||
/// When true, launch the child with `--pod <name>` so the pod process
|
||||
/// resolves name-keyed state before falling back to fresh creation.
|
||||
resume_by_pod_name: bool,
|
||||
/// Optional profile choices passed to `insomnia-pod --profile` for
|
||||
/// Optional profile choices passed with `--profile` for
|
||||
/// fresh spawns. This is not used for resume/attach flows because those must
|
||||
/// restore Pod state rather than re-evaluate a profile source.
|
||||
profile_choices: Vec<ProfileChoice>,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user