diff --git a/crates/insomnia/src/lib.rs b/crates/insomnia/src/lib.rs index c4ab37aa..977691cc 100644 --- a/crates/insomnia/src/lib.rs +++ b/crates/insomnia/src/lib.rs @@ -3,8 +3,6 @@ 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, @@ -19,10 +17,6 @@ impl PodRuntimeCommand { } } - pub fn executable_only(program: impl Into) -> Self { - Self::new(program, Vec::new()) - } - pub fn for_current_exe() -> io::Result { Ok(Self::for_executable(std::env::current_exe()?)) } @@ -33,25 +27,12 @@ impl PodRuntimeCommand { /// 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 development/test overrides safe while the - /// default path is always `current_exe() + ["pod"]`. + /// The default launch path is always the current `insomnia` executable plus + /// the unified `pod` prefix argument. pub fn resolve() -> io::Result { - if let Some(command) = Self::from_override_env() { - return Ok(command); - } Self::for_current_exe() } - pub fn from_override_env() -> Option { - 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 } @@ -84,28 +65,6 @@ impl fmt::Display for PodRuntimeCommand { #[cfg(test)] mod tests { use super::*; - use std::sync::Mutex; - - static ENV_LOCK: Mutex<()> = Mutex::new(()); - - struct EnvRestore(Option); - - 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() { @@ -139,18 +98,4 @@ mod tests { .collect::>() ); } - - #[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()); - } } diff --git a/crates/pod/src/spawn/tool.rs b/crates/pod/src/spawn/tool.rs index 32809ab2..2f68d9e0 100644 --- a/crates/pod/src/spawn/tool.rs +++ b/crates/pod/src/spawn/tool.rs @@ -220,6 +220,9 @@ pub struct SpawnPodTool { /// Directory the spawned Pod should run in when the LLM did not /// override it. Defaults to the spawner's pwd — see module docs. spawner_pwd: PathBuf, + /// Optional typed runtime command injected by tests. Production resolves + /// the runtime command from `std::env::current_exe()` at launch time. + runtime_command: Option, /// Shared registry of spawned children, also used by the /// pod-comm tools (`SendToPod` / `ReadPodOutput` / `StopPod`) and by /// Pod discovery. Writes the list to runtime and durable Pod state on @@ -258,12 +261,14 @@ impl SpawnPodTool { spawner_manifest: PodManifest, available_profiles: AvailableProfiles, spawner_scope: SharedScope, + runtime_command: Option, ) -> Self { Self { spawner_name, callback_socket, runtime_base, spawner_pwd, + runtime_command, registry, parent_socket, spawner_manifest, @@ -409,9 +414,14 @@ impl SpawnPodTool { spawn_config_json: &str, predicted_socket: &Path, ) -> Result<(), ToolError> { - let runtime_command = PodRuntimeCommand::resolve().map_err(|error| { - ToolError::ExecutionFailed(format!("failed to resolve Pod runtime command: {error}")) - })?; + let runtime_command = match &self.runtime_command { + Some(command) => command.clone(), + None => 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. @@ -764,6 +774,59 @@ pub fn spawn_pod_tool( spawner_manifest: PodManifest, spawner_scope: SharedScope, prompts: Arc, +) -> ToolDefinition { + spawn_pod_tool_impl( + spawner_name, + callback_socket, + runtime_base, + spawner_pwd, + registry, + parent_socket, + spawner_manifest, + spawner_scope, + prompts, + None, + ) +} + +#[doc(hidden)] +pub fn spawn_pod_tool_with_runtime_command( + spawner_name: String, + callback_socket: PathBuf, + runtime_base: PathBuf, + spawner_pwd: PathBuf, + registry: Arc, + parent_socket: Option, + spawner_manifest: PodManifest, + spawner_scope: SharedScope, + prompts: Arc, + runtime_command: PodRuntimeCommand, +) -> ToolDefinition { + spawn_pod_tool_impl( + spawner_name, + callback_socket, + runtime_base, + spawner_pwd, + registry, + parent_socket, + spawner_manifest, + spawner_scope, + prompts, + Some(runtime_command), + ) +} + +fn spawn_pod_tool_impl( + spawner_name: String, + callback_socket: PathBuf, + runtime_base: PathBuf, + spawner_pwd: PathBuf, + registry: Arc, + parent_socket: Option, + spawner_manifest: PodManifest, + spawner_scope: SharedScope, + prompts: Arc, + runtime_command: Option, ) -> ToolDefinition { Arc::new(move || { let schema = schemars::schema_for!(SpawnPodInput); @@ -794,6 +857,7 @@ pub fn spawn_pod_tool( spawner_manifest.clone(), available_profiles, spawner_scope.clone(), + runtime_command.clone(), )); (meta, tool) }) diff --git a/crates/pod/tests/spawn_pod_test.rs b/crates/pod/tests/spawn_pod_test.rs index a280af76..77334ed8 100644 --- a/crates/pod/tests/spawn_pod_test.rs +++ b/crates/pod/tests/spawn_pod_test.rs @@ -1,15 +1,15 @@ //! Integration tests for the `SpawnPod` tool. //! //! These tests exercise the tool's pod-registry delegation, subprocess -//! launch, socket handoff, and `spawned_pods.json` write without relying -//! on the real Pod runtime executable. `INSOMNIA_POD_COMMAND` is pointed at -//! `/bin/true` (which exits immediately) while a test-owned Unix -//! listener pre-binds the predicted socket path, so the tool sees the -//! "child" as live. +//! launch, socket handoff, and `spawned_pods.json` write through an injected +//! typed runtime command. The mock command exits immediately while a +//! test-owned Unix listener pre-binds the predicted socket path, so the tool +//! sees the "child" as live. use std::path::{Path, PathBuf}; use std::sync::{LazyLock, Mutex}; +use insomnia::PodRuntimeCommand; use llm_worker::tool::{ToolError, ToolOutput}; use manifest::{ AuthRef, ModelManifest, Permission, PodManifest, PodManifestConfig, PodMetaConfig, SchemeKind, @@ -18,7 +18,7 @@ use manifest::{ use pod::runtime::dir::{RuntimeDir, SpawnedPodRecord}; use pod::runtime::pod_registry::{self, LockFileGuard}; use pod::spawn::registry::SpawnedPodRegistry; -use pod::spawn::tool::spawn_pod_tool; +use pod::spawn::tool::spawn_pod_tool_with_runtime_command; use protocol::stream::{JsonLineReader, JsonLineWriter}; use protocol::{Event, Method}; use serde_json::json; @@ -26,8 +26,8 @@ use std::sync::Arc; use tempfile::TempDir; use tokio::net::UnixListener; -/// Serialises tests that mutate `INSOMNIA_RUNTIME_DIR` / -/// `INSOMNIA_POD_COMMAND` across the thread-pooled test harness. +/// Serialises tests that mutate `INSOMNIA_RUNTIME_DIR` across the +/// thread-pooled test harness. static ENV_LOCK: LazyLock> = LazyLock::new(|| Mutex::new(())); struct EnvGuard { @@ -141,11 +141,8 @@ fn accept_one_method(listener: UnixListener) -> tokio::task::JoinHandle PodRuntimeCommand { + PodRuntimeCommand::new(which_true(), Vec::new()) } /// `/bin/true` only exists on FHS-compliant systems. Resolve it via PATH @@ -213,7 +210,6 @@ fn shared_scope_for(allow_root: &Path) -> SharedScope { fn clear_env() { unsafe { std::env::remove_var("INSOMNIA_RUNTIME_DIR"); - std::env::remove_var("INSOMNIA_POD_COMMAND"); } } @@ -224,14 +220,13 @@ async fn spawn_pod_delegates_scope_and_sends_run() { let allow_root = TempDir::new().unwrap(); let (_tmp, runtime_base, spawner_socket, spawner_rd) = setup_spawner("root", allow_root.path()).await; - point_runtime_command_at_true(); let (_predicted_socket, listener) = bind_mock_pod_socket(&runtime_base, "child").await; let received = accept_one_method(listener); let registry = SpawnedPodRegistry::new(spawner_rd.clone()); let spawner_scope = shared_scope_for(allow_root.path()); - let def = spawn_pod_tool( + let def = spawn_pod_tool_with_runtime_command( "root".into(), spawner_socket.clone(), runtime_base.clone(), @@ -241,6 +236,7 @@ async fn spawn_pod_delegates_scope_and_sends_run() { dummy_manifest(allow_root.path()), spawner_scope.clone(), builtin_prompts(), + mock_runtime_command(), ); let (_meta, tool) = def(); @@ -317,11 +313,10 @@ async fn spawn_pod_rejects_scope_outside_spawner() { let outside = TempDir::new().unwrap(); let (_tmp, runtime_base, spawner_socket, spawner_rd) = setup_spawner("root", allow_root.path()).await; - point_runtime_command_at_true(); let registry = SpawnedPodRegistry::new(spawner_rd); let spawner_scope = shared_scope_for(allow_root.path()); - let def = spawn_pod_tool( + let def = spawn_pod_tool_with_runtime_command( "root".into(), spawner_socket, runtime_base, @@ -331,6 +326,7 @@ async fn spawn_pod_rejects_scope_outside_spawner() { dummy_manifest(allow_root.path()), spawner_scope.clone(), builtin_prompts(), + mock_runtime_command(), ); let (_meta, tool) = def(); @@ -379,7 +375,6 @@ async fn spawn_pod_rolls_back_reservation_when_socket_never_appears() { let allow_root = TempDir::new().unwrap(); let (_tmp, runtime_base, spawner_socket, spawner_rd) = setup_spawner("root", allow_root.path()).await; - point_runtime_command_at_true(); // Deliberately do NOT bind a socket at the predicted path. The // tool's wait_for_socket should time out, triggering rollback. @@ -394,7 +389,7 @@ async fn spawn_pod_rolls_back_reservation_when_socket_never_appears() { let registry = SpawnedPodRegistry::new(spawner_rd); let spawner_scope = shared_scope_for(allow_root.path()); - let def = spawn_pod_tool( + let def = spawn_pod_tool_with_runtime_command( "root".into(), spawner_socket, runtime_base, @@ -404,6 +399,7 @@ async fn spawn_pod_rolls_back_reservation_when_socket_never_appears() { dummy_manifest(allow_root.path()), spawner_scope.clone(), builtin_prompts(), + mock_runtime_command(), ); let (_meta, tool) = def(); diff --git a/docs/environment.md b/docs/environment.md index 2540026e..ca0afeda 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -77,7 +77,6 @@ Credential env var は interoperability のために現時点では残ってい - `INSOMNIA_USER_MANIFEST` は通常の profile-based Pod/TUI startup の一部ではない。one-file manifest の debug / compatibility path には `insomnia pod --manifest ` を使う。 - ambient `.insomnia/manifest.toml` discovery は通常の fresh startup の一部ではない。 -- `INSOMNIA_POD_COMMAND` は single-binary 化に伴って削除する。Pod runtime は `insomnia pod ...` の typed command として起動する。 - `INSOMNIA_TEST_*` のような test-only 環境変数は supported surface にしない。既存利用も削除する。 - `insomnia-pod` は installed command ではない。Pod runtime は `insomnia pod ...` から起動する。 - 通常 runtime は `.env` ファイルを load しない。 @@ -89,7 +88,7 @@ Credential env var は interoperability のために現時点では残ってい 1. test-only env var を削除し、public env behavior を検証する test だけを shared guard / test-support crate に集約する。 2. path resolution は `manifest::paths` に集約し、path precedence rule を別の場所で重複実装しない。 3. credential source は resolved config 上で明示し、process-env convention を増やすより typed secret reference へ寄せる。encrypted secret store 導入時に credential env var 依存を削除する。 -4. `INSOMNIA_POD_COMMAND` は削除し、Pod runtime 起動は `current_exe() + ["pod"]` の typed command に一本化する。 +4. Pod runtime 起動は環境変数ではなく `current_exe() + ["pod"]` の typed command に一本化する。 5. fallback env は独立した設定項目として増やさず、対応する main key の解決順として文書化する。 6. 空の env value は、変数 category に応じて unset / invalid のどちらとして扱うかを一貫させ、新しい supported variable を追加する場合は挙動を文書化する。 7. 外部 process integration が env inheritance / filtering を必要とする場合は、ambient な inherited process state に頼らず、明示的な policy boundary として設計する。