cli: remove pod command env override

This commit is contained in:
Keisuke Hirata 2026-05-31 19:03:49 +09:00
parent 44ff1411a3
commit c618fa694c
No known key found for this signature in database
4 changed files with 86 additions and 82 deletions

View File

@ -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<PathBuf>) -> Self {
Self::new(program, Vec::new())
}
pub fn for_current_exe() -> io::Result<Self> {
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<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
}
@ -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<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() {
@ -139,18 +98,4 @@ mod tests {
.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());
}
}

View File

@ -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<PodRuntimeCommand>,
/// 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<PodRuntimeCommand>,
) -> 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<PromptCatalog>,
) -> 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<SpawnedPodRegistry>,
parent_socket: Option<PathBuf>,
spawner_manifest: PodManifest,
spawner_scope: SharedScope,
prompts: Arc<PromptCatalog>,
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<SpawnedPodRegistry>,
parent_socket: Option<PathBuf>,
spawner_manifest: PodManifest,
spawner_scope: SharedScope,
prompts: Arc<PromptCatalog>,
runtime_command: Option<PodRuntimeCommand>,
) -> 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)
})

View File

@ -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<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
struct EnvGuard {
@ -141,11 +141,8 @@ fn accept_one_method(listener: UnixListener) -> tokio::task::JoinHandle<Option<M
})
}
fn point_runtime_command_at_true() {
let path = which_true();
unsafe {
std::env::set_var("INSOMNIA_POD_COMMAND", &path);
}
fn mock_runtime_command() -> 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();

View File

@ -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 <PATH>` を使う。
- 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 として設計する。