Compare commits

..

52 Commits

Author SHA1 Message Date
6e5ed683d6
decision: remove credential env compatibility 2026-06-01 06:23:48 +09:00
faac237f0d
plan: clarify phased secrets implementation 2026-06-01 06:19:31 +09:00
50ccf0f21b
decision: simplify secret store model 2026-06-01 06:05:09 +09:00
9a00c91632
plan: detail encrypted secrets requirements 2026-06-01 05:58:02 +09:00
52d8807282
close: dev pod runtime env 2026-06-01 05:41:58 +09:00
32d20eea01
merge: dev pod runtime env 2026-06-01 05:41:36 +09:00
17e799a766
review: approve dev pod runtime env 2026-06-01 05:41:28 +09:00
0031953ed3
dev: add pod runtime command override 2026-06-01 05:37:17 +09:00
ca9e1840cf
close: tui runtime extraction 2026-05-31 22:57:04 +09:00
a24d026b0e
merge: tui runtime extraction 2026-05-31 22:56:39 +09:00
8ec398522e
review: approve tui runtime extraction 2026-05-31 22:56:32 +09:00
4d897186c2
tui: extract single pod runtime 2026-05-31 22:51:21 +09:00
c4cc93512f
plan: rescope tui runtime extraction 2026-05-31 22:46:32 +09:00
0a538380a9
close: tui view mode state 2026-05-31 22:45:41 +09:00
9afe8a3243
merge: tui view mode state 2026-05-31 22:45:10 +09:00
72d6ac8719
review: approve tui view mode move 2026-05-31 22:45:04 +09:00
bc31bfac58
tui: move view mode state 2026-05-31 22:41:38 +09:00
80a41d3264
close: tui cli parsing extraction 2026-05-31 22:38:32 +09:00
7141734f07
close: insomnia cli ownership 2026-05-31 22:20:05 +09:00
6b49be085d
merge: insomnia cli ownership 2026-05-31 22:17:32 +09:00
b12e3320a9
review: approve insomnia cli ownership 2026-05-31 22:17:15 +09:00
37281b64f2
cli: reject resume with pod selection 2026-05-31 22:15:12 +09:00
22d974a722
cli: move product entrypoint to insomnia 2026-05-31 22:07:52 +09:00
2deb93c7ce
plan: preflight insomnia cli ownership 2026-05-31 21:42:24 +09:00
4c7088d757
ticket: add dev pod runtime command env 2026-05-31 21:41:24 +09:00
f0efafbba9
close: single binary cli migration 2026-05-31 21:15:52 +09:00
5783503f74
docs: remove obsolete nix package doc 2026-05-31 21:11:48 +09:00
530c14e4b8
close: remove resource_dir 2026-05-31 20:58:30 +09:00
f7293411cd
merge: remove resource_dir 2026-05-31 20:54:37 +09:00
18ae9934e1
review: approve resource_dir removal 2026-05-31 20:54:28 +09:00
365ec8b7fa
manifest: embed builtin resources 2026-05-31 20:47:06 +09:00
607923dfbd
plan: preflight resource_dir removal 2026-05-31 20:28:43 +09:00
f5c4943337
ticket: plan insomnia cli ownership 2026-05-31 20:20:34 +09:00
3a18842ac7
ticket: plan resource_dir removal 2026-05-31 20:09:12 +09:00
469410cbcf
docs: prefer pure env fallback tests 2026-05-31 19:58:57 +09:00
813ff7e199
close: pure path fallback tests 2026-05-31 19:54:51 +09:00
7851fc0aac
merge: pure path fallback tests 2026-05-31 19:54:24 +09:00
974dde3b88
review: approve pure path fallback tests 2026-05-31 19:54:18 +09:00
e232f5468a
test: make path fallback tests pure 2026-05-31 19:51:23 +09:00
6fa67097aa
ticket: plan pure path fallback tests 2026-05-31 19:47:02 +09:00
506719796e
docs: remove absent env surfaces 2026-05-31 19:31:33 +09:00
20f975c9b7
docs: simplify test env policy 2026-05-31 19:29:31 +09:00
1f29d1a39d
docs: drop explicit test env name 2026-05-31 19:28:33 +09:00
e605a5886f
close: remove pod command env override 2026-05-31 19:12:05 +09:00
400cca9252
docs: drop pod command env reference 2026-05-31 19:11:50 +09:00
458a87e81a
merge: remove pod command env override
# Conflicts:
#	docs/environment.md
2026-05-31 19:10:58 +09:00
e8d8f139d0
review: approve pod command env removal 2026-05-31 19:09:52 +09:00
e8e50bfa6b
close: eliminate test-only env vars 2026-05-31 19:04:31 +09:00
b61504e821
merge: eliminate test-only env vars 2026-05-31 19:04:04 +09:00
fc3a0718a1
review: approve test env removal 2026-05-31 19:03:55 +09:00
c618fa694c
cli: remove pod command env override 2026-05-31 19:03:49 +09:00
e64a559595
test: remove test-only env vars 2026-05-31 19:00:16 +09:00
75 changed files with 3264 additions and 1248 deletions

16
Cargo.lock generated
View File

@ -332,7 +332,6 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
name = "client"
version = "0.1.0"
dependencies = [
"insomnia",
"manifest",
"protocol",
"serde_json",
@ -1483,6 +1482,17 @@ dependencies = [
[[package]]
name = "insomnia"
version = "0.1.0"
dependencies = [
"client",
"memory",
"pod",
"serde",
"serde_json",
"session-store",
"tempfile",
"tokio",
"tui",
]
[[package]]
name = "instability"
@ -2334,11 +2344,11 @@ dependencies = [
"async-trait",
"chrono",
"clap",
"client",
"dotenv",
"fs4",
"futures",
"include_dir",
"insomnia",
"libc",
"llm-worker",
"manifest",
@ -3917,8 +3927,6 @@ dependencies = [
"crossterm 0.28.1",
"llm-worker",
"manifest",
"memory",
"pod",
"pod-registry",
"pod-store",
"protocol",

View File

@ -42,6 +42,7 @@ provider = { path = "crates/provider" }
session-metrics = { path = "crates/session-metrics" }
session-store = { path = "crates/session-store" }
tools = { path = "crates/tools" }
tui = { path = "crates/tui" }
# External
# Note: `reqwest` and `chrono` are not aggregated here because some crates

View File

@ -7,7 +7,6 @@ license.workspace = true
[dependencies]
protocol = { workspace = true }
manifest = { workspace = true }
insomnia = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros", "net", "io-util", "sync", "time", "process", "fs"] }
uuid = { workspace = true }

View File

@ -9,7 +9,10 @@
//! TUI / GUI / E2E ハーネスはこの crate に依存して protocol を喋る。
mod pod_client;
pub mod runtime_command;
pub mod spawn;
pub use runtime_command::PodRuntimeCommand;
pub use pod_client::PodClient;
pub use spawn::{SpawnConfig, SpawnError, SpawnReady, spawn_pod};

View File

@ -3,7 +3,7 @@ use std::fmt;
use std::io;
use std::path::{Path, PathBuf};
pub const POD_COMMAND_OVERRIDE_ENV: &str = "INSOMNIA_POD_COMMAND";
const POD_RUNTIME_COMMAND_ENV: &str = "INSOMNIA_POD_RUNTIME_COMMAND";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PodRuntimeCommand {
@ -19,10 +19,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,23 +29,30 @@ 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. During development, a non-empty
/// `INSOMNIA_POD_RUNTIME_COMMAND` value replaces only the executable path;
/// the `pod` prefix is still added here and the env value is not parsed as a
/// shell command.
pub fn resolve() -> io::Result<Self> {
if let Some(command) = Self::from_override_env() {
return Ok(command);
}
Self::for_current_exe()
Self::resolve_from_env_value(
std::env::var_os(POD_RUNTIME_COMMAND_ENV),
std::env::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;
fn resolve_from_env_value<F>(
override_program: Option<OsString>,
current_exe: F,
) -> io::Result<Self>
where
F: FnOnce() -> io::Result<PathBuf>,
{
if let Some(program) = override_program.filter(|program| !program.as_os_str().is_empty()) {
return Ok(Self::for_executable(program));
}
Some(Self::executable_only(raw))
Ok(Self::for_executable(current_exe()?))
}
pub fn program(&self) -> &Path {
@ -84,28 +87,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() {
@ -141,16 +122,47 @@ mod tests {
}
#[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");
}
fn resolve_uses_current_exe_when_override_is_unset() {
let command = PodRuntimeCommand::resolve_from_env_value(None, || {
Ok(PathBuf::from("/opt/insomnia/bin/insomnia"))
})
.unwrap();
let command = PodRuntimeCommand::resolve().unwrap();
assert_eq!(
command,
PodRuntimeCommand::for_executable("/opt/insomnia/bin/insomnia")
);
}
assert_eq!(command.program(), Path::new("/tmp/mock pod --flag"));
assert!(command.prefix_args().is_empty());
#[test]
fn resolve_uses_current_exe_when_override_is_empty() {
let command = PodRuntimeCommand::resolve_from_env_value(Some(OsString::new()), || {
Ok(PathBuf::from("/opt/insomnia/bin/insomnia"))
})
.unwrap();
assert_eq!(
command,
PodRuntimeCommand::for_executable("/opt/insomnia/bin/insomnia")
);
}
#[test]
fn resolve_override_replaces_only_program_and_keeps_pod_prefix() {
let command = PodRuntimeCommand::resolve_from_env_value(
Some(OsString::from("/tmp/rebuilt insomnia")),
|| panic!("override must not inspect current_exe"),
)
.unwrap();
assert_eq!(command.program(), Path::new("/tmp/rebuilt 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<_>>()
);
}
}

View File

@ -15,7 +15,7 @@ use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::time::Duration;
use insomnia::PodRuntimeCommand;
use crate::PodRuntimeCommand;
use tokio::process::Command;
use uuid::Uuid;
@ -24,6 +24,7 @@ const READY_TIMEOUT: Duration = Duration::from_secs(20);
/// `spawn_pod` の入力。
pub struct SpawnConfig {
pub runtime_command: PodRuntimeCommand,
/// `pod.name` として使う識別子。runtime ディレクトリ
/// (`manifest::paths::pod_runtime_dir`) の解決と、ready 行に乗る
/// 名前との突き合わせに使う。
@ -52,7 +53,10 @@ pub enum SpawnError {
Io(io::Error),
/// runtime ディレクトリが解決できなかった (環境変数未設定等)。
RuntimeDirUnavailable,
PodLaunchFailed(io::Error),
PodLaunchFailed {
command: PodRuntimeCommand,
source: io::Error,
},
PodExitedEarly {
stderr_tail: String,
},
@ -67,7 +71,10 @@ impl std::fmt::Display for SpawnError {
f,
"could not resolve runtime directory (set INSOMNIA_HOME, INSOMNIA_RUNTIME_DIR, XDG_RUNTIME_DIR, or HOME)"
),
Self::PodLaunchFailed(e) => write!(f, "failed to launch pod: {e}"),
Self::PodLaunchFailed { command, source } => write!(
f,
"failed to launch pod runtime command `{command}`: {source}"
),
Self::PodExitedEarly { stderr_tail } => {
if stderr_tail.is_empty() {
write!(f, "pod exited before becoming ready")
@ -84,7 +91,14 @@ impl std::fmt::Display for SpawnError {
}
}
impl std::error::Error for SpawnError {}
impl std::error::Error for SpawnError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(error) | Self::PodLaunchFailed { source: error, .. } => Some(error),
Self::RuntimeDirUnavailable | Self::PodExitedEarly { .. } | Self::Timeout => None,
}
}
}
impl From<io::Error> for SpawnError {
fn from(e: io::Error) -> Self {
@ -100,17 +114,15 @@ pub async fn spawn_pod<F>(config: SpawnConfig, mut progress: F) -> Result<SpawnR
where
F: FnMut(&str),
{
let runtime_command = PodRuntimeCommand::resolve().map_err(SpawnError::Io)?;
let pod_runtime_dir = manifest::paths::pod_runtime_dir(&config.pod_name)
.ok_or(SpawnError::RuntimeDirUnavailable)?;
std::fs::create_dir_all(&pod_runtime_dir).map_err(SpawnError::Io)?;
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(runtime_command.program());
let mut command = Command::new(config.runtime_command.program());
command
.args(runtime_command.prefix_args())
.args(config.runtime_command.prefix_args())
.current_dir(&config.cwd)
.stdin(Stdio::null())
.stdout(Stdio::null())
@ -133,7 +145,12 @@ where
.arg("--session-pod-name")
.arg(&config.pod_name);
}
let mut child = command.spawn().map_err(SpawnError::PodLaunchFailed)?;
let mut child = command
.spawn()
.map_err(|source| SpawnError::PodLaunchFailed {
command: config.runtime_command.clone(),
source,
})?;
// Default `kill_on_drop = false` plus `process_group(0)` makes this
// a detached Pod once startup succeeds: dropping the handle does not

View File

@ -5,3 +5,14 @@ edition.workspace = true
license.workspace = true
[dependencies]
client = { workspace = true }
memory = { workspace = true }
pod = { workspace = true }
session-store = { workspace = true }
tui = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
[dev-dependencies]
tempfile = { workspace = true }

594
crates/insomnia/src/main.rs Normal file
View File

@ -0,0 +1,594 @@
mod memory_lint;
use std::fmt;
use std::path::PathBuf;
use std::process::ExitCode;
use client::PodRuntimeCommand;
use memory_lint::{LintCliOptions, LintStatus};
use session_store::SegmentId;
use tui::{LaunchMode, LaunchOptions};
#[derive(Debug)]
enum Mode {
Help,
MemoryLintHelp,
MemoryLint(LintCliOptions),
PodRuntime(Vec<String>),
Tui(LaunchMode),
}
#[derive(Debug)]
struct ParseError(String);
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for ParseError {}
#[tokio::main]
async fn main() -> ExitCode {
let mode = match parse_args() {
Ok(mode) => mode,
Err(e) => {
eprintln!("insomnia: {e}");
eprintln!("try `insomnia --help` for usage.");
return ExitCode::FAILURE;
}
};
match mode {
Mode::Help => {
print_help();
ExitCode::SUCCESS
}
Mode::MemoryLintHelp => {
print_memory_lint_help();
ExitCode::SUCCESS
}
Mode::MemoryLint(options) => match memory_lint::run(&options) {
Ok(LintStatus::Clean) => ExitCode::SUCCESS,
Ok(LintStatus::Failed) => ExitCode::FAILURE,
Err(e) => {
eprintln!("insomnia memory lint: {e}");
ExitCode::FAILURE
}
},
Mode::PodRuntime(args) => pod::entrypoint::run_cli_from("insomnia pod", args).await,
Mode::Tui(mode) => {
let runtime_command = match PodRuntimeCommand::resolve() {
Ok(command) => command,
Err(e) => {
eprintln!("insomnia: failed to resolve Pod runtime command: {e}");
return ExitCode::FAILURE;
}
};
tui::launch(LaunchOptions {
mode,
runtime_command,
})
.await
}
}
}
fn parse_args() -> Result<Mode, ParseError> {
parse_args_from(std::env::args().skip(1))
}
fn parse_args_from<I, S>(args: I) -> Result<Mode, ParseError>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let args = args.into_iter().map(Into::into).collect::<Vec<_>>();
parse_args_slice(&args)
}
fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
if args.is_empty() {
return Ok(Mode::Tui(LaunchMode::Spawn { profile: None }));
}
match args[0].as_str() {
"--help" | "-h" => return Ok(Mode::Help),
"pod" => return Ok(Mode::PodRuntime(args[1..].to_vec())),
"memory" if args.get(1).map(String::as_str) == Some("lint") => {
let lint_args = &args[2..];
if lint_args.iter().any(|arg| arg == "--help" || arg == "-h") {
return Ok(Mode::MemoryLintHelp);
}
let options =
memory_lint::parse_lint_args(lint_args).map_err(|e| ParseError(e.to_string()))?;
return Ok(Mode::MemoryLint(options));
}
"memory" => {
return Ok(Mode::Tui(LaunchMode::PodName {
pod_name: "memory".to_string(),
socket_override: None,
}));
}
_ => {}
}
let mut resume = false;
let mut session = None;
let mut pod_name = None;
let mut socket_override = None;
let mut profile = None;
let mut multi = false;
let mut positional = None;
let mut i = 0;
while i < args.len() {
let arg = &args[i];
match arg.as_str() {
"--resume" | "-r" => {
resume = true;
i += 1;
}
"--multi" => {
multi = true;
i += 1;
}
"--session" => {
let value = args
.get(i + 1)
.ok_or_else(|| ParseError("--session requires a value".to_string()))?;
session = Some(parse_session_id(value)?);
i += 2;
}
"--pod" => {
let value = args
.get(i + 1)
.ok_or_else(|| ParseError("--pod requires a value".to_string()))?;
if value.starts_with('-') {
return Err(ParseError("--pod requires a value".to_string()));
}
pod_name = Some(value.clone());
i += 2;
}
"--socket" => {
let value = args
.get(i + 1)
.ok_or_else(|| ParseError("--socket requires a value".to_string()))?;
if value.starts_with('-') {
return Err(ParseError("--socket requires a value".to_string()));
}
socket_override = Some(PathBuf::from(value));
i += 2;
}
"--profile" => {
let value = args
.get(i + 1)
.ok_or_else(|| ParseError("--profile requires a value".to_string()))?;
if value.starts_with('-') {
return Err(ParseError("--profile requires a value".to_string()));
}
profile = Some(value.clone());
i += 2;
}
arg if arg.starts_with("--session=") => {
let value = arg.trim_start_matches("--session=");
if value.is_empty() {
return Err(ParseError("--session requires a value".to_string()));
}
session = Some(parse_session_id(value)?);
i += 1;
}
arg if arg.starts_with("--pod=") => {
let value = arg.trim_start_matches("--pod=");
if value.is_empty() {
return Err(ParseError("--pod requires a value".to_string()));
}
pod_name = Some(value.to_string());
i += 1;
}
arg if arg.starts_with("--socket=") => {
let value = arg.trim_start_matches("--socket=");
if value.is_empty() {
return Err(ParseError("--socket requires a value".to_string()));
}
socket_override = Some(PathBuf::from(value));
i += 1;
}
arg if arg.starts_with("--profile=") => {
let value = arg.trim_start_matches("--profile=");
if value.is_empty() {
return Err(ParseError("--profile requires a value".to_string()));
}
profile = Some(value.to_string());
i += 1;
}
arg if arg.starts_with('-') => {
return Err(ParseError(format!("unknown argument: {arg}")));
}
value => {
if positional.replace(value.to_string()).is_some() {
return Err(ParseError(
"only one positional Pod name is supported".to_string(),
));
}
i += 1;
}
}
}
if pod_name.is_some() && positional.is_some() {
return Err(ParseError(
"--pod and a positional Pod name are mutually exclusive".to_string(),
));
}
if profile.is_some()
&& (resume
|| session.is_some()
|| pod_name.is_some()
|| positional.is_some()
|| socket_override.is_some()
|| multi)
{
return Err(ParseError(
"--profile can only be used for fresh spawn".to_string(),
));
}
if multi && resume {
return Err(ParseError(
"--multi and --resume are mutually exclusive".to_string(),
));
}
if multi && session.is_some() {
return Err(ParseError(
"--multi and --session are mutually exclusive".to_string(),
));
}
if multi && pod_name.is_some() {
return Err(ParseError(
"--multi and --pod are mutually exclusive".to_string(),
));
}
if multi && positional.is_some() {
return Err(ParseError(
"--multi cannot be used with a positional Pod name".to_string(),
));
}
if multi && socket_override.is_some() {
return Err(ParseError(
"--multi and --socket are mutually exclusive".to_string(),
));
}
if pod_name.is_some() && session.is_some() {
return Err(ParseError(
"--pod and --session are mutually exclusive".to_string(),
));
}
if pod_name.is_some() && resume {
return Err(ParseError(
"--pod and --resume are mutually exclusive".to_string(),
));
}
if positional.is_some() && resume {
return Err(ParseError(
"--resume cannot be used with a positional Pod name".to_string(),
));
}
if socket_override.is_some() && pod_name.is_none() && positional.is_none() {
return Err(ParseError(
"--socket requires --pod or a positional Pod name".to_string(),
));
}
if resume && session.is_some() {
return Err(ParseError(
"--resume and --session are mutually exclusive".to_string(),
));
}
if multi {
return Ok(Mode::Tui(LaunchMode::Multi));
}
let pod_name = pod_name.or(positional);
if let Some(pod_name) = pod_name {
return Ok(Mode::Tui(LaunchMode::PodName {
pod_name,
socket_override,
}));
}
if resume {
return Ok(Mode::Tui(LaunchMode::Resume));
}
if let Some(id) = session {
return Ok(Mode::Tui(LaunchMode::ResumeWithSession(id)));
}
Ok(Mode::Tui(LaunchMode::Spawn { profile }))
}
fn parse_session_id(value: &str) -> Result<SegmentId, ParseError> {
value
.parse()
.map_err(|_| ParseError(format!("invalid --session UUID: {value}")))
}
fn print_help() {
println!(
"insomnia\n\nUsage:\n insomnia [OPTIONS] [POD_NAME]\n insomnia pod [POD_OPTIONS]\n insomnia memory lint [OPTIONS]\n\nOptions:\n -r, --resume Open the Pod picker and resume/attach a Pod\n --multi Open the multi-Pod dashboard\n --pod <NAME> Attach/restore/create a Pod by name\n --socket <PATH> Attach to a specific Pod socket with --pod\n --session <UUID> Resume a specific session segment\n --profile <REF> Start a fresh Pod from a profile\n -h, --help Print help\n"
);
}
fn print_memory_lint_help() {
println!(
"insomnia memory lint\n\nUsage:\n insomnia memory lint [OPTIONS]\n\nOptions:\n --workspace <PATH> Workspace root to lint (defaults to cwd)\n --json Emit a JSON report\n --warnings-as-errors Return failure when warnings are present\n -h, --help Print help\n"
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_pod_name_mode() {
match parse_args_from(["--pod", "agent", "--socket", "/tmp/agent.sock"]).unwrap() {
Mode::Tui(LaunchMode::PodName {
pod_name,
socket_override,
}) => {
assert_eq!(pod_name, "agent");
assert_eq!(socket_override, Some(PathBuf::from("/tmp/agent.sock")));
}
_ => panic!("expected PodName mode"),
}
}
#[test]
fn parse_positional_name_uses_pod_name_mode() {
match parse_args_from(["agent"]).unwrap() {
Mode::Tui(LaunchMode::PodName {
pod_name,
socket_override,
}) => {
assert_eq!(pod_name, "agent");
assert_eq!(socket_override, None);
}
_ => panic!("expected PodName mode"),
}
}
#[test]
fn parse_memory_alone_remains_positional_pod_name() {
match parse_args_from(["memory"]).unwrap() {
Mode::Tui(LaunchMode::PodName {
pod_name,
socket_override,
}) => {
assert_eq!(pod_name, "memory");
assert_eq!(socket_override, None);
}
_ => panic!("expected PodName mode"),
}
}
#[test]
fn parse_pod_subcommand_uses_runtime_mode() {
match parse_args_from(["pod", "--pod", "agent", "--profile", "default"]).unwrap() {
Mode::PodRuntime(args) => assert_eq!(args, ["--pod", "agent", "--profile", "default"]),
_ => panic!("expected PodRuntime mode"),
}
}
#[test]
fn parse_literal_pod_name_still_available_with_flag() {
match parse_args_from(["--pod", "pod"]).unwrap() {
Mode::Tui(LaunchMode::PodName {
pod_name,
socket_override,
}) => {
assert_eq!(pod_name, "pod");
assert_eq!(socket_override, None);
}
_ => panic!("expected PodName mode"),
}
}
#[test]
fn parse_memory_lint_mode() {
match parse_args_from([
"memory",
"lint",
"--workspace",
"/tmp/ws",
"--json",
"--warnings-as-errors",
])
.unwrap()
{
Mode::MemoryLint(options) => {
assert_eq!(options.workspace, Some(PathBuf::from("/tmp/ws")));
assert!(options.json);
assert!(options.warnings_as_errors);
}
_ => panic!("expected MemoryLint mode"),
}
}
#[test]
fn parse_memory_lint_rejects_usage_errors() {
let err = parse_args_from(["memory", "lint", "--workspace"]).unwrap_err();
assert_eq!(err.to_string(), "--workspace requires a value");
}
#[test]
fn parse_memory_lint_workspace_equals() {
match parse_args_from(["memory", "lint", "--workspace=/tmp/ws"]).unwrap() {
Mode::MemoryLint(options) => {
assert_eq!(options.workspace, Some(PathBuf::from("/tmp/ws")));
assert!(!options.json);
assert!(!options.warnings_as_errors);
}
_ => panic!("expected MemoryLint mode"),
}
}
#[test]
fn memory_lint_with_other_second_word_remains_positional_pod_name() {
match parse_args_from(["memory", "other"]).unwrap() {
Mode::Tui(LaunchMode::PodName { pod_name, .. }) => assert_eq!(pod_name, "memory"),
_ => panic!("expected PodName mode"),
}
}
#[test]
fn parse_rejects_pod_and_session() {
let segment_id = session_store::new_segment_id().to_string();
let err = parse_args_from(["--pod", "agent", "--session", &segment_id]).unwrap_err();
assert_eq!(
err.to_string(),
"--pod and --session are mutually exclusive"
);
}
#[test]
fn parse_rejects_resume_and_pod_name_selection() {
let cases = [
(
vec!["-r".to_string(), "--pod".to_string(), "agent".to_string()],
"--pod and --resume are mutually exclusive",
),
(
vec!["--pod".to_string(), "agent".to_string(), "-r".to_string()],
"--pod and --resume are mutually exclusive",
),
(
vec!["-r".to_string(), "agent".to_string()],
"--resume cannot be used with a positional Pod name",
),
];
for (args, message) in cases {
let err = parse_args_from(args).unwrap_err();
assert_eq!(err.to_string(), message);
}
}
#[test]
fn parse_profile_spawn_mode() {
match parse_args_from(["--profile", "/profiles/coder.lua"]).unwrap() {
Mode::Tui(LaunchMode::Spawn { profile }) => {
assert_eq!(profile, Some("/profiles/coder.lua".to_string()));
}
_ => panic!("expected Spawn mode"),
}
}
#[test]
fn parse_profile_rejects_resume_attach_modes() {
let segment_id = session_store::new_segment_id().to_string();
let cases = [
(
vec![
"--profile".to_string(),
"p.lua".to_string(),
"--resume".to_string(),
],
"--profile can only be used for fresh spawn",
),
(
vec![
"--profile".to_string(),
"p.lua".to_string(),
"--session".to_string(),
segment_id,
],
"--profile can only be used for fresh spawn",
),
(
vec![
"--profile".to_string(),
"p.lua".to_string(),
"--socket".to_string(),
"/tmp/insomnia/sock".to_string(),
],
"--profile can only be used for fresh spawn",
),
(
vec![
"--profile".to_string(),
"p.lua".to_string(),
"agent".to_string(),
],
"--profile can only be used for fresh spawn",
),
];
for (args, message) in cases {
let err = parse_args_from(args).unwrap_err();
assert_eq!(err.to_string(), message);
}
}
#[test]
fn parse_multi_mode() {
match parse_args_from(["--multi"]).unwrap() {
Mode::Tui(LaunchMode::Multi) => {}
_ => panic!("expected Multi mode"),
}
}
#[test]
fn parse_top_level_help() {
match parse_args_from(["--help"]).unwrap() {
Mode::Help => {}
_ => panic!("expected Help mode"),
}
}
#[test]
fn parse_memory_lint_help() {
match parse_args_from(["memory", "lint", "--help"]).unwrap() {
Mode::MemoryLintHelp => {}
_ => panic!("expected MemoryLintHelp mode"),
}
}
#[test]
fn parse_multi_conflicts_are_clear() {
let segment_id = session_store::new_segment_id().to_string();
let cases = [
(
vec!["--multi".to_string(), "--resume".to_string()],
"--multi and --resume are mutually exclusive",
),
(
vec!["--multi".to_string(), "--session".to_string(), segment_id],
"--multi and --session are mutually exclusive",
),
(
vec![
"--multi".to_string(),
"--pod".to_string(),
"agent".to_string(),
],
"--multi and --pod are mutually exclusive",
),
(
vec!["--multi".to_string(), "agent".to_string()],
"--multi cannot be used with a positional Pod name",
),
(
vec![
"--multi".to_string(),
"--socket".to_string(),
"/tmp/a.sock".to_string(),
],
"--multi and --socket are mutually exclusive",
),
];
for (args, message) in cases {
let err = parse_args_from(args).unwrap_err();
assert_eq!(err.to_string(), message);
}
}
}

View File

@ -25,49 +25,36 @@
use std::path::PathBuf;
/// Environment variable that points at installed project resources.
pub const RESOURCE_DIR_ENV: &str = "INSOMNIA_RESOURCE_DIR";
/// 設定ディレクトリ。`profiles.toml`, `providers.toml`, `models.toml`,
/// `prompts/` などが置かれる。
pub fn config_dir() -> Option<PathBuf> {
if let Some(p) = env_path("INSOMNIA_CONFIG_DIR") {
return Some(p);
}
if let Some(p) = env_path("INSOMNIA_HOME") {
return Some(p.join("config"));
}
if let Some(p) = env_path("XDG_CONFIG_HOME") {
return Some(p.join("insomnia"));
}
Some(env_path("HOME")?.join(".config").join("insomnia"))
resolve_config_dir_from_parts(
env_path("INSOMNIA_CONFIG_DIR"),
env_path("INSOMNIA_HOME"),
env_path("XDG_CONFIG_HOME"),
env_path("HOME"),
)
}
/// データディレクトリ。`sessions/` などプログラムが書く永続データの
/// 置き場。
pub fn data_dir() -> Option<PathBuf> {
if let Some(p) = env_path("INSOMNIA_DATA_DIR") {
return Some(p);
}
if let Some(p) = env_path("INSOMNIA_HOME") {
return Some(p);
}
Some(env_path("HOME")?.join(".insomnia"))
resolve_data_dir_from_parts(
env_path("INSOMNIA_DATA_DIR"),
env_path("INSOMNIA_HOME"),
env_path("HOME"),
)
}
/// ランタイムディレクトリ。socket, `pods.json`, Pod ごとの `pid` /
/// `status.json` 等が置かれる。再起動で消えて構わない。
pub fn runtime_dir() -> Option<PathBuf> {
if let Some(p) = env_path("INSOMNIA_RUNTIME_DIR") {
return Some(p);
}
if let Some(p) = env_path("INSOMNIA_HOME") {
return Some(p.join("run"));
}
if let Some(p) = env_path("XDG_RUNTIME_DIR") {
return Some(p.join("insomnia"));
}
Some(env_path("HOME")?.join(".insomnia").join("run"))
resolve_runtime_dir_from_parts(
env_path("INSOMNIA_RUNTIME_DIR"),
env_path("INSOMNIA_HOME"),
env_path("XDG_RUNTIME_DIR"),
env_path("HOME"),
)
}
// ---- well-known file getters ------------------------------------------------
@ -77,64 +64,38 @@ pub fn runtime_dir() -> Option<PathBuf> {
/// This is application/profile selection configuration, not a Pod manifest
/// layer.
pub fn user_profiles_path() -> Option<PathBuf> {
Some(config_dir()?.join("profiles.toml"))
user_profiles_path_from_config_dir(config_dir())
}
/// `<config_dir>/prompts/` — user prompts ライブラリ。
pub fn user_prompts_dir() -> Option<PathBuf> {
Some(config_dir()?.join("prompts"))
}
/// Root resource directory used for bundled prompts, profiles, catalogs, and docs.
pub fn resource_dir() -> Option<PathBuf> {
if let Some(p) = env_path(RESOURCE_DIR_ENV) {
return Some(p);
}
if let Ok(exe) = std::env::current_exe() {
if let Some(prefix) = exe.parent().and_then(|bin| bin.parent()) {
let installed = prefix.join("share").join("insomnia").join("resources");
if installed.exists() {
return Some(installed);
}
}
}
Some(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.join("resources"),
)
}
/// Bundled Lua profile registry directory. Missing directories are treated as
/// an empty builtin registry by discovery.
pub fn builtin_profiles_dir() -> Option<PathBuf> {
Some(resource_dir()?.join("profiles"))
user_prompts_dir_from_config_dir(config_dir())
}
/// `<config_dir>/prompts.toml` — user prompt pack。
pub fn user_pack_file() -> Option<PathBuf> {
Some(config_dir()?.join("prompts.toml"))
user_pack_file_from_config_dir(config_dir())
}
/// `<config_dir>/<file_name>` — providers.toml / models.toml 等の
/// user override ファイル。
pub fn user_catalog_override(file_name: &str) -> Option<PathBuf> {
Some(config_dir()?.join(file_name))
user_catalog_override_from_config_dir(config_dir(), file_name)
}
/// `<data_dir>/sessions/` — session store のデフォルト位置。
pub fn sessions_dir() -> Option<PathBuf> {
Some(data_dir()?.join("sessions"))
sessions_dir_from_data_dir(data_dir())
}
/// `<runtime_dir>/pods.json` — machine-wide Pod allocation registry。
pub fn pod_registry_path() -> Option<PathBuf> {
Some(runtime_dir()?.join("pods.json"))
pod_registry_path_from_runtime_dir(runtime_dir())
}
/// `<runtime_dir>/<pod_name>/` — Pod ごとのランタイムディレクトリ。
pub fn pod_runtime_dir(pod_name: &str) -> Option<PathBuf> {
Some(runtime_dir()?.join(pod_name))
pod_runtime_dir_from_runtime_dir(runtime_dir(), pod_name)
}
/// `<runtime_dir>/<pod_name>/sock` — Pod の Unix socket パス。
@ -144,206 +105,306 @@ pub fn pod_runtime_dir(pod_name: &str) -> Option<PathBuf> {
/// attach フロー等) からの**予測**はこの関数で行う。両者は同じパス
/// を返すことが期待される。
pub fn pod_socket_path(pod_name: &str) -> Option<PathBuf> {
Some(pod_runtime_dir(pod_name)?.join("sock"))
pod_socket_path_from_runtime_dir(runtime_dir(), pod_name)
}
// ---- internals --------------------------------------------------------------
fn resolve_config_dir_from_parts(
insomnia_config_dir: Option<PathBuf>,
insomnia_home: Option<PathBuf>,
xdg_config_home: Option<PathBuf>,
home: Option<PathBuf>,
) -> Option<PathBuf> {
if let Some(p) = insomnia_config_dir {
return Some(p);
}
if let Some(p) = insomnia_home {
return Some(p.join("config"));
}
if let Some(p) = xdg_config_home {
return Some(p.join("insomnia"));
}
Some(home?.join(".config").join("insomnia"))
}
fn resolve_data_dir_from_parts(
insomnia_data_dir: Option<PathBuf>,
insomnia_home: Option<PathBuf>,
home: Option<PathBuf>,
) -> Option<PathBuf> {
if let Some(p) = insomnia_data_dir {
return Some(p);
}
if let Some(p) = insomnia_home {
return Some(p);
}
Some(home?.join(".insomnia"))
}
fn resolve_runtime_dir_from_parts(
insomnia_runtime_dir: Option<PathBuf>,
insomnia_home: Option<PathBuf>,
xdg_runtime_dir: Option<PathBuf>,
home: Option<PathBuf>,
) -> Option<PathBuf> {
if let Some(p) = insomnia_runtime_dir {
return Some(p);
}
if let Some(p) = insomnia_home {
return Some(p.join("run"));
}
if let Some(p) = xdg_runtime_dir {
return Some(p.join("insomnia"));
}
Some(home?.join(".insomnia").join("run"))
}
fn user_profiles_path_from_config_dir(config_dir: Option<PathBuf>) -> Option<PathBuf> {
Some(config_dir?.join("profiles.toml"))
}
fn user_prompts_dir_from_config_dir(config_dir: Option<PathBuf>) -> Option<PathBuf> {
Some(config_dir?.join("prompts"))
}
fn user_pack_file_from_config_dir(config_dir: Option<PathBuf>) -> Option<PathBuf> {
Some(config_dir?.join("prompts.toml"))
}
fn user_catalog_override_from_config_dir(
config_dir: Option<PathBuf>,
file_name: &str,
) -> Option<PathBuf> {
Some(config_dir?.join(file_name))
}
fn sessions_dir_from_data_dir(data_dir: Option<PathBuf>) -> Option<PathBuf> {
Some(data_dir?.join("sessions"))
}
fn pod_registry_path_from_runtime_dir(runtime_dir: Option<PathBuf>) -> Option<PathBuf> {
Some(runtime_dir?.join("pods.json"))
}
fn pod_runtime_dir_from_runtime_dir(
runtime_dir: Option<PathBuf>,
pod_name: &str,
) -> Option<PathBuf> {
Some(runtime_dir?.join(pod_name))
}
fn pod_socket_path_from_runtime_dir(
runtime_dir: Option<PathBuf>,
pod_name: &str,
) -> Option<PathBuf> {
Some(pod_runtime_dir_from_runtime_dir(runtime_dir, pod_name)?.join("sock"))
}
/// 空文字列の env は未設定として扱う。`std::env::var` は `Ok("")` と
/// `Err(NotPresent)` を区別するが、パス解決においては両者を未設定と
/// 同等に扱うのが直感的。
fn env_path(name: &str) -> Option<PathBuf> {
std::env::var(name)
.ok()
.filter(|s| !s.is_empty())
.map(PathBuf::from)
let value = std::env::var(name).ok()?;
path_from_env_value(Some(value.as_str()))
}
fn path_from_env_value(value: Option<&str>) -> Option<PathBuf> {
value.filter(|s| !s.is_empty()).map(PathBuf::from)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Mutex, MutexGuard, OnceLock};
/// プロセス全体で env を弄るテスト同士が並行に走らないように保護
/// する。Cargo の test harness はファイル単位で別プロセスにせず
/// マルチスレッドで実行するため、env を読む全テストはこの lock を
/// 取ってから操作する。
fn env_lock() -> MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|e| e.into_inner())
}
/// テスト中だけ env を上書きし、drop 時に元の値に戻す RAII guard。
struct EnvGuard {
vars: Vec<(&'static str, Option<String>)>,
_lock: MutexGuard<'static, ()>,
}
impl EnvGuard {
fn new(overrides: &[(&'static str, Option<&str>)]) -> Self {
let lock = env_lock();
let names = [
"INSOMNIA_CONFIG_DIR",
"INSOMNIA_DATA_DIR",
"INSOMNIA_RUNTIME_DIR",
"INSOMNIA_RESOURCE_DIR",
"INSOMNIA_HOME",
"XDG_CONFIG_HOME",
"XDG_RUNTIME_DIR",
"HOME",
];
let saved: Vec<_> = names.iter().map(|n| (*n, std::env::var(n).ok())).collect();
// SAFETY: env_lock() 取得済みなので env への並行アクセスは
// この test バイナリ内では発生しない。
unsafe {
for (n, _) in &saved {
std::env::remove_var(n);
}
for (n, v) in overrides {
if let Some(v) = v {
std::env::set_var(n, v);
}
}
}
Self {
vars: saved,
_lock: lock,
}
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
// SAFETY: lock を握ったまま元に戻す。
unsafe {
for (n, v) in &self.vars {
match v {
Some(v) => std::env::set_var(n, v),
None => std::env::remove_var(n),
}
}
}
}
}
#[test]
fn config_dir_falls_back_to_home_dot_config() {
let _g = EnvGuard::new(&[("HOME", Some("/h"))]);
assert_eq!(config_dir().unwrap(), PathBuf::from("/h/.config/insomnia"));
assert_eq!(
resolve_config_dir_from_parts(None, None, None, Some(PathBuf::from("/h"))).unwrap(),
PathBuf::from("/h/.config/insomnia")
);
}
#[test]
fn config_dir_uses_xdg_when_set() {
let _g = EnvGuard::new(&[("HOME", Some("/h")), ("XDG_CONFIG_HOME", Some("/x"))]);
assert_eq!(config_dir().unwrap(), PathBuf::from("/x/insomnia"));
assert_eq!(
resolve_config_dir_from_parts(
None,
None,
Some(PathBuf::from("/x")),
Some(PathBuf::from("/h")),
)
.unwrap(),
PathBuf::from("/x/insomnia")
);
}
#[test]
fn config_dir_insomnia_home_outranks_xdg() {
let _g = EnvGuard::new(&[
("HOME", Some("/h")),
("XDG_CONFIG_HOME", Some("/x")),
("INSOMNIA_HOME", Some("/sand")),
]);
assert_eq!(config_dir().unwrap(), PathBuf::from("/sand/config"));
assert_eq!(
resolve_config_dir_from_parts(
None,
Some(PathBuf::from("/sand")),
Some(PathBuf::from("/x")),
Some(PathBuf::from("/h")),
)
.unwrap(),
PathBuf::from("/sand/config")
);
}
#[test]
fn config_dir_explicit_wins_over_insomnia_home() {
let _g = EnvGuard::new(&[
("HOME", Some("/h")),
("INSOMNIA_HOME", Some("/sand")),
("INSOMNIA_CONFIG_DIR", Some("/explicit-cfg")),
]);
assert_eq!(config_dir().unwrap(), PathBuf::from("/explicit-cfg"));
assert_eq!(
resolve_config_dir_from_parts(
Some(PathBuf::from("/explicit-cfg")),
Some(PathBuf::from("/sand")),
None,
Some(PathBuf::from("/h")),
)
.unwrap(),
PathBuf::from("/explicit-cfg")
);
}
#[test]
fn data_dir_default_is_dot_insomnia() {
let _g = EnvGuard::new(&[("HOME", Some("/h"))]);
assert_eq!(data_dir().unwrap(), PathBuf::from("/h/.insomnia"));
assert_eq!(
resolve_data_dir_from_parts(None, None, Some(PathBuf::from("/h"))).unwrap(),
PathBuf::from("/h/.insomnia")
);
}
#[test]
fn data_dir_insomnia_home_is_data_dir_itself() {
let _g = EnvGuard::new(&[("HOME", Some("/h")), ("INSOMNIA_HOME", Some("/sand"))]);
assert_eq!(data_dir().unwrap(), PathBuf::from("/sand"));
assert_eq!(
resolve_data_dir_from_parts(
None,
Some(PathBuf::from("/sand")),
Some(PathBuf::from("/h"))
)
.unwrap(),
PathBuf::from("/sand")
);
}
#[test]
fn data_dir_explicit_wins_over_insomnia_home() {
assert_eq!(
resolve_data_dir_from_parts(
Some(PathBuf::from("/explicit-data")),
Some(PathBuf::from("/sand")),
Some(PathBuf::from("/h")),
)
.unwrap(),
PathBuf::from("/explicit-data")
);
}
#[test]
fn runtime_dir_prefers_xdg_runtime_dir() {
let _g = EnvGuard::new(&[
("HOME", Some("/h")),
("XDG_RUNTIME_DIR", Some("/xdg-runtime")),
]);
assert_eq!(
runtime_dir().unwrap(),
resolve_runtime_dir_from_parts(
None,
None,
Some(PathBuf::from("/xdg-runtime")),
Some(PathBuf::from("/h")),
)
.unwrap(),
PathBuf::from("/xdg-runtime/insomnia")
);
}
#[test]
fn runtime_dir_falls_back_to_dot_insomnia_run() {
let _g = EnvGuard::new(&[("HOME", Some("/h"))]);
assert_eq!(runtime_dir().unwrap(), PathBuf::from("/h/.insomnia/run"));
assert_eq!(
resolve_runtime_dir_from_parts(None, None, None, Some(PathBuf::from("/h"))).unwrap(),
PathBuf::from("/h/.insomnia/run")
);
}
#[test]
fn runtime_dir_insomnia_home_is_run_subdir() {
let _g = EnvGuard::new(&[
("HOME", Some("/h")),
("XDG_RUNTIME_DIR", Some("/run/user/1000")),
("INSOMNIA_HOME", Some("/sand")),
]);
assert_eq!(runtime_dir().unwrap(), PathBuf::from("/sand/run"));
assert_eq!(
resolve_runtime_dir_from_parts(
None,
Some(PathBuf::from("/sand")),
Some(PathBuf::from("/run/user/1000")),
Some(PathBuf::from("/h")),
)
.unwrap(),
PathBuf::from("/sand/run")
);
}
#[test]
fn empty_env_treated_as_unset() {
let _g = EnvGuard::new(&[("HOME", Some("/h")), ("XDG_CONFIG_HOME", Some(""))]);
assert_eq!(config_dir().unwrap(), PathBuf::from("/h/.config/insomnia"));
fn runtime_dir_explicit_wins_over_insomnia_home() {
assert_eq!(
resolve_runtime_dir_from_parts(
Some(PathBuf::from("/explicit-run")),
Some(PathBuf::from("/sand")),
Some(PathBuf::from("/run/user/1000")),
Some(PathBuf::from("/h")),
)
.unwrap(),
PathBuf::from("/explicit-run")
);
}
#[test]
fn empty_env_value_treated_as_unset_before_path_resolution() {
let xdg_config_home = path_from_env_value(Some(""));
assert_eq!(
resolve_config_dir_from_parts(None, None, xdg_config_home, Some(PathBuf::from("/h")))
.unwrap(),
PathBuf::from("/h/.config/insomnia")
);
}
#[test]
fn returns_none_when_nothing_set() {
let _g = EnvGuard::new(&[]);
assert!(config_dir().is_none());
assert!(data_dir().is_none());
assert!(runtime_dir().is_none());
assert!(resolve_config_dir_from_parts(None, None, None, None).is_none());
assert!(resolve_data_dir_from_parts(None, None, None).is_none());
assert!(resolve_runtime_dir_from_parts(None, None, None, None).is_none());
}
#[test]
fn well_known_files_compose_off_base_dirs() {
let _g = EnvGuard::new(&[("INSOMNIA_HOME", Some("/sand"))]);
let config_dir = Some(PathBuf::from("/sand/config"));
let data_dir = Some(PathBuf::from("/sand"));
let runtime_dir = Some(PathBuf::from("/sand/run"));
assert_eq!(
user_profiles_path().unwrap(),
user_profiles_path_from_config_dir(config_dir.clone()).unwrap(),
PathBuf::from("/sand/config/profiles.toml")
);
assert_eq!(
user_prompts_dir().unwrap(),
user_prompts_dir_from_config_dir(config_dir.clone()).unwrap(),
PathBuf::from("/sand/config/prompts")
);
assert_eq!(
user_pack_file().unwrap(),
user_pack_file_from_config_dir(config_dir.clone()).unwrap(),
PathBuf::from("/sand/config/prompts.toml")
);
assert_eq!(
user_catalog_override("providers.toml").unwrap(),
user_catalog_override_from_config_dir(config_dir, "providers.toml").unwrap(),
PathBuf::from("/sand/config/providers.toml")
);
assert_eq!(sessions_dir().unwrap(), PathBuf::from("/sand/sessions"));
assert_eq!(
pod_registry_path().unwrap(),
sessions_dir_from_data_dir(data_dir).unwrap(),
PathBuf::from("/sand/sessions")
);
assert_eq!(
pod_registry_path_from_runtime_dir(runtime_dir.clone()).unwrap(),
PathBuf::from("/sand/run/pods.json")
);
assert_eq!(
pod_runtime_dir("foo").unwrap(),
pod_runtime_dir_from_runtime_dir(runtime_dir.clone(), "foo").unwrap(),
PathBuf::from("/sand/run/foo")
);
assert_eq!(
pod_socket_path("foo").unwrap(),
pod_socket_path_from_runtime_dir(runtime_dir, "foo").unwrap(),
PathBuf::from("/sand/run/foo/sock")
);
}

View File

@ -22,6 +22,8 @@ use crate::{
const PROFILE_FORMAT_V1: &str = "insomnia.lua-profile.v1";
const BUILTIN_DEFAULT_PROFILE_NAME: &str = "default";
const BUILTIN_DEFAULT_PROFILE: &str = include_str!("../../../resources/profiles/default.lua");
const BUILTIN_MODEL_CATALOG: &str = include_str!("../../../resources/models/builtin.toml");
const DEFAULT_POD_NAME: &str = "insomnia";
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
@ -126,7 +128,10 @@ pub enum ProfileSource {
Registry {
source: ProfileRegistrySource,
name: String,
path: PathBuf,
#[serde(default, skip_serializing_if = "Option::is_none")]
path: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
provenance: Option<String>,
},
}
@ -134,15 +139,62 @@ pub enum ProfileSource {
pub struct ProfileRegistryEntry {
pub source: ProfileRegistrySource,
pub name: String,
pub path: PathBuf,
pub path: Option<PathBuf>,
pub provenance: String,
pub description: Option<String>,
pub is_default: bool,
artifact: ProfileRegistryArtifact,
}
impl ProfileRegistryEntry {
pub fn qualified_name(&self) -> String {
format!("{}:{}", self.source, self.name)
}
fn path(
source: ProfileRegistrySource,
name: String,
path: PathBuf,
description: Option<String>,
) -> Self {
let provenance = path.display().to_string();
Self {
source,
name,
path: Some(path.clone()),
provenance,
description,
is_default: false,
artifact: ProfileRegistryArtifact::Path(path),
}
}
fn embedded(
source: ProfileRegistrySource,
name: &'static str,
label: &'static str,
content: &'static str,
description: Option<String>,
) -> Self {
Self {
source,
name: name.to_string(),
path: None,
provenance: label.to_string(),
description,
is_default: false,
artifact: ProfileRegistryArtifact::Embedded { label, content },
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ProfileRegistryArtifact {
Path(PathBuf),
Embedded {
label: &'static str,
content: &'static str,
},
}
#[derive(Debug, Clone, Default)]
@ -241,7 +293,6 @@ struct ProfileDefault {
#[derive(Debug, Clone)]
pub struct ProfileDiscovery {
builtin_dir: Option<PathBuf>,
user_config: Option<PathBuf>,
project_config: Option<PathBuf>,
}
@ -249,27 +300,19 @@ pub struct ProfileDiscovery {
impl ProfileDiscovery {
pub fn for_cwd(cwd: &Path) -> Self {
Self {
builtin_dir: paths::builtin_profiles_dir(),
user_config: paths::user_profiles_path(),
project_config: find_project_profiles_from(cwd),
}
}
pub fn with_sources(
builtin_dir: Option<PathBuf>,
user_config: Option<PathBuf>,
project_config: Option<PathBuf>,
) -> Self {
pub fn with_sources(user_config: Option<PathBuf>, project_config: Option<PathBuf>) -> Self {
Self {
builtin_dir,
user_config,
project_config,
}
}
pub fn discover(&self) -> Result<ProfileRegistry, ProfileError> {
let mut registry = ProfileRegistry::default();
if let Some(dir) = &self.builtin_dir {
discover_profile_dir(&mut registry, ProfileRegistrySource::Builtin, dir)?;
}
add_builtin_profiles(&mut registry);
if let Some(path) = &self.user_config {
load_profile_registry_file(&mut registry, ProfileRegistrySource::User, path)?;
}
@ -371,15 +414,27 @@ impl ProfileResolver {
)),
ProfileSelector::Named { .. } | ProfileSelector::Default => {
let entry = registry.select(selector)?.clone();
self.resolve_path(
&entry.path,
ProfileSource::Registry {
source: entry.source,
name: entry.name,
path: absolutize(&entry.path)?,
},
options,
)
let source = ProfileSource::Registry {
source: entry.source,
name: entry.name.clone(),
path: entry.path.as_deref().map(absolutize).transpose()?,
provenance: (entry.path.is_none()).then(|| entry.provenance.clone()),
};
self.resolve_registry_entry(&entry, source, options)
}
}
}
fn resolve_registry_entry(
&self,
entry: &ProfileRegistryEntry,
source: ProfileSource,
options: ProfileResolveOptions,
) -> Result<ResolvedProfile, ProfileError> {
match &entry.artifact {
ProfileRegistryArtifact::Path(path) => self.resolve_path(path, source, options),
ProfileRegistryArtifact::Embedded { label, content } => {
self.resolve_embedded_profile(label, content, source, options)
}
}
}
@ -431,6 +486,30 @@ impl ProfileResolver {
raw_artifact,
)
}
fn resolve_embedded_profile(
&self,
label: &'static str,
content: &'static str,
source: ProfileSource,
options: ProfileResolveOptions,
) -> Result<ResolvedProfile, ProfileError> {
let workspace_base = absolutize(
self.workspace_base
.as_deref()
.unwrap_or_else(|| Path::new(".")),
)?;
let lua_value = evaluate_embedded_lua_profile(label, content)?;
let raw_artifact = lua_value.clone();
resolve_lua_profile_value(
source,
&workspace_base,
&workspace_base,
options,
lua_value,
raw_artifact,
)
}
}
fn resolve_lua_profile_value(
@ -591,13 +670,12 @@ fn load_profile_registry_file(
let base = path.parent().unwrap_or_else(|| Path::new("."));
for (name, entry_config) in config.profile {
let (entry_path, description) = entry_config.into_parts();
registry.push_entry(ProfileRegistryEntry {
registry.push_entry(ProfileRegistryEntry::path(
source,
name,
path: join_if_relative(base, &entry_path),
join_if_relative(base, &entry_path),
description,
is_default: false,
});
));
}
if let Some(default) = config.default {
let (default_source, default_name) = parse_profile_ref(&default);
@ -625,49 +703,14 @@ fn find_project_profiles_from(start: &Path) -> Option<PathBuf> {
None
}
fn discover_profile_dir(
registry: &mut ProfileRegistry,
source: ProfileRegistrySource,
dir: &Path,
) -> Result<(), ProfileError> {
if !dir.is_dir() {
return Ok(());
}
for entry in std::fs::read_dir(dir).map_err(|source| ProfileError::ConfigRead {
path: dir.to_path_buf(),
source,
})? {
let entry = entry.map_err(|source| ProfileError::ConfigRead {
path: dir.to_path_buf(),
source,
})?;
let path = entry.path();
if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("lua") {
if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
registry.push_entry(ProfileRegistryEntry {
source,
name: name.to_string(),
path,
description: None,
is_default: false,
});
}
} else if path.is_dir() {
let profile = path.join("profile.lua");
if profile.is_file()
&& let Some(name) = path.file_name().and_then(|s| s.to_str())
{
registry.push_entry(ProfileRegistryEntry {
source,
name: name.to_string(),
path: profile,
description: None,
is_default: false,
});
}
}
}
Ok(())
fn add_builtin_profiles(registry: &mut ProfileRegistry) {
registry.push_entry(ProfileRegistryEntry::embedded(
ProfileRegistrySource::Builtin,
BUILTIN_DEFAULT_PROFILE_NAME,
"builtin:default",
BUILTIN_DEFAULT_PROFILE,
Some("Bundled default Insomnia coding profile".into()),
));
}
fn parse_profile_ref(raw: &str) -> (Option<ProfileRegistrySource>, String) {
@ -687,15 +730,38 @@ fn evaluate_lua_profile(
path: path.to_path_buf(),
source,
})?;
evaluate_lua_profile_source(
&content,
path.display().to_string(),
LocalModuleRoot::Filesystem(module_root.to_path_buf()),
)
}
fn evaluate_embedded_lua_profile(
label: &'static str,
content: &'static str,
) -> Result<serde_json::Value, ProfileError> {
evaluate_lua_profile_source(
content,
label.to_string(),
LocalModuleRoot::Disabled { label },
)
}
fn evaluate_lua_profile_source(
content: &str,
chunk_name: String,
module_root: LocalModuleRoot,
) -> Result<serde_json::Value, ProfileError> {
let lua = Lua::new_with(
StdLib::TABLE | StdLib::STRING | StdLib::MATH | StdLib::UTF8,
LuaOptions::default(),
)
.map_err(ProfileError::Lua)?;
install_lua_api(&lua, module_root.to_path_buf())?;
install_lua_api(&lua, module_root)?;
let value: LuaValue = lua
.load(&content)
.set_name(path.display().to_string())
.load(content)
.set_name(chunk_name)
.eval()
.map_err(ProfileError::Lua)?;
match value {
@ -706,7 +772,7 @@ fn evaluate_lua_profile(
}
}
fn install_lua_api(lua: &Lua, module_root: PathBuf) -> Result<(), ProfileError> {
fn install_lua_api(lua: &Lua, module_root: LocalModuleRoot) -> Result<(), ProfileError> {
let loader = Rc::new(RefCell::new(LocalModuleLoader {
root: module_root,
cache: HashMap::new(),
@ -743,11 +809,16 @@ fn install_lua_api(lua: &Lua, module_root: PathBuf) -> Result<(), ProfileError>
}
struct LocalModuleLoader {
root: PathBuf,
root: LocalModuleRoot,
cache: HashMap<String, RegistryKey>,
loading: HashSet<String>,
}
enum LocalModuleRoot {
Filesystem(PathBuf),
Disabled { label: &'static str },
}
fn require_module(
lua: &Lua,
loader: &Rc<RefCell<LocalModuleLoader>>,
@ -773,7 +844,19 @@ fn require_module(
)));
}
}
let path = local_module_path(&loader.borrow().root, name).map_err(mlua::Error::RuntimeError)?;
let path = {
let state = loader.borrow();
match &state.root {
LocalModuleRoot::Filesystem(root) => {
local_module_path(root, name).map_err(mlua::Error::RuntimeError)?
}
LocalModuleRoot::Disabled { label } => {
return Err(mlua::Error::RuntimeError(format!(
"local require `{name}` is not available for embedded profile `{label}`"
)));
}
}
};
let content = std::fs::read_to_string(&path).map_err(|e| {
mlua::Error::RuntimeError(format!(
"failed to read local module `{name}` ({}): {e}",
@ -1042,9 +1125,7 @@ fn model_context_window(model: Option<&ModelManifest>) -> Option<u64> {
}
fn builtin_model_context_window(reference: &str) -> Option<u64> {
let (provider, model_id) = reference.split_once('/')?;
let path = paths::resource_dir()?.join("models").join("builtin.toml");
let content = std::fs::read_to_string(path).ok()?;
let parsed: toml::Value = toml::from_str(&content).ok()?;
let parsed: toml::Value = toml::from_str(BUILTIN_MODEL_CATALOG).ok()?;
for entry in parsed.get("model")?.as_array()? {
let table = entry.as_table()?;
if table.get("provider")?.as_str()? == provider && table.get("id")?.as_str()? == model_id {
@ -1187,58 +1268,8 @@ pub enum ProfileError {
mod tests {
use super::*;
use crate::{ReasoningControl, ReasoningEffort, SchemeKind};
use std::sync::{Mutex, MutexGuard, OnceLock};
use tempfile::TempDir;
fn env_lock() -> MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|e| e.into_inner())
}
struct EnvGuard {
vars: Vec<(&'static str, Option<String>)>,
_lock: MutexGuard<'static, ()>,
}
impl EnvGuard {
fn new(overrides: &[(&'static str, Option<&str>)]) -> Self {
let lock = env_lock();
let names = [
"INSOMNIA_CONFIG_DIR",
"INSOMNIA_RESOURCE_DIR",
"INSOMNIA_HOME",
"XDG_CONFIG_HOME",
"HOME",
];
let saved: Vec<_> = names.iter().map(|n| (*n, std::env::var(n).ok())).collect();
unsafe {
for (n, _) in &saved {
std::env::remove_var(n);
}
for (n, v) in overrides {
if let Some(v) = v {
std::env::set_var(n, v);
}
}
}
Self {
vars: saved,
_lock: lock,
}
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
unsafe {
for (n, v) in &self.vars {
match v {
Some(v) => std::env::set_var(n, v),
None => std::env::remove_var(n),
}
}
}
}
}
fn write_profile(dir: &Path, name: &str, body: &str) -> PathBuf {
let path = dir.join(name);
std::fs::write(&path, body).unwrap();
@ -1266,14 +1297,15 @@ mod tests {
}
#[test]
fn builtin_default_profile_is_registered_as_default() {
let registry = ProfileDiscovery::with_sources(paths::builtin_profiles_dir(), None, None)
let registry = ProfileDiscovery::with_sources(None, None)
.discover()
.unwrap();
let default = registry.default_entry().unwrap();
assert_eq!(default.source, ProfileRegistrySource::Builtin);
assert_eq!(default.name, BUILTIN_DEFAULT_PROFILE_NAME);
assert!(default.is_default);
assert!(default.path.ends_with("resources/profiles/default.lua"));
assert_eq!(default.path, None);
assert_eq!(default.provenance, "builtin:default");
}
#[test]
fn resolves_plain_lua_profile_with_runtime_pod_name_and_scope_intent() {
@ -1458,6 +1490,15 @@ return profile {
resolved.profile.as_ref().unwrap().name.as_deref(),
Some("default")
);
assert_eq!(
resolved.source,
ProfileSource::Registry {
source: ProfileRegistrySource::Builtin,
name: "default".into(),
path: None,
provenance: Some("builtin:default".into()),
}
);
}
#[test]
fn unsupported_profile_extension_has_clear_diagnostic() {
@ -1486,14 +1527,19 @@ return profile {
)
.unwrap();
std::fs::write(&project_config, "default = \"project:coder\"\n[profile.coder]\npath = \"profiles/project-coder.lua\"\ndescription = \"Project coder\"\n").unwrap();
let registry =
ProfileDiscovery::with_sources(None, Some(user_config), Some(project_config))
.discover()
.unwrap();
let registry = ProfileDiscovery::with_sources(Some(user_config), Some(project_config))
.discover()
.unwrap();
let default = registry.default_entry().unwrap();
assert_eq!(default.source, ProfileRegistrySource::Project);
assert_eq!(default.name, "coder");
assert!(default.path.ends_with("profiles/project-coder.lua"));
assert!(
default
.path
.as_ref()
.unwrap()
.ends_with("profiles/project-coder.lua")
);
}
#[test]
fn default_marks_direct_profile_entry() {
@ -1506,7 +1552,7 @@ return profile {
"default = \"coder\"\n[profile]\ncoder = \"profiles/coder.lua\"\n",
)
.unwrap();
let registry = ProfileDiscovery::with_sources(None, None, Some(project_config))
let registry = ProfileDiscovery::with_sources(None, Some(project_config))
.discover()
.unwrap();
let default = registry.default_entry().unwrap();
@ -1525,20 +1571,18 @@ return profile {
#[test]
fn unqualified_ambiguous_names_fail_closed() {
let mut registry = ProfileRegistry::default();
registry.push_entry(ProfileRegistryEntry {
source: ProfileRegistrySource::User,
name: "coder".to_string(),
path: PathBuf::from("/user/coder.lua"),
description: None,
is_default: false,
});
registry.push_entry(ProfileRegistryEntry {
source: ProfileRegistrySource::Project,
name: "coder".to_string(),
path: PathBuf::from("/project/coder.lua"),
description: None,
is_default: false,
});
registry.push_entry(ProfileRegistryEntry::path(
ProfileRegistrySource::User,
"coder".to_string(),
PathBuf::from("/user/coder.lua"),
None,
));
registry.push_entry(ProfileRegistryEntry::path(
ProfileRegistrySource::Project,
"coder".to_string(),
PathBuf::from("/project/coder.lua"),
None,
));
let err = registry
.select(&ProfileSelector::named("coder"))
.unwrap_err();
@ -1549,6 +1593,9 @@ return profile {
"coder",
))
.unwrap();
assert_eq!(selected.path, PathBuf::from("/project/coder.lua"));
assert_eq!(
selected.path.as_deref(),
Some(Path::new("/project/coder.lua"))
);
}
}

View File

@ -14,7 +14,7 @@ pod-store = { workspace = true }
manifest = { workspace = true }
protocol = { workspace = true }
provider = { workspace = true }
insomnia = { workspace = true }
client = { workspace = true }
pod-registry = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }

View File

@ -14,7 +14,7 @@ use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use insomnia::PodRuntimeCommand;
use client::PodRuntimeCommand;
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
use manifest::{Permission, ScopeRule};
use pod_store::{PodActiveSegmentRef, PodMetadata, PodMetadataStore};
@ -346,7 +346,13 @@ where
command.arg("--store").arg(store_dir);
}
let mut child = command.spawn().map_err(PodDiscoveryError::RestoreSpawn)?;
let mut child =
command
.spawn()
.map_err(|source| PodDiscoveryError::RestoreLaunchFailed {
command: runtime_command.clone(),
source,
})?;
let deadline = tokio::time::Instant::now() + RESTORE_START_TIMEOUT;
loop {
if probe_socket(socket_path).await.reachable {
@ -545,6 +551,12 @@ pub enum PodDiscoveryError {
ScopeLock(#[from] pod_registry::ScopeLockError),
#[error("failed to launch restore process: {0}")]
RestoreSpawn(io::Error),
#[error("failed to launch restore runtime command `{command}`: {source}")]
RestoreLaunchFailed {
command: PodRuntimeCommand,
#[source]
source: io::Error,
},
#[error("restore process exited before socket became reachable: {status}")]
RestoreExited { status: std::process::ExitStatus },
#[error("restore process did not become reachable before timeout")]
@ -779,6 +791,7 @@ fn discovery_error_to_tool_error(error: PodDiscoveryError) -> ToolError {
| PodDiscoveryError::PodStore(_)
| PodDiscoveryError::ScopeLock(_)
| PodDiscoveryError::RestoreSpawn(_)
| PodDiscoveryError::RestoreLaunchFailed { .. }
| PodDiscoveryError::RestoreExited { .. }
| PodDiscoveryError::RestoreTimeout => ToolError::ExecutionFailed(error.to_string()),
}

View File

@ -12,7 +12,7 @@ use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use insomnia::PodRuntimeCommand;
use client::PodRuntimeCommand;
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
use manifest::{
CompactionConfigPartial, FileUploadLimitsPartial, Permission, PermissionConfigPartial,
@ -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)
})
@ -868,7 +932,7 @@ mod tests {
std::fs::write(&registry_path, registry_toml).unwrap();
AvailableProfiles {
registry: Some(
ProfileDiscovery::with_sources(None, None, Some(registry_path))
ProfileDiscovery::with_sources(None, Some(registry_path))
.discover()
.unwrap(),
),
@ -1236,7 +1300,7 @@ return profile {
assert!(invalid.contains("Use `default`, `inherit`"));
assert!(invalid.contains("`project:coder`"));
let no_default = build_spawn_config_json_for_profile(
let default_config = build_spawn_config_json_for_profile(
&parent,
&available,
&project,
@ -1245,17 +1309,15 @@ return profile {
&scope,
SpawnProfileSelector::Default,
)
.unwrap_err();
assert!(no_default.contains("no default profile"), "{no_default}");
assert!(no_default.contains("Use `default`, `inherit`"));
assert!(no_default.contains("`project:coder`"));
.unwrap();
assert!(default_config.contains("\"name\":\"child\""));
let user_config = tmp.path().join("user-profiles.toml");
std::fs::write(&user_config, "[profile]\ncoder = \"user-coder.lua\"\n").unwrap();
let project_config = project.join(".insomnia/profiles.toml");
let ambiguous = AvailableProfiles {
registry: Some(
ProfileDiscovery::with_sources(None, Some(user_config), Some(project_config))
ProfileDiscovery::with_sources(Some(user_config), Some(project_config))
.discover()
.unwrap(),
),

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 client::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

@ -234,6 +234,17 @@ async fn brave_search(
)));
}
brave_search_with_api_key(client, cfg, &api_key, query, limit, offset).await
}
async fn brave_search_with_api_key(
client: &Client,
cfg: &WebSearchConfig,
api_key: &str,
query: &str,
limit: usize,
offset: usize,
) -> Result<ToolOutput, ToolError> {
let endpoint = cfg.base_url.as_deref().unwrap_or(BRAVE_SEARCH_ENDPOINT);
let mut url = Url::parse(endpoint).map_err(|err| {
ToolError::InvalidArgument(format!("invalid Brave search endpoint: {err}"))
@ -1694,6 +1705,17 @@ mod tests {
}))
}
fn brave_search_config(base_url: String) -> WebSearchConfig {
WebSearchConfig {
enabled: Some(true),
provider: Some(WebSearchProvider::Brave),
api_key_env: None,
timeout_secs: Some(2),
base_url: Some(base_url),
..Default::default()
}
}
#[test]
fn validates_brave_query_limits() {
validate_brave_query("hello world").unwrap();
@ -2019,30 +2041,16 @@ mod tests {
async fn searches_brave_with_bounded_output() {
let response = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{\"web\":{\"results\":[{\"title\":\"Example\",\"url\":\"https://example.com\",\"description\":\"Snippet\",\"extra_snippets\":[\"Extra\"],\"language\":\"en\"}]}}";
let (addr, captured) = serve_once_capture(response).await;
let env_name = format!("INSOMNIA_TEST_BRAVE_KEY_{}", std::process::id());
unsafe { std::env::set_var(&env_name, "test-key") };
let tools = WebTools::new(Some(WebConfig {
enabled: Some(true),
allow_private_addresses: Some(true),
search: Some(WebSearchConfig {
enabled: Some(true),
provider: Some(WebSearchProvider::Brave),
api_key_env: Some(env_name.clone()),
timeout_secs: Some(2),
base_url: Some(format!("http://{addr}/search")),
..Default::default()
}),
search: None,
fetch: None,
}));
let result = tools
.run_search(WebSearchInput {
query: "insomnia".into(),
limit: Some(1),
offset: Some(0),
})
let cfg = brave_search_config(format!("http://{addr}/search"));
let result = brave_search_with_api_key(&tools.client, &cfg, "test-key", "insomnia", 1, 0)
.await
.unwrap();
unsafe { std::env::remove_var(&env_name) };
let value: Value = serde_json::from_str(result.content.as_deref().unwrap()).unwrap();
let request = captured.lock().await.clone().unwrap();
assert!(request.starts_with("GET /search?q=insomnia&count=1&offset=0 "));
@ -2065,29 +2073,16 @@ mod tests {
);
let response: &'static str = Box::leak(response.into_boxed_str());
let addr = serve_once(response).await;
let env_name = format!("INSOMNIA_TEST_BRAVE_OVERSIZED_KEY_{}", std::process::id());
unsafe { std::env::set_var(&env_name, "test-key") };
let tools = WebTools::new(Some(WebConfig {
enabled: Some(true),
allow_private_addresses: Some(true),
search: Some(WebSearchConfig {
enabled: Some(true),
provider: Some(WebSearchProvider::Brave),
api_key_env: Some(env_name.clone()),
base_url: Some(format!("http://{addr}/search")),
..Default::default()
}),
search: None,
fetch: None,
}));
let err = tools
.run_search(WebSearchInput {
query: "insomnia".into(),
limit: Some(1),
offset: Some(0),
})
let cfg = brave_search_config(format!("http://{addr}/search"));
let err = brave_search_with_api_key(&tools.client, &cfg, "test-key", "insomnia", 1, 0)
.await
.unwrap_err();
unsafe { std::env::remove_var(&env_name) };
assert!(err.to_string().contains("Content-Length"));
}
}

View File

@ -4,10 +4,6 @@ version = "0.1.0"
edition.workspace = true
license.workspace = true
[[bin]]
name = "insomnia"
path = "src/main.rs"
[dependencies]
client = { workspace = true }
protocol = { workspace = true }
@ -19,10 +15,8 @@ unicode-width = "0.2.2"
uuid = { workspace = true }
toml = { workspace = true }
manifest = { workspace = true }
memory = { workspace = true }
session-store = { workspace = true }
pod-store = { workspace = true }
pod = { workspace = true }
pod-registry = { workspace = true }
serde = { workspace = true, features = ["derive"] }
pulldown-cmark = { version = "0.13.3", default-features = false }

View File

@ -16,7 +16,7 @@ use crate::command::{
use crate::input::InputBuffer;
use crate::scroll::Scroll;
use crate::task::TaskStore;
use crate::ui::Mode;
use crate::view_mode::Mode;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandCompletionApply {

117
crates/tui/src/lib.rs Normal file
View File

@ -0,0 +1,117 @@
mod app;
mod block;
mod cache;
mod command;
mod input;
mod markdown;
mod multi_pod;
mod picker;
mod pod_list;
mod scroll;
mod single_pod;
mod spawn;
mod task;
mod tool;
mod ui;
mod view_mode;
use std::io;
use std::path::PathBuf;
use std::process::ExitCode;
use crossterm::event::{DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste};
use crossterm::execute;
use crossterm::terminal::{LeaveAlternateScreen, disable_raw_mode, enable_raw_mode};
use session_store::SegmentId;
use client::PodRuntimeCommand;
#[derive(Debug, Clone)]
pub struct LaunchOptions {
pub mode: LaunchMode,
pub runtime_command: PodRuntimeCommand,
}
#[derive(Debug, Clone)]
pub enum LaunchMode {
Spawn {
profile: Option<String>,
},
/// `insomnia <name>` / `insomnia --pod <name>`: attach to a live Pod by name if
/// 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,
socket_override: Option<PathBuf>,
},
/// `insomnia -r` / `insomnia --resume`: open the Pod picker, then attach to the
/// selected live Pod or restore the selected stopped Pod by name.
Resume,
/// `insomnia --session <UUID>`: skip the picker, go straight to the
/// resume name dialog with `id` baked in.
ResumeWithSession(SegmentId),
/// `insomnia --multi`: open the multi-Pod dashboard. This is intentionally
/// separate from `-r`/`--resume`, which keeps its single-Pod picker
/// meaning.
Multi,
}
pub async fn launch(options: LaunchOptions) -> ExitCode {
let LaunchOptions {
mode,
runtime_command,
} = options;
if let Err(e) = enable_raw_mode() {
eprintln!("insomnia: failed to enter raw mode: {e}");
return ExitCode::FAILURE;
}
if let Err(e) = execute!(io::stdout(), EnableBracketedPaste) {
let _ = disable_raw_mode();
eprintln!("insomnia: {e}");
return ExitCode::FAILURE;
}
let result = match mode {
LaunchMode::Spawn { profile } => {
single_pod::run_spawn(None, profile, runtime_command).await
}
LaunchMode::PodName {
pod_name,
socket_override,
} => single_pod::run_pod_name(pod_name, socket_override, runtime_command).await,
LaunchMode::Resume => single_pod::run_resume(runtime_command).await,
LaunchMode::ResumeWithSession(id) => {
single_pod::run_spawn(Some(id), None, runtime_command).await
}
LaunchMode::Multi => single_pod::run_multi(runtime_command).await,
};
// Always restore the terminal first so any pending eprintln below
// shows up cleanly in scrollback rather than inside an active
// alternate-screen buffer.
let mut stdout = io::stdout();
let _ = execute!(
stdout,
DisableMouseCapture,
LeaveAlternateScreen,
DisableBracketedPaste
);
let _ = disable_raw_mode();
let _ = execute!(stdout, crossterm::cursor::Show);
match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
// SpawnError has already been painted into the inline
// viewport's final frame, so it's already visible in the
// user's scrollback — printing it again would be a noisy
// duplicate. Other errors (pod-name failures, terminal setup
// hiccups, etc.) need surfacing here.
if e.downcast_ref::<spawn::SpawnError>().is_none() {
eprintln!("insomnia: {e}");
}
ExitCode::FAILURE
}
}
}

View File

@ -1,23 +1,6 @@
mod app;
mod block;
mod cache;
mod command;
mod input;
mod markdown;
mod memory_lint;
mod multi_pod;
mod picker;
mod pod_list;
mod scroll;
mod spawn;
mod task;
mod tool;
mod ui;
use std::future::Future;
use std::io;
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
@ -26,24 +9,23 @@ use std::thread;
use std::time::Duration;
use crossterm::event::{
self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
Event as TermEvent, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind,
self, DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyEvent,
KeyModifiers, MouseEvent, MouseEventKind,
};
use crossterm::execute;
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
use protocol::{Method, PodStatus};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use session_store::SegmentId;
use tokio::sync::mpsc;
use client::PodClient;
use client::{PodClient, PodRuntimeCommand};
use crate::app::{ActionbarNoticeLevel, ActionbarNoticeSource, App};
use crate::picker::PickerOutcome;
use crate::spawn::{SpawnOutcome, SpawnReady};
use crate::{multi_pod, picker, spawn, ui};
type FullscreenTerminal = Terminal<CrosstermBackend<io::Stdout>>;
@ -59,296 +41,10 @@ fn resolve_socket(pod_name: &str, override_path: Option<PathBuf>) -> PathBuf {
})
}
#[derive(Debug)]
enum Mode {
Spawn {
profile: Option<String>,
},
/// `insomnia <name>` / `insomnia --pod <name>`: attach to a live Pod by name if
/// 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,
socket_override: Option<PathBuf>,
},
/// `insomnia -r` / `insomnia --resume`: open the Pod picker, then attach to the
/// selected live Pod or restore the selected stopped Pod by name.
Resume,
/// `insomnia --session <UUID>`: skip the picker, go straight to the
/// resume name dialog with `id` baked in.
ResumeWithSession(SegmentId),
/// `insomnia --multi`: open the multi-Pod dashboard. This is intentionally
/// separate from `-r`/`--resume`, which keeps its single-Pod picker
/// meaning.
Multi,
/// `insomnia memory lint`: headless lint for workspace memory and knowledge files.
MemoryLint(memory_lint::LintCliOptions),
/// `insomnia pod ...`: run the Pod runtime parser/entrypoint without TUI side effects.
PodRuntime(Vec<String>),
}
#[derive(Debug)]
enum ParseError {
Conflict(&'static str),
InvalidSession(String),
MemoryLint(memory_lint::UsageError),
MissingValue(&'static str),
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Conflict(message) => write!(f, "{message}"),
Self::InvalidSession(s) => write!(f, "invalid --session UUID: {s}"),
Self::MemoryLint(err) => write!(f, "{err}"),
Self::MissingValue(flag) => write!(f, "{flag} requires a value"),
}
}
}
fn parse_args() -> Result<Mode, ParseError> {
parse_args_from(std::env::args().skip(1))
}
fn parse_args_from<I, S>(args: I) -> Result<Mode, ParseError>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let args: Vec<String> = args.into_iter().map(Into::into).collect();
if args.first().map(String::as_str) == Some("memory")
&& args.get(1).map(String::as_str) == Some("lint")
{
let options = memory_lint::parse_lint_args(&args[2..]).map_err(ParseError::MemoryLint)?;
return Ok(Mode::MemoryLint(options));
}
if args.first().map(String::as_str) == Some("pod") {
return Ok(Mode::PodRuntime(args[1..].to_vec()));
}
let mut resume = false;
let mut multi = false;
let mut session: Option<SegmentId> = None;
let mut pod: Option<String> = None;
let mut profile: Option<String> = None;
let mut socket_override: Option<PathBuf> = None;
let mut socket_seen = false;
let mut positional: Option<String> = None;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"-r" | "--resume" => {
resume = true;
i += 1;
}
"--multi" => {
multi = true;
i += 1;
}
"--session" => {
let raw = args
.get(i + 1)
.ok_or(ParseError::MissingValue("--session"))?;
session = Some(
raw.parse::<SegmentId>()
.map_err(|_| ParseError::InvalidSession(raw.clone()))?,
);
i += 2;
}
"--pod" => {
let raw = args.get(i + 1).ok_or(ParseError::MissingValue("--pod"))?;
pod = Some(raw.clone());
i += 2;
}
"--profile" => {
let raw = args
.get(i + 1)
.ok_or(ParseError::MissingValue("--profile"))?;
profile = Some(raw.clone());
i += 2;
}
"--socket" => {
socket_seen = true;
let raw = args
.get(i + 1)
.ok_or(ParseError::MissingValue("--socket"))?;
socket_override = Some(PathBuf::from(raw));
i += 2;
}
other if positional.is_none() && !other.starts_with('-') => {
positional = Some(other.to_string());
i += 1;
}
_ => {
// Unknown flag or extra positional — keep older
// behaviour of ignoring unknowns rather than aborting.
i += 1;
}
}
}
if multi {
if resume {
return Err(ParseError::Conflict(
"--multi and --resume are mutually exclusive",
));
}
if session.is_some() {
return Err(ParseError::Conflict(
"--multi and --session are mutually exclusive",
));
}
if pod.is_some() {
return Err(ParseError::Conflict(
"--multi and --pod are mutually exclusive",
));
}
if positional.is_some() {
return Err(ParseError::Conflict(
"--multi cannot be used with a positional Pod name",
));
}
if socket_seen {
return Err(ParseError::Conflict(
"--multi and --socket are mutually exclusive",
));
}
if profile.is_some() {
return Err(ParseError::Conflict(
"--multi and --profile are mutually exclusive",
));
}
return Ok(Mode::Multi);
}
if resume && session.is_some() {
return Err(ParseError::Conflict(
"--resume and --session are mutually exclusive",
));
}
if pod.is_some() && session.is_some() {
return Err(ParseError::Conflict(
"--pod and --session are mutually exclusive",
));
}
if pod.is_some() && resume {
return Err(ParseError::Conflict(
"--pod and --resume are mutually exclusive",
));
}
if profile.is_some()
&& (resume || session.is_some() || pod.is_some() || positional.is_some() || socket_seen)
{
return Err(ParseError::Conflict(
"--profile can only be used for fresh spawn",
));
}
if let Some(pod_name) = pod {
return Ok(Mode::PodName {
pod_name,
socket_override,
});
}
if let Some(id) = session {
return Ok(Mode::ResumeWithSession(id));
}
if resume {
return Ok(Mode::Resume);
}
if let Some(pod_name) = positional {
return Ok(Mode::PodName {
pod_name,
socket_override,
});
}
Ok(Mode::Spawn { profile })
}
#[tokio::main]
async fn main() -> ExitCode {
let mode = match parse_args() {
Ok(m) => m,
Err(e) => {
eprintln!("insomnia: {e}");
return match e {
ParseError::MemoryLint(_) => ExitCode::from(2),
_ => ExitCode::FAILURE,
};
}
};
if let Mode::MemoryLint(ref options) = mode {
return match memory_lint::run(options) {
Ok(memory_lint::LintStatus::Clean) => ExitCode::SUCCESS,
Ok(memory_lint::LintStatus::Failed) => ExitCode::FAILURE,
Err(err) => {
eprintln!("insomnia: {err}");
ExitCode::from(2)
}
};
}
if let Mode::PodRuntime(args) = mode {
return pod::entrypoint::run_cli_from("insomnia pod", args).await;
}
if let Err(e) = enable_raw_mode() {
eprintln!("insomnia: failed to enter raw mode: {e}");
return ExitCode::FAILURE;
}
if let Err(e) = execute!(io::stdout(), EnableBracketedPaste) {
let _ = disable_raw_mode();
eprintln!("insomnia: {e}");
return ExitCode::FAILURE;
}
let result = match mode {
Mode::Spawn { profile } => run_spawn(None, profile).await,
Mode::PodName {
pod_name,
socket_override,
} => run_pod_name(pod_name, socket_override).await,
Mode::Resume => run_resume().await,
Mode::ResumeWithSession(id) => run_spawn(Some(id), None).await,
Mode::Multi => run_multi().await,
Mode::MemoryLint(_) => unreachable!("memory lint returns before terminal setup"),
Mode::PodRuntime(_) => unreachable!("pod runtime returns before terminal setup"),
};
// Always restore the terminal first so any pending eprintln below
// shows up cleanly in scrollback rather than inside an active
// alternate-screen buffer.
let mut stdout = io::stdout();
let _ = execute!(
stdout,
DisableMouseCapture,
LeaveAlternateScreen,
DisableBracketedPaste
);
let _ = disable_raw_mode();
let _ = execute!(stdout, crossterm::cursor::Show);
match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
// SpawnError has already been painted into the inline
// viewport's final frame, so it's already visible in the
// user's scrollback — printing it again would be a noisy
// duplicate. Other errors (pod-name failures, terminal setup
// hiccups, etc.) need surfacing here.
if e.downcast_ref::<spawn::SpawnError>().is_none() {
eprintln!("insomnia: {e}");
}
ExitCode::FAILURE
}
}
}
async fn run_pod_name(
pub(crate) async fn run_pod_name(
pod_name: String,
socket_override: Option<PathBuf>,
runtime_command: PodRuntimeCommand,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(client) = try_connect_live_pod(&pod_name, socket_override.clone()).await {
let mut terminal = enter_fullscreen()?;
@ -356,7 +52,7 @@ async fn run_pod_name(
return Ok(());
}
let ready = match spawn::run_pod_name(pod_name).await? {
let ready = match spawn::run_pod_name(pod_name, runtime_command).await? {
SpawnOutcome::Ready(r) => r,
SpawnOutcome::Cancelled => return Ok(()),
};
@ -380,6 +76,7 @@ async fn run_connected_pod(
async fn run_pod_name_nested(
terminal: &mut FullscreenTerminal,
request: multi_pod::OpenPodRequest,
runtime_command: PodRuntimeCommand,
) -> Result<(), Box<dyn std::error::Error>> {
let multi_pod::OpenPodRequest {
pod_name,
@ -390,16 +87,17 @@ async fn run_pod_name_nested(
return run_connected_pod(terminal, pod_name, client).await;
}
let ready = spawn_pod_name_from_fullscreen(terminal, &pod_name).await?;
let ready = spawn_pod_name_from_fullscreen(terminal, &pod_name, runtime_command).await?;
run_ready_pod(terminal, ready).await
}
async fn spawn_pod_name_from_fullscreen(
terminal: &mut FullscreenTerminal,
pod_name: &str,
runtime_command: PodRuntimeCommand,
) -> Result<SpawnReady, Box<dyn std::error::Error>> {
leave_fullscreen(terminal)?;
let outcome = spawn::run_pod_name(pod_name.to_string()).await;
let outcome = spawn::run_pod_name(pod_name.to_string(), runtime_command).await;
enter_fullscreen_existing(terminal)?;
terminal.clear()?;
@ -463,7 +161,9 @@ async fn connect_live_pod(
.map(|client| (registry_socket, client))
}
async fn run_resume() -> Result<(), Box<dyn std::error::Error>> {
pub(crate) async fn run_resume(
runtime_command: PodRuntimeCommand,
) -> Result<(), Box<dyn std::error::Error>> {
// Pick a Pod in its own inline viewport, dropping the viewport before
// attaching/restoring so each phase gets fresh vertical room.
let (pod_name, socket_override) = match picker::run().await? {
@ -473,10 +173,12 @@ async fn run_resume() -> Result<(), Box<dyn std::error::Error>> {
} => (pod_name, socket_override),
PickerOutcome::Cancelled => return Ok(()),
};
run_pod_name(pod_name, socket_override).await
run_pod_name(pod_name, socket_override, runtime_command).await
}
async fn run_multi() -> Result<(), Box<dyn std::error::Error>> {
pub(crate) async fn run_multi(
runtime_command: PodRuntimeCommand,
) -> Result<(), Box<dyn std::error::Error>> {
let mut app = multi_pod::load_app().await?;
let mut terminal = enter_fullscreen()?;
@ -488,7 +190,7 @@ async fn run_multi() -> Result<(), Box<dyn std::error::Error>> {
}
multi_pod::MultiPodOutcome::Open(request) => {
let pod_name = request.pod_name.clone();
match run_pod_name_nested(&mut terminal, request).await {
match run_pod_name_nested(&mut terminal, request, runtime_command.clone()).await {
Ok(()) => app.finish_open(&pod_name, Ok(())),
Err(error) if is_recoverable_multi_open_error(error.as_ref()) => {
app.finish_open(&pod_name, Err(error.as_ref()));
@ -508,11 +210,12 @@ fn is_recoverable_multi_open_error(error: &(dyn std::error::Error + 'static)) ->
error.is::<spawn::SpawnError>() || error.is::<NestedOpenCancelled>()
}
async fn run_spawn(
pub(crate) async fn run_spawn(
resume_from: Option<SegmentId>,
profile: Option<String>,
runtime_command: PodRuntimeCommand,
) -> Result<(), Box<dyn std::error::Error>> {
let ready = match spawn::run(resume_from, profile).await? {
let ready = match spawn::run(resume_from, profile, runtime_command).await? {
SpawnOutcome::Ready(r) => r,
SpawnOutcome::Cancelled => return Ok(()),
};
@ -1178,231 +881,6 @@ mod tests {
use super::*;
use protocol::{Event, RewindTarget, RewindTargetId, Segment};
#[test]
fn parse_pod_name_mode() {
match parse_args_from(["--pod", "agent", "--socket", "/tmp/agent.sock"]).unwrap() {
Mode::PodName {
pod_name,
socket_override,
} => {
assert_eq!(pod_name, "agent");
assert_eq!(socket_override, Some(PathBuf::from("/tmp/agent.sock")));
}
_ => panic!("expected PodName mode"),
}
}
#[test]
fn parse_positional_name_uses_pod_name_mode() {
match parse_args_from(["agent"]).unwrap() {
Mode::PodName {
pod_name,
socket_override,
} => {
assert_eq!(pod_name, "agent");
assert_eq!(socket_override, None);
}
_ => panic!("expected PodName mode"),
}
}
#[test]
fn parse_memory_alone_remains_positional_pod_name() {
match parse_args_from(["memory"]).unwrap() {
Mode::PodName {
pod_name,
socket_override,
} => {
assert_eq!(pod_name, "memory");
assert_eq!(socket_override, None);
}
_ => panic!("expected PodName mode"),
}
}
#[test]
fn parse_pod_subcommand_uses_runtime_mode() {
match parse_args_from(["pod", "--pod", "agent", "--profile", "default"]).unwrap() {
Mode::PodRuntime(args) => assert_eq!(args, ["--pod", "agent", "--profile", "default"]),
_ => panic!("expected PodRuntime mode"),
}
}
#[test]
fn parse_literal_pod_name_still_available_with_flag() {
match parse_args_from(["--pod", "pod"]).unwrap() {
Mode::PodName {
pod_name,
socket_override,
} => {
assert_eq!(pod_name, "pod");
assert_eq!(socket_override, None);
}
_ => panic!("expected PodName mode"),
}
}
#[test]
fn parse_memory_lint_mode() {
match parse_args_from([
"memory",
"lint",
"--workspace",
"/tmp/ws",
"--json",
"--warnings-as-errors",
])
.unwrap()
{
Mode::MemoryLint(options) => {
assert_eq!(options.workspace, Some(PathBuf::from("/tmp/ws")));
assert!(options.json);
assert!(options.warnings_as_errors);
}
_ => panic!("expected MemoryLint mode"),
}
}
#[test]
fn parse_memory_lint_rejects_usage_errors() {
let err = parse_args_from(["memory", "lint", "--workspace"]).unwrap_err();
assert_eq!(err.to_string(), "--workspace requires a value");
}
#[test]
fn parse_memory_lint_workspace_equals() {
match parse_args_from(["memory", "lint", "--workspace=/tmp/ws"]).unwrap() {
Mode::MemoryLint(options) => {
assert_eq!(options.workspace, Some(PathBuf::from("/tmp/ws")));
assert!(!options.json);
assert!(!options.warnings_as_errors);
}
_ => panic!("expected MemoryLint mode"),
}
}
#[test]
fn memory_lint_with_other_second_word_remains_positional_pod_name() {
match parse_args_from(["memory", "other"]).unwrap() {
Mode::PodName { pod_name, .. } => assert_eq!(pod_name, "memory"),
_ => panic!("expected PodName mode"),
}
}
#[test]
fn parse_rejects_pod_and_session() {
let segment_id = session_store::new_segment_id().to_string();
let err = parse_args_from(["--pod", "agent", "--session", &segment_id]).unwrap_err();
assert_eq!(
err.to_string(),
"--pod and --session are mutually exclusive"
);
}
#[test]
fn parse_profile_spawn_mode() {
match parse_args_from(["--profile", "/profiles/coder.lua"]).unwrap() {
Mode::Spawn { profile } => {
assert_eq!(profile, Some("/profiles/coder.lua".to_string()));
}
_ => panic!("expected Spawn mode"),
}
}
#[test]
fn parse_profile_rejects_resume_attach_modes() {
let segment_id = session_store::new_segment_id().to_string();
let cases = [
(
vec![
"--profile".to_string(),
"p.lua".to_string(),
"--resume".to_string(),
],
"--profile can only be used for fresh spawn",
),
(
vec![
"--profile".to_string(),
"p.lua".to_string(),
"--session".to_string(),
segment_id,
],
"--profile can only be used for fresh spawn",
),
(
vec![
"--profile".to_string(),
"p.lua".to_string(),
"--socket".to_string(),
"/tmp/insomnia/sock".to_string(),
],
"--profile can only be used for fresh spawn",
),
(
vec![
"--profile".to_string(),
"p.lua".to_string(),
"agent".to_string(),
],
"--profile can only be used for fresh spawn",
),
];
for (args, message) in cases {
let err = parse_args_from(args).unwrap_err();
assert_eq!(err.to_string(), message);
}
}
#[test]
fn parse_multi_mode() {
match parse_args_from(["--multi"]).unwrap() {
Mode::Multi => {}
_ => panic!("expected Multi mode"),
}
}
#[test]
fn parse_multi_conflicts_are_clear() {
let segment_id = session_store::new_segment_id().to_string();
let cases = [
(
vec!["--multi".to_string(), "--resume".to_string()],
"--multi and --resume are mutually exclusive",
),
(
vec!["--multi".to_string(), "--session".to_string(), segment_id],
"--multi and --session are mutually exclusive",
),
(
vec![
"--multi".to_string(),
"--pod".to_string(),
"agent".to_string(),
],
"--multi and --pod are mutually exclusive",
),
(
vec!["--multi".to_string(), "agent".to_string()],
"--multi cannot be used with a positional Pod name",
),
(
vec![
"--multi".to_string(),
"--socket".to_string(),
"/tmp/a.sock".to_string(),
],
"--multi and --socket are mutually exclusive",
),
];
for (args, message) in cases {
let err = parse_args_from(args).unwrap_err();
assert_eq!(err.to_string(), message);
}
}
#[tokio::test]
async fn terminal_event_is_selected_before_ready_pod_event() {
let (tx, mut rx) = mpsc::unbounded_channel();

View File

@ -15,7 +15,7 @@ use std::io;
use std::path::{Path, PathBuf};
use std::time::Duration;
use client::{SpawnConfig, spawn_pod};
use client::{PodRuntimeCommand, SpawnConfig, spawn_pod};
use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers};
use manifest::ProfileDiscovery;
use ratatui::Terminal;
@ -76,6 +76,7 @@ type InlineTerminal = Terminal<CrosstermBackend<io::Stdout>>;
pub async fn run(
resume_from: Option<SegmentId>,
profile: Option<String>,
runtime_command: PodRuntimeCommand,
) -> Result<SpawnOutcome, SpawnError> {
let defaults = load_spawn_defaults()?;
let mut profile_choices = if resume_from.is_some() {
@ -143,7 +144,7 @@ pub async fn run(
form.message = Some(("starting pod...".to_string(), MessageKind::Progress));
terminal.draw(|f| draw_form(f, &form))?;
match wait_for_ready(&mut terminal, &mut form).await {
match wait_for_ready(&mut terminal, &mut form, &runtime_command).await {
Ok(ready) => {
form.message = Some((
format!("ready: {} attaching...", ready.pod_name),
@ -165,13 +166,16 @@ pub async fn run(
/// 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> {
pub async fn run_pod_name(
pod_name: String,
runtime_command: PodRuntimeCommand,
) -> Result<SpawnOutcome, SpawnError> {
let defaults = load_spawn_defaults()?;
let mut form = form_for_pod_name(pod_name, defaults);
let mut terminal = make_inline_terminal()?;
terminal.draw(|f| draw_form(f, &form))?;
match wait_for_ready(&mut terminal, &mut form).await {
match wait_for_ready(&mut terminal, &mut form, &runtime_command).await {
Ok(ready) => {
form.message = Some((
format!("ready: {} attaching...", ready.pod_name),
@ -360,8 +364,10 @@ fn sanitise_default_name(s: &str) -> String {
async fn wait_for_ready(
terminal: &mut InlineTerminal,
form: &mut Form,
runtime_command: &PodRuntimeCommand,
) -> Result<SpawnReady, SpawnError> {
let config = SpawnConfig {
runtime_command: runtime_command.clone(),
pod_name: form.name.clone(),
profile: form.selected_profile_selector(),
cwd: form.cwd.clone(),
@ -687,7 +693,10 @@ description = "Project coder"
let (choices, default_index) = profile_choices_for_cwd(&project);
assert_eq!(choices[0].selector.as_deref(), Some("builtin:default"));
assert_eq!(choices[0].label, "builtin:default");
assert_eq!(
choices[0].label,
"builtin:default — Bundled default Insomnia coding profile"
);
assert_eq!(default_index, 1);
assert_eq!(choices[1].selector.as_deref(), Some("project:coder"));
assert_eq!(choices[1].label, "project:coder (default) — Project coder");

View File

@ -12,7 +12,7 @@ use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use crate::block::{Block, ToolCallBlock, ToolCallState};
use crate::cache::FileCache;
use crate::ui::Mode;
use crate::view_mode::Mode;
/// Maximum body lines in normal mode for tool output previews.
const NORMAL_MAX_BODY: usize = 5;

View File

@ -31,36 +31,7 @@ use crate::app::{ActionbarNoticeLevel, App, CompletionState, alert_source_label,
use crate::block::{Block, CompactEvent, ThinkingBlock, ThinkingState};
use crate::command::CommandCandidate;
use crate::task::{TaskCounts, TaskEntry, TaskStatus, TaskStore};
/// Display density for the history view.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
/// Every block fully expanded.
Detail,
/// Completed blocks compressed to roughly 56 lines; in-progress
/// tool blocks stay in detail.
Normal,
/// Each block rendered as a single line.
Overview,
}
impl Mode {
pub fn cycle(self) -> Self {
match self {
Mode::Detail => Mode::Normal,
Mode::Normal => Mode::Overview,
Mode::Overview => Mode::Detail,
}
}
pub fn label(self) -> &'static str {
match self {
Mode::Detail => "detail",
Mode::Normal => "normal",
Mode::Overview => "overview",
}
}
}
use crate::view_mode::Mode;
pub fn draw(frame: &mut Frame, app: &mut App) {
let area = frame.area();

View File

@ -0,0 +1,29 @@
/// Display density for the history view.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
/// Every block fully expanded.
Detail,
/// Completed blocks compressed to roughly 56 lines; in-progress
/// tool blocks stay in detail.
Normal,
/// Each block rendered as a single line.
Overview,
}
impl Mode {
pub fn cycle(self) -> Self {
match self {
Mode::Detail => Mode::Normal,
Mode::Normal => Mode::Overview,
Mode::Overview => Mode::Detail,
}
}
pub fn label(self) -> &'static str {
match self {
Mode::Detail => "detail",
Mode::Normal => "normal",
Mode::Overview => "overview",
}
}
}

View File

@ -2,7 +2,7 @@
INSOMNIA では、プロセス境界で本当に必要な場合を除き、環境変数の利用を避ける。新しい ambient な入力を増やすより、明示的な profile / manifest / config file / typed secret reference / CLI argument を優先する。
それでも、path discovery、runtime directory、package resource lookup、外部 provider の credential 慣習との移行互換のために、一部の環境変数はまだサポートしている。この文書に載せた環境変数は公開 surface として扱う。ただし、fallback 変数は独立した設定項目ではなく、対応する main key の解決順の一部として扱う。開発・テスト都合だけの環境変数は原則として追加せず、既存のものも削除する。
それでも、path discovery、runtime directory、外部 provider の credential 慣習との移行互換のために、一部の環境変数はまだサポートしている。この文書に載せた通常 runtime 用の環境変数は公開 surface として扱う。ただし、fallback 変数は独立した設定項目ではなく、対応する main key の解決順の一部として扱う。開発・テスト都合だけの環境変数は、通常ユーザー向け configuration として扱わない明確な escape hatch に限る。
## 原則
@ -23,7 +23,6 @@ Path 系の環境変数は論理的な key ごとに立項する。`XDG_*` や `
| `config_dir` | `INSOMNIA_CONFIG_DIR` | `$INSOMNIA_HOME/config``$XDG_CONFIG_HOME/insomnia``$HOME/.config/insomnia` | 人が書く設定・override の置き場。`profiles.toml`、prompt override、model/provider override など。 |
| `data_dir` | `INSOMNIA_DATA_DIR` | `$INSOMNIA_HOME``$HOME/.insomnia` | プログラムが書く永続データの置き場。session log、Pod metadata など、再起動後も restore / replay の根拠になるもの。通常ユーザー向けの primary knob ではなく、migration、test、isolated data store 用の advanced override。 |
| `runtime_dir` | `INSOMNIA_RUNTIME_DIR` | `$INSOMNIA_HOME/run``$XDG_RUNTIME_DIR/insomnia``$HOME/.insomnia/run` | socket、pid/status file、live registry mirror など、再起動で捨ててよい runtime state の置き場。 |
| `resource_dir` | `INSOMNIA_RESOURCE_DIR` | installed executable から見た `share/insomnia/resources` → build tree の `resources/` | bundled prompts、builtin profiles、provider/model catalog など、package が所有する immutable-ish な builtin asset の置き場。通常 user configuration ではなく、packaging / development / debug 用の override。 |
空の path 環境変数は、`manifest::paths` では原則として unset 相当に扱う。
@ -35,13 +34,9 @@ Path 系の環境変数は論理的な key ごとに立項する。`XDG_*` や `
このため、socket や pid file を `data_dir` に置かない。永続データと揮発 runtime state は分ける。
### `resource_dir``config_dir` の違い
### Builtin assets と `config_dir`
`resource_dir` は package-owned builtin asset の場所である。installed package では `share/insomnia/resources` に置かれ、binary version と対応する。ユーザーが普段編集する場所ではない。
`config_dir` は user/project-owned override の場所である。`profiles.toml` や prompt/model/provider override はここに置き、package update で上書きされない。
つまり、builtin fallback は `resource_dir`、user override は `config_dir` で扱う。`INSOMNIA_RESOURCE_DIR` を user configuration の代わりに使わない。
Builtin profiles and catalogs are embedded in the binary at build time. User/project-owned overrides remain under `config_dir` and project `.insomnia/` files such as `profiles.toml`; package runtime resource lookup is not a supported configuration surface.
## Credential と外部 auth
@ -61,9 +56,17 @@ Provider credential は、現在は manifest / profile / catalog の設定から
Credential env var は interoperability のために現時点では残っているが、長期的に望ましい secret mechanism ではない。現時点では適切なら `auth.file` を優先し、今後は typed secret reference へ寄せる。credential UX のために implicit `.env` loading を追加しないこと。project secret を漏らしやすく、profile ごとの credential model とも相性が悪い。
## Build / example / test 変数
## Development-only escape hatches
これらは通常の application configuration ではない。test-only の user-facing env var は supported surface として立てず、既存の `INSOMNIA_TEST_*` も削除する。test が public env behavior を検証する必要がある場合だけ、shared guard / test-support crate で process environment mutation を閉じ込める。
これらは dogfooding / self-rebuild / fixture などの開発運用だけの逃げ道であり、通常ユーザー向けの configuration surface ではない。profile、manifest、CLI option の代替として案内しない。
| 変数 | Context | 備考 |
| --- | --- | --- |
| `INSOMNIA_POD_RUNTIME_COMMAND` | 開発中に起動中の `insomnia` binary が rebuild され、`std::env::current_exe()` が `target/debug/insomnia (deleted)` のような stale path を返す場合の Pod runtime executable override。 | Unset または empty の場合は既定どおり current executable に `pod` prefix argument を付けて起動する。Non-empty の場合は値を executable path としてそのまま使い、`pod` prefix argument は常に自動追加する。shell parsing や argument splitting は行わないため、値に flags や `pod` を含めない。 |
## Build / example variables
これらは通常の application configuration ではない。
| 変数 | Context | 備考 |
| --- | --- | --- |
@ -73,23 +76,14 @@ Credential env var は interoperability のために現時点では残ってい
| `RUST_LOG` | example / dev diagnostics。 | example CLI が tracing setup 経由で読む場合がある。 |
| `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY` などの provider example vars | `llm-worker` や Pod example / fixture recorder。 | example code が `dotenv::dotenv().ok()` を呼ぶことがある。通常の `insomnia` runtime startup には適用されない。 |
## deprecated または意図的に存在しない surface
- `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 しない。
## 整理方針
環境変数に関わるコードを触る場合は、以下を優先する。
1. test-only env var を削除し、public env behavior を検証する test だけを shared guard / test-support crate に集約する。
1. fallback / precedence の test は、process environment を読ませず、直接入力を渡せる小さな pure helper で検証する。
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 に一本化する。
3. test が process environment を変更するのは、process env から読む thin wrapper 自体を検証する場合や、subprocess isolation に必要な場合に限る。
4. credential source は resolved config 上で明示し、process-env convention を増やすより typed secret reference へ寄せる。encrypted secret store 導入時に credential env var 依存を削除する。
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 として設計する。

View File

@ -1,76 +0,0 @@
# Nix package
INSOMNIA provides a flake package for installing the user-facing `insomnia` command without relying on a source checkout at runtime. The Pod runtime still runs as a separate process through `insomnia pod ...`; the installed package does not expose a separate `insomnia-pod` command.
## Build
From the repository root:
```sh
nix build .#
```
The default package is implemented by `package.nix` and builds the Cargo package `tui` as the installed binary `insomnia`. The `pod` crate remains a library dependency that provides the Pod runtime entrypoint used by `insomnia pod ...`. The derivation uses the checked-in `Cargo.lock`, so Cargo dependencies are fetched by the normal Nix Rust packaging path instead of by network access during the build.
The package output contains:
- `bin/insomnia` — terminal UI and `insomnia pod ...` runtime entrypoint.
- `share/insomnia/resources/` — bundled runtime resources, including `resources/prompts/`.
- `share/doc/insomnia/nix.md` — this document.
- `share/doc/insomnia/environment.md` — environment-variable policy and supported variables.
## Run
After `nix build`:
```sh
./result/bin/insomnia pod --help
./result/bin/insomnia
```
With flakes:
```sh
nix run .#insomnia
nix run .#insomnia -- pod --help
```
`nix run .#` defaults to the TUI.
## Configuration discovery
The Nix package does not put user configuration, sessions, sockets, or other mutable state in the Nix store. The installed binary keeps the same path semantics as non-Nix builds:
| Purpose | Override | `INSOMNIA_HOME` fallback | XDG / default fallback |
| --- | --- | --- | --- |
| User config (`profiles.toml`, prompt overrides, model/provider overrides) | `INSOMNIA_CONFIG_DIR` | `$INSOMNIA_HOME/config` | `$XDG_CONFIG_HOME/insomnia`, then `$HOME/.config/insomnia` |
| Persistent data (`sessions/`, Pod metadata) | `INSOMNIA_DATA_DIR` | `$INSOMNIA_HOME` | `$HOME/.insomnia` |
| Runtime state (sockets, lock files, live registry) | `INSOMNIA_RUNTIME_DIR` | `$INSOMNIA_HOME/run` | `$XDG_RUNTIME_DIR/insomnia`, then `$HOME/.insomnia/run` |
Normal fresh startup is profile-based. The package ships a builtin default profile, user/project `profiles.toml` files may select or define profiles, and `insomnia pod --manifest <PATH>` remains a one-file compatibility/debug input. `INSOMNIA_USER_MANIFEST` and ambient `.insomnia/manifest.toml` discovery are not part of normal Pod/TUI startup. See [`environment.md`](environment.md) for the environment-variable policy; new configuration should prefer profiles/manifests/config files over additional environment variables.
## Validation
The package derivation has a credential-free install check that verifies:
- `insomnia pod --help` starts successfully.
- `insomnia` is installed and reaches argument parsing.
- `bin/insomnia-pod` is not installed.
- bundled prompt resources and this Nix usage document are present in the output.
For full validation before handing changes to review, run:
```sh
nix build .#
nix flake check
cargo fmt --check
cargo check -p tui -p pod -p client
```
These checks do not require provider credentials.
## Known limitations
- The package currently installs only the `insomnia` command; development-only wrappers from `devshell.nix` are not part of the installable package.
- The TUI does not currently expose a conventional `--help` / `--version` CLI path, so the package smoke check uses an argument-parse failure path for the TUI rather than launching an interactive session.
- Bundled resources are installed under `share/insomnia/resources/` for packaging completeness and inspection. Built-in prompt/resource loading remains governed by the existing application code and user/project override rules.

View File

@ -119,12 +119,12 @@ Use `--manifest` only when you need the complete low-level Manifest escape hatch
## Builtin defaults
Base defaults that are independent of profile choice live in Rust constants under `crates/manifest/src/defaults.rs` and in `PodManifestConfig::builtin_defaults()`. The bundled default role profile lives at `resources/profiles/default.lua` and is discovered as `builtin:default`.
Base defaults that are independent of profile choice live in Rust constants under `crates/manifest/src/defaults.rs` and in `PodManifestConfig::builtin_defaults()`. The default role profile is embedded from `resources/profiles/default.lua` at build time and is discovered as `builtin:default`.
デフォルト値を変更するときは、次のどちらを変更するのかを明確にする。
- all manifests/profiles の baseline default: Rust defaults
- ordinary dogfooding/default role: `resources/profiles/default.lua`
- ordinary dogfooding/default role: embedded `builtin:default` profile sourced from `resources/profiles/default.lua`
## Path resolution

View File

@ -40,7 +40,7 @@ rustPlatform.buildRustPackage rec {
filter = sourceFilter;
};
cargoHash = "sha256-fisV77ZqAPsI0eLZIqw06HTj1CfmnL3NBHhjruZPZUE=";
cargoHash = "sha256-tYql+E6s7WeZ2vQSfjH0BzmXmNQaqm54VQy8mUnpzLg=";
depsExtraArgs = {
# nixpkgs 25.11's fetchCargoVendor still uses crates.io's API
@ -83,7 +83,7 @@ rustPlatform.buildRustPackage rec {
cargoBuildFlags = [
"-p"
"tui"
"insomnia"
];
# The package check is a credential-free install smoke check below. Running the
@ -92,10 +92,7 @@ rustPlatform.buildRustPackage rec {
doCheck = false;
postInstall = ''
install -Dm644 docs/nix.md "$out/share/doc/insomnia/nix.md"
install -Dm644 docs/environment.md "$out/share/doc/insomnia/environment.md"
mkdir -p "$out/share/insomnia"
cp -R resources "$out/share/insomnia/resources"
'';
doInstallCheck = true;
@ -105,14 +102,13 @@ rustPlatform.buildRustPackage rec {
"$out/bin/insomnia" pod --help >/dev/null
test -x "$out/bin/insomnia"
test ! -e "$out/bin/insomnia-pod"
test ! -e "$out/share/insomnia/resources"
if "$out/bin/insomnia" --session not-a-uuid 2>insomnia.err; then
echo "insomnia unexpectedly accepted an invalid --session value" >&2
exit 1
fi
grep -q "invalid --session UUID" insomnia.err
test -d "$out/share/insomnia/resources/prompts"
test -f "$out/share/doc/insomnia/nix.md"
test -f "$out/share/doc/insomnia/environment.md"
runHook postInstallCheck

View File

@ -2,12 +2,12 @@
id: 20260531-005557-single-binary-insomnia-cli
slug: single-binary-insomnia-cli
title: CLI: migrate toward a single insomnia binary
status: open
status: closed
kind: task
priority: P2
labels: [cli, architecture, nix]
created_at: 2026-05-31T00:55:57Z
updated_at: 2026-05-31T04:32:30Z
updated_at: 2026-05-31T12:15:50Z
assignee: null
legacy_ticket: null
---

View File

@ -0,0 +1,27 @@
Completed the umbrella migration from the previous two-command installed layout toward a single primary `insomnia` executable.
Completed phases:
- `insomnia-pod-subcommand-runtime`: moved Pod runtime startup behind `pod::entrypoint` and added `insomnia pod ...` dispatch.
- `spawn-through-insomnia-pod-subcommand`: changed internal spawn/restore defaults to typed runtime command resolution using current executable plus the `pod` prefix argument.
- `remove-insomnia-pod-binary`: removed the long-term `insomnia-pod` binary/package/devshell/flake output.
- Follow-up cleanup removed `INSOMNIA_POD_COMMAND` and `INSOMNIA_RESOURCE_DIR`, keeping the runtime command/config surface narrower.
Outcome:
- The installed package exposes `bin/insomnia` only.
- `insomnia pod ...` is the Pod runtime entrypoint; Pods remain separate processes.
- Internal spawn/restore uses typed command construction rather than shell string parsing.
- `insomnia-pod` is not kept as a compatibility alias.
- Headless `insomnia memory lint` behavior remains part of the `insomnia` CLI surface.
Validation/evidence across completed phases included:
- `insomnia pod --help`
- focused parser/spawn/restore tests
- `cargo fmt --check`
- `cargo check -p tui -p pod -p client`
- `nix build .#insomnia`
- checks that `bin/insomnia-pod` is absent
- `./tickets.sh doctor`
- `git diff --check`
Follow-up intentionally remains separate:
- `insomnia-crate-cli-owner` now tracks the architectural cleanup where the `insomnia` crate owns the product CLI/binary entrypoint and `tui` becomes a library implementation crate. That is not required for this umbrella's original single-installed-binary migration to be complete.

View File

@ -36,4 +36,39 @@ Revised decision from user discussion:
Initial implementation should start by extracting the Pod runtime into a library entrypoint and adding `insomnia pod ...`; subsequent steps can migrate spawn defaults and remove `insomnia-pod` from packaging.
---
<!-- event: close author: hare at: 2026-05-31T12:15:50Z status: closed -->
## Closed
Completed the umbrella migration from the previous two-command installed layout toward a single primary `insomnia` executable.
Completed phases:
- `insomnia-pod-subcommand-runtime`: moved Pod runtime startup behind `pod::entrypoint` and added `insomnia pod ...` dispatch.
- `spawn-through-insomnia-pod-subcommand`: changed internal spawn/restore defaults to typed runtime command resolution using current executable plus the `pod` prefix argument.
- `remove-insomnia-pod-binary`: removed the long-term `insomnia-pod` binary/package/devshell/flake output.
- Follow-up cleanup removed `INSOMNIA_POD_COMMAND` and `INSOMNIA_RESOURCE_DIR`, keeping the runtime command/config surface narrower.
Outcome:
- The installed package exposes `bin/insomnia` only.
- `insomnia pod ...` is the Pod runtime entrypoint; Pods remain separate processes.
- Internal spawn/restore uses typed command construction rather than shell string parsing.
- `insomnia-pod` is not kept as a compatibility alias.
- Headless `insomnia memory lint` behavior remains part of the `insomnia` CLI surface.
Validation/evidence across completed phases included:
- `insomnia pod --help`
- focused parser/spawn/restore tests
- `cargo fmt --check`
- `cargo check -p tui -p pod -p client`
- `nix build .#insomnia`
- checks that `bin/insomnia-pod` is absent
- `./tickets.sh doctor`
- `git diff --check`
Follow-up intentionally remains separate:
- `insomnia-crate-cli-owner` now tracks the architectural cleanup where the `insomnia` crate owns the product CLI/binary entrypoint and `tui` becomes a library implementation crate. That is not required for this umbrella's original single-installed-binary migration to be complete.
---

View File

@ -2,12 +2,12 @@
id: 20260531-074258-tui-extract-cli-parsing
slug: tui-extract-cli-parsing
title: TUI: extract CLI parsing from main.rs
status: open
status: closed
kind: task
priority: P2
labels: [tui, cleanup]
created_at: 2026-05-31T07:42:58Z
updated_at: 2026-05-31T07:42:58Z
updated_at: 2026-05-31T13:38:30Z
assignee: null
legacy_ticket: null
---

View File

@ -0,0 +1,19 @@
This ticket is completed/superseded by `insomnia-crate-cli-owner`.
The original goal was to remove CLI parsing from `crates/tui/src/main.rs` so the TUI crate would stop owning top-level product CLI parsing. The later `insomnia-crate-cli-owner` implementation went further:
- the product binary moved from package `tui` to package `insomnia`;
- top-level CLI parsing and dispatch now live in `crates/insomnia/src/main.rs`;
- `tui` is now a library implementation crate with no product `main.rs`;
- `insomnia pod ...` and `insomnia memory lint ...` are routed by the `insomnia` crate;
- normal TUI launch is delegated to `tui::launch(...)`.
Because the TUI crate no longer owns the product entrypoint or CLI parser, there is no remaining `tui/src/main.rs` CLI parsing extraction to implement under this ticket. Further CLI parser cleanup, if desired, belongs to the `insomnia` crate, not this TUI cleanup ticket.
Validation evidence from `insomnia-crate-cli-owner` closure:
- `cargo test -p insomnia`
- CLI smoke for `--help`, `pod --help`, `memory lint --help`, invalid `--session`, and resume/Pod selection conflicts
- `cargo check -p client -p pod -p tui -p insomnia`
- `nix build .#insomnia`
- `./tickets.sh doctor`
- `git diff --check`

View File

@ -0,0 +1,34 @@
<!-- event: create author: tickets.sh at: 2026-05-31T07:42:58Z -->
## Created
Created by tickets.sh create.
---
<!-- event: close author: hare at: 2026-05-31T13:38:30Z status: closed -->
## Closed
This ticket is completed/superseded by `insomnia-crate-cli-owner`.
The original goal was to remove CLI parsing from `crates/tui/src/main.rs` so the TUI crate would stop owning top-level product CLI parsing. The later `insomnia-crate-cli-owner` implementation went further:
- the product binary moved from package `tui` to package `insomnia`;
- top-level CLI parsing and dispatch now live in `crates/insomnia/src/main.rs`;
- `tui` is now a library implementation crate with no product `main.rs`;
- `insomnia pod ...` and `insomnia memory lint ...` are routed by the `insomnia` crate;
- normal TUI launch is delegated to `tui::launch(...)`.
Because the TUI crate no longer owns the product entrypoint or CLI parser, there is no remaining `tui/src/main.rs` CLI parsing extraction to implement under this ticket. Further CLI parser cleanup, if desired, belongs to the `insomnia` crate, not this TUI cleanup ticket.
Validation evidence from `insomnia-crate-cli-owner` closure:
- `cargo test -p insomnia`
- CLI smoke for `--help`, `pod --help`, `memory lint --help`, invalid `--session`, and resume/Pod selection conflicts
- `cargo check -p client -p pod -p tui -p insomnia`
- `nix build .#insomnia`
- `./tickets.sh doctor`
- `git diff --check`
---

View File

@ -2,12 +2,12 @@
id: 20260531-074258-tui-extract-single-pod-runtime
slug: tui-extract-single-pod-runtime
title: TUI: extract single-Pod runtime loop from main.rs
status: open
status: closed
kind: task
priority: P2
labels: [tui, cleanup]
created_at: 2026-05-31T07:42:58Z
updated_at: 2026-05-31T07:42:58Z
updated_at: 2026-05-31T13:57:02Z
assignee: null
legacy_ticket: null
---

View File

@ -0,0 +1,20 @@
Extracted the current single-Pod TUI runtime/event-loop implementation out of `crates/tui/src/lib.rs`.
Implementation:
- Added `crates/tui/src/single_pod.rs`.
- Moved single-Pod attach/spawn/resume orchestration, fullscreen terminal helpers, event loop/drain logic, key/mouse handling, compact/rewind/queue/interrupt handling, and related tests into `single_pod.rs`.
- Left `lib.rs` as the TUI library façade/high-level dispatcher with module declarations, `LaunchOptions`, `LaunchMode`, and `launch(...)`.
- Preserved public `tui::launch`, `LaunchOptions`, and `LaunchMode` behavior for the `insomnia` crate.
- Did not split `multi_pod.rs`, move render helpers, redesign keybindings, rewrite App state, or change Pod protocol/profile/manifest semantics.
Review:
- External reviewer `tui-runtime-reviewer-20260531` approved implementation commit `4d89718`.
- Reviewer noted one non-blocking follow-up: the thin `run_multi` bridge now lives in `single_pod.rs`; acceptable for this extraction, but could move to a small launch/runtime coordination module if dashboard orchestration grows.
Validation after merge:
- `cargo fmt --check`
- `cargo test -p tui`
- `cargo check -p tui -p insomnia` (passed with existing dead-code warnings)
- `./tickets.sh doctor`
- `git diff --check`
- `wc -l crates/tui/src/lib.rs crates/tui/src/single_pod.rs` showed `lib.rs` at 117 lines and `single_pod.rs` at 1765 lines.

View File

@ -0,0 +1,119 @@
<!-- event: create author: tickets.sh at: 2026-05-31T07:42:58Z -->
## Created
Created by tickets.sh create.
---
<!-- event: plan author: hare at: 2026-05-31T13:46:30Z -->
## Plan
Preflight classification: implementation-ready with updated current-code scope.
The original ticket was written before `insomnia-crate-cli-owner`, when `crates/tui/src/main.rs` owned both product CLI and single-Pod runtime loop. That product CLI entrypoint has since moved to the `insomnia` crate and `tui` is now library-only. The remaining cleanup target is therefore the same runtime/event-loop code now living in `crates/tui/src/lib.rs`, not an obsolete `main.rs` file.
Updated intent:
- Move the single-Pod TUI runtime/event-loop implementation out of `crates/tui/src/lib.rs` into a focused TUI module, likely `single_pod.rs`.
- Keep `lib.rs` as the library façade: module declarations, launch API/types, and high-level dispatch.
Requirements:
- Extract single-Pod runtime functions from `lib.rs` into a focused module while preserving behavior.
- Include related helpers only where mechanical and clear: event loop, key/mouse handling, stream/event drain helpers, and per-run connection orchestration.
- Keep terminal setup/restore behavior, input queueing, interruption, task reminders, manual rewind/compact handling, and Pod socket behavior unchanged.
- Do not move render helpers, split `multi_pod.rs`, redesign keybindings, or perform unrelated TUI module cleanup.
- Keep public `tui::launch`, `LaunchOptions`, and `LaunchMode` behavior stable for the `insomnia` crate.
Current code map:
- `crates/tui/src/lib.rs`: library façade plus large single-Pod runtime loop and helpers after CLI ownership migration.
- `crates/tui/src/multi_pod.rs`: dashboard runtime; out of scope except imports needed by `lib.rs`.
- `crates/tui/src/app.rs`, `input.rs`, `ui.rs`, `tool.rs`, `view_mode.rs`: single-Pod runtime dependencies; should be imported by the extracted module as needed.
Critical risks:
- Over-extracting unrelated launch/multi-Pod code and increasing churn.
- Accidentally changing key/mouse/event handling semantics while moving functions.
- Creating circular module visibility or broad `pub` exposure. Prefer `pub(crate)` only where `lib.rs` must call into `single_pod`.
Intent packet for coder:
Intent:
- Extract the single-Pod runtime loop from current `tui/src/lib.rs` into a focused module and leave `lib.rs` as a thin façade/high-level dispatcher.
Invariants:
- Behavior-preserving move.
- `tui` remains library-only; `insomnia` remains CLI owner.
- No Pod protocol/profile/manifest changes.
- No broad TUI layout refactor beyond this extraction.
Validation:
- `cargo fmt --check`
- focused `cargo test -p tui` tests for queueing/interrupt/compact/rewind/stream handling if available
- full `cargo test -p tui`
- `cargo check -p tui -p insomnia`
- `./tickets.sh doctor`
- `git diff --check`
---
<!-- event: review author: hare at: 2026-05-31T13:56:32Z status: approve -->
## Review: approve
External reviewer: `tui-runtime-reviewer-20260531`
Reviewed implementation commit: `4d89718` (`tui: extract single pod runtime`)
Verdict: approve
Summary:
- This is a behavior-preserving mechanical extraction.
- `crates/tui/src/lib.rs` is now a façade/high-level dispatcher.
- Single-Pod runtime/event-loop code now lives in `crates/tui/src/single_pod.rs`.
- Public `tui::launch`, `LaunchOptions`, and `LaunchMode` signatures remain stable.
Requirements mapping:
- `lib.rs` keeps module declarations, public launch API/types, and high-level mode dispatch.
- `single_pod.rs` owns Pod-name/spawn/resume attach paths, fullscreen terminal helpers, main loop/drain logic, key/mouse handling, and the focused tests that cover those behaviors.
- Terminal setup/restore behavior is preserved: raw mode/bracketed paste global setup remains in `lib.rs`, while fullscreen/alternate-screen helpers moved with single-Pod runtime code.
- Input queueing, interruption, task reminders, compact/rewind handling, and Pod socket behavior are moved with tests and unchanged by inspection.
- No render helper move, `multi_pod.rs` split, App rewrite, keybinding redesign, Pod protocol/profile/manifest change, crate rename, or CLI ownership regression was introduced.
Blockers: none.
Non-blocking follow-up:
- `single_pod.rs` now contains a thin `run_multi` dashboard bridge. This is acceptable here because it bridges dashboard open requests into private single-Pod attach helpers, but if dashboard orchestration grows a future cleanup could move that bridge into a small launch/runtime coordination module.
Validation adequacy:
- Coder validation covered fmt, full TUI tests, TUI/insomnia check, doctor, and diff check.
- Reviewer reran `cargo fmt --check`, `cargo test -p tui`, `cargo check -p tui -p insomnia`, `./tickets.sh doctor`, and `git diff --check HEAD^ HEAD`; all passed with existing dead-code warnings.
---
<!-- event: close author: hare at: 2026-05-31T13:57:02Z status: closed -->
## Closed
Extracted the current single-Pod TUI runtime/event-loop implementation out of `crates/tui/src/lib.rs`.
Implementation:
- Added `crates/tui/src/single_pod.rs`.
- Moved single-Pod attach/spawn/resume orchestration, fullscreen terminal helpers, event loop/drain logic, key/mouse handling, compact/rewind/queue/interrupt handling, and related tests into `single_pod.rs`.
- Left `lib.rs` as the TUI library façade/high-level dispatcher with module declarations, `LaunchOptions`, `LaunchMode`, and `launch(...)`.
- Preserved public `tui::launch`, `LaunchOptions`, and `LaunchMode` behavior for the `insomnia` crate.
- Did not split `multi_pod.rs`, move render helpers, redesign keybindings, rewrite App state, or change Pod protocol/profile/manifest semantics.
Review:
- External reviewer `tui-runtime-reviewer-20260531` approved implementation commit `4d89718`.
- Reviewer noted one non-blocking follow-up: the thin `run_multi` bridge now lives in `single_pod.rs`; acceptable for this extraction, but could move to a small launch/runtime coordination module if dashboard orchestration grows.
Validation after merge:
- `cargo fmt --check`
- `cargo test -p tui`
- `cargo check -p tui -p insomnia` (passed with existing dead-code warnings)
- `./tickets.sh doctor`
- `git diff --check`
- `wc -l crates/tui/src/lib.rs crates/tui/src/single_pod.rs` showed `lib.rs` at 117 lines and `single_pod.rs` at 1765 lines.
---

View File

@ -2,12 +2,12 @@
id: 20260531-074258-tui-move-view-mode-state
slug: tui-move-view-mode-state
title: TUI: move view mode state out of ui module
status: open
status: closed
kind: task
priority: P2
labels: [tui, cleanup]
created_at: 2026-05-31T07:42:58Z
updated_at: 2026-05-31T07:42:58Z
updated_at: 2026-05-31T13:45:39Z
assignee: null
legacy_ticket: null
---

View File

@ -0,0 +1,20 @@
Moved single-Pod view/history mode state out of the render module.
Implementation:
- Added `crates/tui/src/view_mode.rs`.
- Moved `Mode` from `ui.rs` to `view_mode.rs` without changing variants or methods.
- Updated `app.rs`, `ui.rs`, and `tool.rs` imports to use `crate::view_mode::Mode`.
- Removed the `app.rs -> ui.rs` dependency caused solely by `Mode`.
- Kept render/key/command behavior unchanged.
Review:
- External reviewer `tui-view-mode-reviewer-20260531` approved implementation commit `bc31bfa`.
Validation after merge:
- `cargo fmt --check`
- `cargo test -p tui mode`
- `cargo test -p tui`
- `cargo check -p tui` (passed with existing dead-code warnings)
- `./tickets.sh doctor`
- `git diff --check`
- `rg "crate::ui::Mode|ui::Mode" crates/tui/src || true` produced no active source hits.

View File

@ -0,0 +1,66 @@
<!-- event: create author: tickets.sh at: 2026-05-31T07:42:58Z -->
## Created
Created by tickets.sh create.
---
<!-- event: review author: hare at: 2026-05-31T13:45:04Z status: approve -->
## Review: approve
External reviewer: `tui-view-mode-reviewer-20260531`
Reviewed implementation commit: `bc31bfa` (`tui: move view mode state`)
Verdict: approve
Summary:
- Moved the single-Pod history display mode type from `ui.rs` to `view_mode.rs`.
- Kept the type name `Mode` and preserved variants/methods (`Detail`, `Normal`, `Overview`, `cycle()`, `label()`).
- Updated `app.rs`, `ui.rs`, and `tool.rs` to import `crate::view_mode::Mode`.
- Removed the state-model dependency from `app.rs` to the render module for this type.
Requirements mapping:
- `Mode` now lives in a focused state-oriented module.
- `app.rs -> ui.rs` dependency caused by `Mode` is removed.
- Rendering/key/command behavior is unchanged by inspection because the enum and methods are a straight move.
- No broad `App` privatization, render-tree move, app split, crate/package rename, or unrelated runtime extraction was introduced.
- No active `crate::ui::Mode` / `ui::Mode` references remain under `crates/tui/src`.
Blockers: none.
Non-blocking follow-ups: none.
Validation adequacy:
- Coder validation covered fmt, focused/full TUI tests, TUI check, doctor, diff-check, and `ui::Mode` grep.
- Reviewer additionally performed read-only diff/reference checks and found no issues.
---
<!-- event: close author: hare at: 2026-05-31T13:45:39Z status: closed -->
## Closed
Moved single-Pod view/history mode state out of the render module.
Implementation:
- Added `crates/tui/src/view_mode.rs`.
- Moved `Mode` from `ui.rs` to `view_mode.rs` without changing variants or methods.
- Updated `app.rs`, `ui.rs`, and `tool.rs` imports to use `crate::view_mode::Mode`.
- Removed the `app.rs -> ui.rs` dependency caused solely by `Mode`.
- Kept render/key/command behavior unchanged.
Review:
- External reviewer `tui-view-mode-reviewer-20260531` approved implementation commit `bc31bfa`.
Validation after merge:
- `cargo fmt --check`
- `cargo test -p tui mode`
- `cargo test -p tui`
- `cargo check -p tui` (passed with existing dead-code warnings)
- `./tickets.sh doctor`
- `git diff --check`
- `rg "crate::ui::Mode|ui::Mode" crates/tui/src || true` produced no active source hits.
---

View File

@ -2,12 +2,12 @@
id: 20260531-085959-eliminate-test-only-env-vars
slug: eliminate-test-only-env-vars
title: Tests: eliminate test-only environment variables
status: open
status: closed
kind: task
priority: P2
labels: [test, env, cleanup]
created_at: 2026-05-31T08:59:59Z
updated_at: 2026-05-31T08:59:59Z
updated_at: 2026-05-31T10:04:28Z
assignee: null
legacy_ticket: null
---

View File

@ -0,0 +1,18 @@
Removed test-only environment-variable usage from active code.
Implementation:
- Removed `INSOMNIA_TEST_*` Brave WebSearch test key generation/dependency.
- Split Brave search request execution so tests can inject an API key directly into a private helper.
- Preserved production behavior: WebSearch still reads configured `web.search.api_key_env` and fails closed for missing/empty values.
- Updated `docs/environment.md` so test-only env vars are not listed as supported surface.
Review:
- External reviewer `eliminate-test-env-vars-reviewer-20260531` approved implementation commit `e64a5595956c970b090cdce851cc962e92723a97`.
Validation after merge:
- `cargo fmt --check`
- `cargo test -p tools`
- `cargo check -p tools` (passed with unrelated existing `llm-worker` dead_code warning)
- `./tickets.sh doctor`
- `git diff --check`
- `git grep -n "INSOMNIA_TEST" -- ':!work-items' || true` produced no active references.

View File

@ -0,0 +1,64 @@
<!-- event: create author: tickets.sh at: 2026-05-31T08:59:59Z -->
## Created
Created by tickets.sh create.
---
<!-- event: review author: hare at: 2026-05-31T10:03:55Z status: approve -->
## Review: approve
External reviewer: `eliminate-test-env-vars-reviewer-20260531`
Reviewed implementation commit: `e64a5595956c970b090cdce851cc962e92723a97` (`test: remove test-only env vars`)
Verdict: approve
Summary:
- WebSearch tests no longer generate or depend on `INSOMNIA_TEST_*` API-key env names.
- The request/search implementation was split so tests can inject an API key directly into a private helper while production still reads the configured `web.search.api_key_env` and fails closed for missing/empty values.
- `docs/environment.md` no longer presents test-only env vars as a supported surface.
Requirements mapping:
- No active non-work-item `INSOMNIA_TEST` references remain.
- No replacement test-only env var was introduced.
- Credential env vars and `INSOMNIA_POD_COMMAND` were not removed by this ticket.
- WebSearch production behavior and network safety boundaries are preserved.
Blockers: none.
Non-blocking follow-up:
- A future public-path fail-closed test could guard missing/empty `api_key_env`, but this is not required for this ticket.
Validation adequacy:
- Coder validation covered fmt, tools tests/check, ticket doctor, diff check, and residual `INSOMNIA_TEST` grep.
- Reviewer performed read-only diff/source/docs/grep review and did not rerun tests.
---
<!-- event: close author: hare at: 2026-05-31T10:04:28Z status: closed -->
## Closed
Removed test-only environment-variable usage from active code.
Implementation:
- Removed `INSOMNIA_TEST_*` Brave WebSearch test key generation/dependency.
- Split Brave search request execution so tests can inject an API key directly into a private helper.
- Preserved production behavior: WebSearch still reads configured `web.search.api_key_env` and fails closed for missing/empty values.
- Updated `docs/environment.md` so test-only env vars are not listed as supported surface.
Review:
- External reviewer `eliminate-test-env-vars-reviewer-20260531` approved implementation commit `e64a5595956c970b090cdce851cc962e92723a97`.
Validation after merge:
- `cargo fmt --check`
- `cargo test -p tools`
- `cargo check -p tools` (passed with unrelated existing `llm-worker` dead_code warning)
- `./tickets.sh doctor`
- `git diff --check`
- `git grep -n "INSOMNIA_TEST" -- ':!work-items' || true` produced no active references.
---

View File

@ -2,12 +2,12 @@
id: 20260531-085959-remove-insomnia-pod-command-env
slug: remove-insomnia-pod-command-env
title: CLI: remove INSOMNIA_POD_COMMAND override
status: open
status: closed
kind: task
priority: P2
labels: [cli, pod, env]
created_at: 2026-05-31T08:59:59Z
updated_at: 2026-05-31T08:59:59Z
updated_at: 2026-05-31T10:12:03Z
assignee: null
legacy_ticket: null
---

View File

@ -0,0 +1,22 @@
Removed `INSOMNIA_POD_COMMAND` as an active runtime/config/test override.
Implementation:
- `PodRuntimeCommand::resolve()` now always uses the typed default: current executable plus the `pod` prefix argument.
- Removed override/env parsing and related tests from the `insomnia` helper crate.
- Updated SpawnPod tests to use typed `PodRuntimeCommand` injection instead of process-wide env mutation.
- Preserved production spawn/restore behavior, detached process handling, and ready/socket delivery behavior.
- Updated docs so no active supported surface documents the removed env override.
Review:
- External reviewer `remove-insomnia-pod-command-reviewer-20260531` approved implementation commit `c618fa6`.
Validation after merge:
- `cargo fmt --check`
- `cargo test -p insomnia`
- `cargo test -p client -p insomnia`
- `cargo test -p pod --lib discovery::tests`
- `cargo test -p pod --test spawn_pod_test`
- `cargo check -p tui -p pod -p client -p insomnia` (passed with unrelated existing warnings)
- `./tickets.sh doctor`
- `git diff --check`
- `git grep -n "INSOMNIA_POD_COMMAND" -- ':!work-items' || true` produced no active references.

View File

@ -0,0 +1,68 @@
<!-- event: create author: tickets.sh at: 2026-05-31T08:59:59Z -->
## Created
Created by tickets.sh create.
---
<!-- event: review author: hare at: 2026-05-31T10:09:52Z status: approve -->
## Review: approve
External reviewer: `remove-insomnia-pod-command-reviewer-20260531`
Reviewed implementation commit: `c618fa6` (`cli: remove pod command env override`)
Verdict: approve
Summary:
- `INSOMNIA_POD_COMMAND` is removed as an active runtime/config/test override.
- Pod runtime launch remains typed through `PodRuntimeCommand`, defaulting to current executable plus the `pod` prefix argument.
- SpawnPod tests now use typed injection rather than process-wide env mutation.
- Production spawn/restore paths still use `PodRuntimeCommand::resolve()`, preserve detached process handling, and retain the ready/socket delivery behavior.
Requirements mapping:
- No active non-work-item `INSOMNIA_POD_COMMAND` references remain in the implementation branch.
- No replacement env var was introduced.
- No `insomnia-pod` binary/alias, Pod protocol/flag/profile semantic changes, or `tui` rename were introduced.
Blockers: none.
Non-blocking follow-up:
- Merge conflict in `docs/environment.md` is expected after `eliminate-test-only-env-vars`; integration should keep the newer generic test-only wording while preserving removal of explicit `INSOMNIA_POD_COMMAND` references.
Validation adequacy:
- Coder validation covered fmt, helper/client/pod tests, cargo check, doctor, diff check, and active-reference grep.
- Reviewer additionally reran targeted tests/checks in the implementation worktree and confirmed no active refs for `INSOMNIA_POD_COMMAND`, `POD_COMMAND_OVERRIDE_ENV`, `from_override_env`, or `executable_only` outside work items.
---
<!-- event: close author: hare at: 2026-05-31T10:12:03Z status: closed -->
## Closed
Removed `INSOMNIA_POD_COMMAND` as an active runtime/config/test override.
Implementation:
- `PodRuntimeCommand::resolve()` now always uses the typed default: current executable plus the `pod` prefix argument.
- Removed override/env parsing and related tests from the `insomnia` helper crate.
- Updated SpawnPod tests to use typed `PodRuntimeCommand` injection instead of process-wide env mutation.
- Preserved production spawn/restore behavior, detached process handling, and ready/socket delivery behavior.
- Updated docs so no active supported surface documents the removed env override.
Review:
- External reviewer `remove-insomnia-pod-command-reviewer-20260531` approved implementation commit `c618fa6`.
Validation after merge:
- `cargo fmt --check`
- `cargo test -p insomnia`
- `cargo test -p client -p insomnia`
- `cargo test -p pod --lib discovery::tests`
- `cargo test -p pod --test spawn_pod_test`
- `cargo check -p tui -p pod -p client -p insomnia` (passed with unrelated existing warnings)
- `./tickets.sh doctor`
- `git diff --check`
- `git grep -n "INSOMNIA_POD_COMMAND" -- ':!work-items' || true` produced no active references.
---

View File

@ -0,0 +1,57 @@
---
id: 20260531-104614-pure-path-fallback-tests
slug: pure-path-fallback-tests
title: Tests: make path fallback tests independent from process env
status: closed
kind: task
priority: P2
labels: [test, env, manifest, cleanup]
created_at: 2026-05-31T10:46:14Z
updated_at: 2026-05-31T10:54:49Z
assignee: null
legacy_ticket: null
---
## Background
The environment-variable cleanup removed test-only env surfaces such as `INSOMNIA_TEST_*` and the Pod runtime command env override. The next issue is that several tests still mutate process-global environment even when they only need to verify deterministic fallback/precedence logic.
The user clarified the intended direction:
- even env vars used by the real application do not need to be read from process env in most tests;
- tests should verify fallback/precedence behavior with direct inputs;
- do not introduce a `PathEnv`/`EnvSnapshot` abstraction or a shared test-support crate just to preserve process-env mutation;
- write the fallback helpers directly and narrowly where needed.
This ticket starts with path resolution, where the behavior is centralized and the env mutation is avoidable.
## Requirements
- In `manifest::paths`, separate fallback/precedence logic from process env reads enough that fallback tests can call pure helpers with direct `Option<PathBuf>`-style inputs.
- Prefer small per-key helpers over a general `PathEnv` struct/trait.
- Example shape: `resolve_config_dir_from_parts(...)`, `resolve_data_dir_from_parts(...)`, `resolve_runtime_dir_from_parts(...)`, or equivalent private helpers.
- Keep helper visibility private or `pub(crate)` only if needed by nearby tests.
- Keep runtime behavior unchanged:
- public path functions still read the same supported env vars in the same order;
- empty env values keep the current unset-equivalent behavior;
- resource lookup fallback behavior remains compatible with packaging/dev use.
- Convert path fallback tests that currently use `std::env::set_var` / `remove_var` to direct helper tests where possible.
- Do not add a new helper crate or broad env abstraction.
- Do not remove credential env behavior in this ticket; that belongs to `manifest-profile-encrypted-secrets`.
- Do not attempt to eliminate subprocess integration env setup if a spawned process still needs runtime isolation; report any remaining env mutation rather than forcing risky API changes.
## Non-goals
- Removing all env reads from production code.
- Removing provider credential env support.
- Redesigning runtime directory authority or Pod process startup.
- Introducing test-support / EnvSnapshot / trait-based environment abstraction.
- Changing documented path semantics.
## Acceptance criteria
- `manifest::paths` fallback/precedence tests no longer mutate process-global environment just to test fallback order.
- The fallback order documented in `docs/environment.md` still matches the code.
- Any remaining `set_var` / `remove_var` in touched tests is either eliminated or explicitly justified in the implementation report.
- No new environment variable surface is introduced.
- `cargo fmt --check`, `cargo test -p manifest paths`, relevant affected tests, `cargo check -p manifest`, `./tickets.sh doctor`, and `git diff --check` pass.

View File

@ -0,0 +1,21 @@
Made `manifest::paths` fallback tests independent from process-global environment mutation.
Implementation:
- Added narrow private per-key fallback helpers for config/data/runtime path resolution.
- Public path functions still read the same env vars and pass them in the same precedence order.
- Empty path env values still resolve as unset.
- Fallback-order tests now pass direct `Option<PathBuf>` inputs instead of using `std::env::set_var` / `remove_var`.
- No `PathEnv`, `EnvSnapshot`, env trait, test-support crate, or new env var surface was introduced.
- Credential env behavior and Pod runtime/process behavior were not changed.
Review:
- External reviewer `pure-path-fallback-reviewer-20260531` approved implementation commit `e232f54`.
Validation after merge:
- `cargo fmt --check`
- `cargo test -p manifest paths`
- `cargo test -p manifest`
- `cargo check -p manifest` (passed with unrelated existing `llm-worker` dead_code warning)
- `./tickets.sh doctor`
- `git diff --check`
- `git grep -n -E 'set_var\(|remove_var\(' -- crates/manifest/src/paths.rs || true` produced no matches.

View File

@ -0,0 +1,131 @@
<!-- event: create author: tickets.sh at: 2026-05-31T10:46:14Z -->
## Created
Created by tickets.sh create.
---
<!-- event: plan author: hare at: 2026-05-31T10:46:51Z -->
## Plan
Preflight classification: implementation-ready.
Intent:
- Remove unnecessary process-global environment mutation from path fallback tests by making the fallback/precedence logic directly testable with explicit inputs.
Requirements:
- Start with `manifest::paths`, where path resolution is centralized and currently owns env fallback tests.
- Use small per-key pure helpers with direct optional path inputs. Do not introduce a general `PathEnv`, `EnvSnapshot`, env trait, or test-support crate.
- Keep public runtime behavior unchanged: the production wrappers still read the same env vars, ignore empty path env values as before, and resolve the same fallback order.
- Keep docs/environment.md behavior aligned with code.
Current code map:
- `crates/manifest/src/paths.rs`: core config/data/runtime/resource path resolution and env fallback tests using local `EnvGuard`.
- `docs/environment.md`: documented fallback order for home/config_dir/data_dir/runtime_dir/resource_dir.
- Other env mutations in `pod`/`pod-registry` are mostly subprocess/runtime-isolation integration setup and should not be force-fixed in this ticket.
- Provider credential env mutation belongs to `manifest-profile-encrypted-secrets`, not this ticket.
Critical risks:
- Accidentally changing path precedence while making tests pure.
- Creating an over-broad abstraction that keeps env mutation around indirectly.
- Treating spawned-process runtime isolation env setup as equivalent to pure fallback tests and breaking integration tests.
Intent packet for implementation:
Intent:
- Make `manifest::paths` fallback tests pure and independent from process env mutation.
Requirements:
- Extract narrow pure helper functions for fallback precedence.
- Convert fallback-order tests to call those helpers directly.
- Keep production path functions and documented behavior unchanged.
- Do not introduce a new crate, `PathEnv` struct, env trait, or test-only env var.
Invariants:
- Path authority remains in `manifest::paths`.
- No new environment-variable surface.
- No provider credential/env cleanup in this ticket.
- No Pod runtime/process model changes.
Non-goals:
- Eliminating all `set_var`/`remove_var` in the repository.
- Reworking resource packaging lookup beyond preserving behavior.
Escalate if:
- Removing env mutation requires changing public APIs or path semantics.
- Resource lookup behavior is ambiguous enough to need a product decision.
- Pod runtime integration tests appear to need broader runtime-dir API changes.
Validation:
- `cargo fmt --check`
- `cargo test -p manifest paths`
- relevant additional manifest tests if names differ
- `cargo check -p manifest`
- `./tickets.sh doctor`
- `git diff --check`
- report remaining `set_var`/`remove_var` in touched areas and why they remain, if any.
---
<!-- event: review author: hare at: 2026-05-31T10:54:18Z status: approve -->
## Review: approve
External reviewer: `pure-path-fallback-reviewer-20260531`
Reviewed implementation commit: `e232f54` (`test: make path fallback tests pure`)
Verdict: approve
Summary:
- `manifest::paths` fallback/precedence logic was split into private pure helpers.
- Tests now pass direct `Option<PathBuf>` inputs to verify fallback order rather than mutating process environment.
- No `PathEnv`/`EnvSnapshot`/trait/test-support crate was introduced.
- The change is limited to `crates/manifest/src/paths.rs`.
Requirements mapping:
- `config_dir`, `data_dir`, and `runtime_dir` public wrappers still read the same env keys and pass them in the existing precedence order.
- Empty env values still become `None` through the retained `env_path`/path conversion behavior.
- `docs/environment.md` fallback order matches the code.
- `git grep -n -E 'set_var\(|remove_var\(' -- crates/manifest/src/paths.rs` has no matches.
- No credential env behavior, Pod runtime/process behavior, or new env var surface was changed.
Blockers: none.
Non-blocking follow-ups: none.
Validation adequacy:
- Coder validation covered fmt, focused/full manifest tests, manifest check, doctor, diff check, and set_var/remove_var grep.
- Reviewer performed read-only diff/source/docs consistency review and grep/diff-check spot checks; no additional Cargo tests were rerun by reviewer.
---
<!-- event: close author: hare at: 2026-05-31T10:54:49Z status: closed -->
## Closed
Made `manifest::paths` fallback tests independent from process-global environment mutation.
Implementation:
- Added narrow private per-key fallback helpers for config/data/runtime path resolution.
- Public path functions still read the same env vars and pass them in the same precedence order.
- Empty path env values still resolve as unset.
- Fallback-order tests now pass direct `Option<PathBuf>` inputs instead of using `std::env::set_var` / `remove_var`.
- No `PathEnv`, `EnvSnapshot`, env trait, test-support crate, or new env var surface was introduced.
- Credential env behavior and Pod runtime/process behavior were not changed.
Review:
- External reviewer `pure-path-fallback-reviewer-20260531` approved implementation commit `e232f54`.
Validation after merge:
- `cargo fmt --check`
- `cargo test -p manifest paths`
- `cargo test -p manifest`
- `cargo check -p manifest` (passed with unrelated existing `llm-worker` dead_code warning)
- `./tickets.sh doctor`
- `git diff --check`
- `git grep -n -E 'set_var\(|remove_var\(' -- crates/manifest/src/paths.rs || true` produced no matches.
---

View File

@ -0,0 +1,53 @@
---
id: 20260531-110818-remove-resource-dir
slug: remove-resource-dir
title: Manifest: remove filesystem resource_dir dependency
status: closed
kind: task
priority: P2
labels: [manifest, profile, nix, env, cleanup]
created_at: 2026-05-31T11:08:18Z
updated_at: 2026-05-31T11:58:28Z
assignee: null
legacy_ticket: null
---
## Background
`manifest::paths::resource_dir()` and `INSOMNIA_RESOURCE_DIR` currently keep a filesystem resource lookup boundary for bundled assets. That boundary appears increasingly historical:
- prompts are already embedded through `include_dir!` in the Pod prompt loader;
- provider/model builtin catalogs are already embedded in the provider catalog path;
- the remaining important uses are builtin Lua profile discovery and a manifest-profile model-context lookup that still reads `resources/models/builtin.toml` through `resource_dir()`.
The desired direction is to remove the filesystem `resource_dir` dependency rather than preserve `INSOMNIA_RESOURCE_DIR` as a public/user-facing configuration surface.
## Requirements
- Investigate and then remove the need for `manifest::paths::resource_dir()` if feasible.
- Remove `INSOMNIA_RESOURCE_DIR` as an active supported environment variable once no production code needs it.
- Preserve current builtin behavior:
- builtin/default profiles remain available through normal profile selection;
- builtin provider/model metadata remains available;
- prompts and profile assets still resolve correctly in dev and installed/Nix builds.
- Prefer embedding builtin assets in Rust over runtime filesystem discovery where this does not create a worse design.
- If builtin Lua profiles need synthetic source labels or special handling for diagnostics/local requires, design that explicitly rather than keeping `resource_dir` by inertia.
- Update Nix packaging only after confirming resources no longer need to be installed at `share/insomnia/resources` for runtime behavior.
- Update `docs/environment.md` to remove `resource_dir` / `INSOMNIA_RESOURCE_DIR` from supported path keys if implementation removes it.
## Non-goals
- Changing user/project `config_dir` override semantics.
- Removing user profile files or project profile support.
- Changing profile selector semantics except where needed to replace builtin profile filesystem discovery.
- Reworking credential env handling.
- Reintroducing ambient manifest discovery.
## Acceptance criteria
- No production code depends on `manifest::paths::resource_dir()` for bundled asset lookup, or any remaining dependency is explicitly justified and documented in the ticket thread.
- `INSOMNIA_RESOURCE_DIR` is removed from active code/docs if `resource_dir()` is removed.
- Builtin default profile/profile registry tests pass without filesystem resource discovery.
- Builtin model/provider lookup behavior remains available and tested.
- Nix build/install checks still pass; installed package does not need runtime filesystem resources unless explicitly justified.
- `cargo fmt --check`, focused manifest/provider/pod/tui tests as relevant, `cargo check` for affected crates, `./tickets.sh doctor`, and `git diff --check` pass.

View File

@ -0,0 +1,31 @@
Removed runtime filesystem resource discovery for bundled assets and removed `INSOMNIA_RESOURCE_DIR` as an active configuration surface.
Implementation:
- Removed `RESOURCE_DIR_ENV`, `resource_dir()`, and `builtin_profiles_dir()` from `manifest::paths`.
- Embedded builtin Lua profile source (`default.lua`) and exposed builtin profile metadata without fake filesystem paths.
- Preserved user/project registry profile and explicit path profile filesystem semantics.
- Replaced manifest-side builtin model context lookup with an embedded `resources/models/builtin.toml` source, avoiding a `manifest -> provider` dependency.
- Removed installed runtime `share/insomnia/resources` packaging and checks from Nix package output.
- Updated environment/Nix/pod-factory docs so runtime resource directory and `INSOMNIA_RESOURCE_DIR` are no longer supported/documented surfaces.
Review:
- External reviewer `remove-resource-dir-reviewer-20260531` approved implementation commit `365ec8b7fad008ab36bdc4de3adadb3696739a07`.
- Reviewer found no blockers. A suggested non-blocking future regression test for embedded profile local `require` diagnostics was recorded in the review thread.
Validation after merge:
- `cargo fmt --check`
- `cargo test -p manifest profile`
- `cargo test -p provider catalog`
- `cargo test -p pod prompt`
- `cargo check -p manifest -p provider -p pod -p tui`
- `./tickets.sh doctor`
- `git diff --check`
- `rg -n 'INSOMNIA_RESOURCE_DIR|RESOURCE_DIR_ENV|resource_dir\(|builtin_profiles_dir\(' crates docs package.nix || true` produced no active hits.
- `nix build .#insomnia`
- `test -x result/bin/insomnia`
- `test ! -e result/bin/insomnia-pod`
- `test ! -e result/share/insomnia/resources`
- `result/bin/insomnia pod --help`
Note:
- Initial post-merge validation hit `No space left on device` while linking. `cargo clean` freed build artifacts, after which Rust and Nix validation passed.

View File

@ -0,0 +1,179 @@
<!-- event: create author: tickets.sh at: 2026-05-31T11:08:18Z -->
## Created
Created by tickets.sh create.
---
<!-- event: plan author: hare at: 2026-05-31T11:28:41Z -->
## Plan
Read-only investigator: `resource-dir-removal-investigator-20260531`
Classification: implementation-ready
Conclusion:
- `manifest::paths::resource_dir()` can be removed.
- `INSOMNIA_RESOURCE_DIR` can be removed as an active supported environment variable after replacing the two remaining runtime filesystem resource dependencies.
- Builtin assets should be embedded rather than discovered through installed `share/insomnia/resources`.
Current code map:
- `crates/manifest/src/paths.rs`
- `RESOURCE_DIR_ENV = "INSOMNIA_RESOURCE_DIR"`
- `resource_dir()` checks env, installed `$prefix/share/insomnia/resources`, then source-tree `resources/`.
- `builtin_profiles_dir()` returns `resource_dir()/profiles`.
- `crates/manifest/src/profile.rs`
- builtin profile discovery still scans `paths::builtin_profiles_dir()`.
- `builtin_model_context_window()` still reads `resource_dir()/models/builtin.toml`.
- `crates/provider/src/catalog.rs`
- provider/model builtin catalogs are already embedded with `include_str!`.
- `crates/pod/src/prompt/loader.rs` and `catalog.rs`
- builtin prompts are already embedded with `include_dir!` / `include_str!`.
- `package.nix` still installs `resources` to `$out/share/insomnia/resources` and checks prompt dir existence.
- `docs/environment.md` and `docs/nix.md` still document runtime resource directory/package resources.
Embedded vs filesystem map:
- Already runtime-embedded:
- `resources/prompts/**`
- `resources/providers/builtin.toml`
- `resources/models/builtin.toml` in provider crate.
- Still runtime-filesystem-dependent:
- `resources/profiles/default.lua` through builtin profile directory scan.
- manifest-side model context lookup through `resource_dir()/models/builtin.toml`.
- Should remain filesystem-based:
- user/project `profiles.toml` and profile paths;
- config prompt/model/provider overrides;
- explicit `--manifest <path>` and explicit filesystem profile path selectors.
Recommended implementation:
1. Remove `RESOURCE_DIR_ENV`, `resource_dir()`, and `builtin_profiles_dir()` from `manifest::paths`.
2. Embed builtin Lua profiles explicitly in `manifest::profile`, starting with `resources/profiles/default.lua`.
3. Preserve profile selector semantics for `default`, `builtin:default`, user/project source-qualified selectors, explicit paths, and ambiguity failures.
4. Use synthetic builtin provenance/diagnostics rather than fake filesystem paths.
5. Keep host Lua modules unchanged. Do not support embedded local filesystem require by falling back to `resources/profiles`; current default profile does not need local require.
6. Replace manifest-side `builtin_model_context_window()` filesystem read with `include_str!("../../../resources/models/builtin.toml")`. Do not make `manifest` depend on `provider` because `provider -> manifest` already exists.
7. Remove Nix runtime install/check for `$out/share/insomnia/resources` after runtime code no longer needs it.
8. Remove `INSOMNIA_RESOURCE_DIR` / `resource_dir` docs from `docs/environment.md`; update Nix/profile docs where they describe installed runtime resources.
Critical risks:
- Changing serialized profile provenance shape for builtin profiles. This is acceptable if tests/docs are updated, but do not pretend embedded profiles have real paths.
- Embedded profile local `require`: current builtin default has none. If future builtin profiles need it, add an explicit embedded module map; do not retain filesystem fallback by inertia.
- Manifest/provider catalog dependency cycle: do not call provider builtin APIs from manifest in this ticket.
- Keep source-tree `resources/` in the Nix build source closure because compile-time `include_str!` / `include_dir!` still need it; only installed runtime resources should disappear.
Implementation-ready intent packet:
Intent:
- Remove runtime filesystem resource discovery for bundled assets and delete `INSOMNIA_RESOURCE_DIR` as an active configuration surface.
Requirements:
- Embed builtin Lua profile(s) and manifest-side builtin model context lookup.
- Preserve existing profile selection behavior and user/project profile semantics.
- Remove runtime `resource_dir` path API and Nix installed resources dependency.
- Update docs to reflect embedded builtin assets and remove `INSOMNIA_RESOURCE_DIR`.
Invariants:
- `config_dir` / user profile / project profile semantics remain unchanged.
- No ambient manifest discovery is restored.
- No `INSOMNIA_RESOURCE_DIR` compatibility path is kept.
- `manifest` must not depend on `provider`.
- Pod runtime remains separate and prompt embedding behavior remains intact.
Non-goals:
- Credential env cleanup.
- Reworking provider catalog ownership into a shared crate.
- Changing user/project config override paths.
Escalate if:
- Builtin profile embedding requires local filesystem require support beyond current default profile.
- A public API requires real builtin profile paths rather than provenance labels.
- Nix packaging unexpectedly needs installed resources for runtime behavior after embedding.
Validation:
- grep for `INSOMNIA_RESOURCE_DIR|RESOURCE_DIR_ENV|resource_dir\(|builtin_profiles_dir\(` in active code/docs/package files.
- `cargo fmt --check`
- `cargo test -p manifest profile`
- focused tests for builtin default profile and compact ratio known-model context
- `cargo test -p provider catalog`
- `cargo test -p pod prompt`
- `cargo check -p manifest -p provider -p pod -p tui`
- `nix build .#insomnia` and install-output checks: `bin/insomnia` exists, `bin/insomnia-pod` absent, `share/insomnia/resources` absent if removed, `insomnia pod --help` works.
- `./tickets.sh doctor`
- `git diff --check`
---
<!-- event: review author: hare at: 2026-05-31T11:54:28Z status: approve -->
## Review: approve
External reviewer: `remove-resource-dir-reviewer-20260531`
Reviewed implementation commit: `365ec8b7fad008ab36bdc4de3adadb3696739a07` (`manifest: embed builtin resources`)
Verdict: approve
Summary:
- Runtime filesystem resource discovery and `INSOMNIA_RESOURCE_DIR` active surface were removed.
- Builtin profile is embedded and represented as `builtin:default` without pretending to have a runtime filesystem path under `resources/profiles`.
- User/project/path profile semantics remain filesystem-backed.
- Builtin model context lookup is embedded on the manifest side without adding a `manifest -> provider` dependency.
- Nix package/docs no longer require installed runtime resources.
Requirements mapping:
- `INSOMNIA_RESOURCE_DIR`, `RESOURCE_DIR_ENV`, `resource_dir()`, and `builtin_profiles_dir()` have no active hits in `crates/`, `docs/`, or `package.nix`.
- Embedded builtin profiles do not silently support local filesystem `require` by falling back to resources; non-filesystem profile local modules produce the existing disabled diagnostic.
- `crates/pod/src/spawn/tool.rs` change is test/API fallout for `ProfileDiscovery::with_sources`, not a runtime/protocol behavior change.
- Runtime installed resources are no longer required; source `resources/` remains build-time input for embeds.
Blockers: none.
Non-blocking follow-ups:
- Add a narrow regression test for an embedded builtin-like profile attempting local `require("x")`, asserting the explicit disabled diagnostic.
- Rerun `nix build .#insomnia` in an environment with enough disk space before treating the ticket as fully release-validated; the final rerun is environment-blocked by disk full.
Validation adequacy:
- Rust validation and forbidden-symbol grep are well targeted.
- Final Nix validation remains pending due to `No space left on device`, not due to a code review finding.
---
<!-- event: close author: hare at: 2026-05-31T11:58:28Z status: closed -->
## Closed
Removed runtime filesystem resource discovery for bundled assets and removed `INSOMNIA_RESOURCE_DIR` as an active configuration surface.
Implementation:
- Removed `RESOURCE_DIR_ENV`, `resource_dir()`, and `builtin_profiles_dir()` from `manifest::paths`.
- Embedded builtin Lua profile source (`default.lua`) and exposed builtin profile metadata without fake filesystem paths.
- Preserved user/project registry profile and explicit path profile filesystem semantics.
- Replaced manifest-side builtin model context lookup with an embedded `resources/models/builtin.toml` source, avoiding a `manifest -> provider` dependency.
- Removed installed runtime `share/insomnia/resources` packaging and checks from Nix package output.
- Updated environment/Nix/pod-factory docs so runtime resource directory and `INSOMNIA_RESOURCE_DIR` are no longer supported/documented surfaces.
Review:
- External reviewer `remove-resource-dir-reviewer-20260531` approved implementation commit `365ec8b7fad008ab36bdc4de3adadb3696739a07`.
- Reviewer found no blockers. A suggested non-blocking future regression test for embedded profile local `require` diagnostics was recorded in the review thread.
Validation after merge:
- `cargo fmt --check`
- `cargo test -p manifest profile`
- `cargo test -p provider catalog`
- `cargo test -p pod prompt`
- `cargo check -p manifest -p provider -p pod -p tui`
- `./tickets.sh doctor`
- `git diff --check`
- `rg -n 'INSOMNIA_RESOURCE_DIR|RESOURCE_DIR_ENV|resource_dir\(|builtin_profiles_dir\(' crates docs package.nix || true` produced no active hits.
- `nix build .#insomnia`
- `test -x result/bin/insomnia`
- `test ! -e result/bin/insomnia-pod`
- `test ! -e result/share/insomnia/resources`
- `result/bin/insomnia pod --help`
Note:
- Initial post-merge validation hit `No space left on device` while linking. `cargo clean` freed build artifacts, after which Rust and Nix validation passed.
---

View File

@ -0,0 +1,62 @@
---
id: 20260531-111956-insomnia-crate-cli-owner
slug: insomnia-crate-cli-owner
title: CLI: make insomnia crate own binary entrypoint and CLI dispatch
status: closed
kind: task
priority: P2
labels: [cli, tui, pod, architecture]
created_at: 2026-05-31T11:19:56Z
updated_at: 2026-05-31T13:20:02Z
assignee: null
legacy_ticket: null
---
## Background
The single-binary migration currently exposes the installed command as `insomnia`, but the binary target and CLI dispatch are still owned by the `tui` package (`cargo run -p tui -- ...`, `crates/tui/src/main.rs`). The helper crate now named `insomnia` was originally introduced for Pod runtime command resolution, which creates a responsibility mismatch.
The desired architecture is that the `insomnia` crate owns the product CLI and binary entrypoint. The `tui` and `pod` crates should be libraries with no `main.rs`, and CLI/tool subcommand routing should happen at the `insomnia` boundary.
## Requirements
- Move product CLI ownership to the `insomnia` crate:
- binary target `insomnia` belongs to package/crate `insomnia`;
- CLI argument parsing and top-level mode/subcommand dispatch live in `insomnia`;
- development entrypoint becomes `cargo run -p insomnia -- ...`.
- Make `tui` a library implementation crate rather than binary/CLI owner.
- `tui` should expose an API for launching the terminal UI with already-parsed options/configuration.
- `tui` should not own `insomnia pod ...` dispatch or headless CLI tool routing.
- Keep `pod` as a library runtime crate with no dependency on `insomnia`.
- `pod` may expose its runtime entrypoint/parser API for `insomnia pod ...`, but it must not know the product CLI crate.
- Move or relocate `PodRuntimeCommand` responsibility out of the current `insomnia` helper role if needed to avoid inverted dependencies.
- `pod -> insomnia` is not acceptable.
- Prefer passing the runtime command from the top-level CLI/orchestrating layer into TUI/client spawn paths.
- If a shared type is still needed, place it in a lower-level crate whose responsibility is process/client mechanics, not in the product CLI crate by accident.
- CLI tools/headless commands should be routed by `insomnia`, not by `tui`.
- Examples: `memory lint`, `pod ...`, future non-TUI commands.
- Preserve observable behavior:
- installed command remains `insomnia`;
- `insomnia -r`, `insomnia --multi`, `insomnia <podname>`, and `insomnia pod ...` keep their behavior;
- no `insomnia-pod` alias is reintroduced;
- Pod runtime remains a separate process.
- Do not add compatibility layers for `cargo run -p tui -- ...`; updating docs/dev commands is sufficient.
## Non-goals
- Renaming the user-facing command away from `insomnia`.
- Merging Pod runtime into the TUI process.
- Reintroducing `INSOMNIA_POD_COMMAND` or test-only env command overrides.
- Redesigning Pod protocol, profile semantics, or manifest semantics.
- Broad TUI module-layout refactors beyond what is necessary to make `tui` library-callable.
## Acceptance criteria
- `cargo run -p insomnia -- --help` and `cargo run -p insomnia -- pod --help` work as the main development entrypoints.
- Package `tui` no longer owns a binary `main.rs` entrypoint, or any remaining binary target is explicitly temporary and not the product CLI owner.
- No active code path requires `pod -> insomnia` or `tui -> insomnia` dependency.
- `insomnia` crate owns top-level CLI parsing/dispatch for TUI launch, Pod runtime launch, and headless CLI tools.
- Pod spawn/restore still launches runtime subprocesses as `insomnia pod ...` through typed command injection from the top-level owner.
- Nix/package outputs still expose only `bin/insomnia`.
- Current docs/dev instructions no longer tell developers to use `cargo run -p tui -- ...` as the primary product CLI path.
- Focused CLI/parser/spawn tests, `cargo fmt --check`, relevant `cargo check`/`cargo test`, `nix build .#insomnia` if packaging changed, `./tickets.sh doctor`, and `git diff --check` pass.

View File

@ -0,0 +1,39 @@
Moved product CLI/binary ownership from `tui` to `insomnia`.
Implementation:
- Moved `PodRuntimeCommand` from the transitional `insomnia` helper role into `client`, so lower crates no longer depend on the product CLI crate.
- Made `tui` a library implementation crate and exposed launch APIs for already-parsed modes/options.
- Added the `insomnia` package binary entrypoint and moved top-level CLI parsing/dispatch there.
- Routed `insomnia pod ...` through `pod::entrypoint` from the `insomnia` crate.
- Routed `insomnia memory lint ...` from the `insomnia` crate.
- Kept normal TUI launch behavior for resume, multi-Pod dashboard, Pod name, `--pod`, and `--session` modes.
- Updated packaging so `package.nix` builds package `insomnia` and still exposes only `bin/insomnia`.
- Updated active development/docs references away from `cargo run -p tui -- ...`.
Review:
- External reviewer `insomnia-cli-owner-reviewer-20260531` initially requested changes for a parser regression around `--resume` combined with Pod selection.
- Fix commit `37281b6` restored mutual exclusion for `-r --pod`, `--pod -r`, and `-r <podname>`, and improved `--multi --pod` diagnostics.
- Reviewer approved after re-review.
Validation after merge:
- `cargo fmt --check`
- `cargo check -p client -p pod -p tui -p insomnia`
- `cargo test -p client`
- `cargo test -p pod`
- `cargo test -p tui`
- `cargo test -p insomnia`
- `cargo run -p insomnia -- --help`
- `cargo run -p insomnia -- pod --help`
- `cargo run -p insomnia -- memory lint --help`
- invalid/conflict CLI smoke tests for `--session not-a-uuid`, `-r --pod`, `--pod -r`, and `-r <podname>`
- dependency checks confirming `client`, `pod`, and `tui` do not depend on `insomnia`, and `tui` does not depend on `pod`
- `nix build .#insomnia`
- `test -x ./result/bin/insomnia`
- `test ! -e ./result/bin/insomnia-pod`
- `./result/bin/insomnia pod --help`
- `./tickets.sh doctor`
- `git diff --check`
- `rg "cargo run -p tui" docs README.md crates package.nix flake.nix || true` produced no active hits.
Notes:
- Validation emitted pre-existing dead-code warnings in `llm-worker`/`tui` but passed.

View File

@ -0,0 +1,193 @@
<!-- event: create author: tickets.sh at: 2026-05-31T11:19:56Z -->
## Created
Created by tickets.sh create.
---
<!-- event: plan author: hare at: 2026-05-31T12:42:22Z -->
## Plan
Read-only investigator: `insomnia-cli-owner-investigator-20260531`
Classification: implementation-ready
Conclusion:
- Proceed with this ticket as one implementation ticket, but implement in ordered internal phases: move `PodRuntimeCommand` to `client`, make `tui` library-callable, move product CLI/binary ownership to `insomnia`, then update packaging/docs.
Current code/dependency map:
- `crates/tui` currently owns the product binary target:
- `crates/tui/Cargo.toml` has `[[bin]] name = "insomnia" path = "src/main.rs"`.
- `crates/tui/src/main.rs` owns top-level CLI parsing, `insomnia pod ...` dispatch, `insomnia memory lint`, and TUI launch.
- `crates/insomnia` currently has no binary and mostly owns `PodRuntimeCommand` only.
- `crates/pod` is already library-only and exposes `pod::entrypoint::run_cli_from`.
- `client`, `pod`, and tests depend on `insomnia` only for `PodRuntimeCommand`, creating the wrong dependency direction for the desired architecture.
- `package.nix` builds `-p tui` even though the installed command is `insomnia`.
Recommended architecture:
```text
insomnia -> tui
insomnia -> pod
insomnia -> client
tui -> client
pod -> client
client -> no insomnia
pod -> no insomnia
tui -> no insomnia
```
Implementation plan:
1. Move `PodRuntimeCommand` from `insomnia` to `client`.
- Prefer `client::runtime_command` with `pub use` from `client`.
- Update `client`, `pod`, and tests to use `client::PodRuntimeCommand`.
- Remove `insomnia` dependency from lower crates.
2. Make `tui` a library implementation crate.
- Add `crates/tui/src/lib.rs`.
- Move TUI module declarations and TUI launch code behind public library API.
- Expose narrow API such as `LaunchOptions`, `LaunchMode`, `launch(...)`.
- Keep terminal setup/teardown in `tui` because it owns terminal behavior.
- Remove product CLI parsing and `pod`/`memory lint` dispatch from `tui`.
3. Make `insomnia` own product CLI/binary.
- Add `crates/insomnia/src/main.rs`.
- Move top-level CLI parser/dispatch from `tui` to `insomnia`.
- Route `pod` to `pod::entrypoint::run_cli_from("insomnia pod", args)`.
- Route `memory lint` from `insomnia`, not `tui`.
- Route normal TUI modes to `tui::launch(...)` with a typed runtime command.
4. Update packaging/docs.
- Remove `[[bin]] name = "insomnia"` from `tui`.
- Make `package.nix` build `-p insomnia`.
- Update active docs/dev commands from `cargo run -p tui -- ...` to `cargo run -p insomnia -- ...`.
Important constraints:
- Do not introduce `pod -> insomnia` or `tui -> insomnia`.
- Do not keep compatibility for `cargo run -p tui -- ...`.
- Do not reintroduce `insomnia-pod`, `INSOMNIA_POD_COMMAND`, or the separate `INSOMNIA_POD_RUNTIME_COMMAND`; that dev override is tracked by `dev-pod-runtime-command-env`.
- Do not change Pod protocol/profile/manifest semantics.
- Avoid broad TUI refactoring beyond making the crate library-callable.
Critical risks:
- `tui/src/main.rs` is large and mixes CLI, terminal lifecycle, event loop, key handling, and tests. Keep changes mechanical and avoid unrelated module cleanup.
- Parser tests should move to `insomnia`; TUI behavior tests should remain with `tui`.
- `memory_lint` is headless product CLI code and should not remain routed through TUI.
- `--help` behavior needs explicit handling because the old parser mostly ignored unknown flags.
Validation:
- `cargo fmt --check`
- `cargo check -p client -p pod -p tui -p insomnia`
- `cargo test -p client`
- `cargo test -p pod`
- `cargo test -p tui`
- `cargo test -p insomnia`
- CLI smoke:
- `cargo run -p insomnia -- --help`
- `cargo run -p insomnia -- pod --help`
- `cargo run -p insomnia -- memory lint --help`
- `cargo run -p insomnia -- --session not-a-uuid`
- Dependency ownership checks:
- `cargo tree -p client -e no-dev --depth 1`
- `cargo tree -p pod -e no-dev --depth 1`
- `cargo tree -p tui -e no-dev --depth 1`
- `cargo tree -p insomnia -e no-dev --depth 1`
- If packaging changed:
- `nix build .#insomnia`
- `./result/bin/insomnia pod --help`
- `test -x ./result/bin/insomnia`
- `test ! -e ./result/bin/insomnia-pod`
- `./tickets.sh doctor`
- `git diff --check`
- `rg "cargo run -p tui" docs README.md crates package.nix flake.nix`
---
<!-- event: review author: hare at: 2026-05-31T13:17:15Z status: approve -->
## Review: approve
External reviewer: `insomnia-cli-owner-reviewer-20260531`
Reviewed implementation commits:
- `22d974a` (`cli: move product entrypoint to insomnia`)
- `37281b6` (`cli: reject resume with pod selection`)
Verdict: approve
Summary:
- Product binary and top-level CLI dispatch moved from the `tui` package to the `insomnia` package.
- `tui` is now a library implementation crate and no longer owns `pod`/`memory lint` top-level dispatch.
- `PodRuntimeCommand` moved to `client`, removing the wrong lower-crate dependency on the product CLI crate.
- `pod` remains library-only and does not depend on `insomnia`.
- Packaging now builds package `insomnia` for the installed `bin/insomnia` output.
Initial review blocker:
- `--resume` combined with `--pod` or positional Pod name selection initially stopped erroring.
Fix:
- Commit `37281b6` restored mutual exclusion:
- `insomnia -r --pod agent` errors.
- `insomnia --pod agent -r` errors.
- `insomnia -r agent` errors.
- `insomnia --multi --pod agent` reports `--multi and --pod are mutually exclusive`.
Requirements mapping:
- `insomnia` owns binary target and top-level CLI dispatch.
- `tui`, `pod`, and `client` do not depend on `insomnia`.
- `tui` does not depend on `pod`.
- `insomnia pod ...`, `insomnia memory lint ...`, and normal TUI launch modes are preserved.
- No `insomnia-pod`, `INSOMNIA_POD_COMMAND`, or `INSOMNIA_POD_RUNTIME_COMMAND` was introduced.
- No Pod protocol/profile/manifest semantic change was found.
Blockers: none.
Validation adequacy:
- Coder ran Rust tests/checks, CLI smoke, dependency ownership checks, Nix build/install smoke, doctor, diff-check, and active `cargo run -p tui` grep.
- Reviewer reran focused parser tests and manually confirmed conflict diagnostics through the built binary after the fix.
---
<!-- event: close author: hare at: 2026-05-31T13:20:02Z status: closed -->
## Closed
Moved product CLI/binary ownership from `tui` to `insomnia`.
Implementation:
- Moved `PodRuntimeCommand` from the transitional `insomnia` helper role into `client`, so lower crates no longer depend on the product CLI crate.
- Made `tui` a library implementation crate and exposed launch APIs for already-parsed modes/options.
- Added the `insomnia` package binary entrypoint and moved top-level CLI parsing/dispatch there.
- Routed `insomnia pod ...` through `pod::entrypoint` from the `insomnia` crate.
- Routed `insomnia memory lint ...` from the `insomnia` crate.
- Kept normal TUI launch behavior for resume, multi-Pod dashboard, Pod name, `--pod`, and `--session` modes.
- Updated packaging so `package.nix` builds package `insomnia` and still exposes only `bin/insomnia`.
- Updated active development/docs references away from `cargo run -p tui -- ...`.
Review:
- External reviewer `insomnia-cli-owner-reviewer-20260531` initially requested changes for a parser regression around `--resume` combined with Pod selection.
- Fix commit `37281b6` restored mutual exclusion for `-r --pod`, `--pod -r`, and `-r <podname>`, and improved `--multi --pod` diagnostics.
- Reviewer approved after re-review.
Validation after merge:
- `cargo fmt --check`
- `cargo check -p client -p pod -p tui -p insomnia`
- `cargo test -p client`
- `cargo test -p pod`
- `cargo test -p tui`
- `cargo test -p insomnia`
- `cargo run -p insomnia -- --help`
- `cargo run -p insomnia -- pod --help`
- `cargo run -p insomnia -- memory lint --help`
- invalid/conflict CLI smoke tests for `--session not-a-uuid`, `-r --pod`, `--pod -r`, and `-r <podname>`
- dependency checks confirming `client`, `pod`, and `tui` do not depend on `insomnia`, and `tui` does not depend on `pod`
- `nix build .#insomnia`
- `test -x ./result/bin/insomnia`
- `test ! -e ./result/bin/insomnia-pod`
- `./result/bin/insomnia pod --help`
- `./tickets.sh doctor`
- `git diff --check`
- `rg "cargo run -p tui" docs README.md crates package.nix flake.nix || true` produced no active hits.
Notes:
- Validation emitted pre-existing dead-code warnings in `llm-worker`/`tui` but passed.
---

View File

@ -0,0 +1,57 @@
---
id: 20260531-124040-dev-pod-runtime-command-env
slug: dev-pod-runtime-command-env
title: Dev: add Pod runtime executable override env
status: closed
kind: task
priority: P2
labels: [cli, pod, env, dev]
created_at: 2026-05-31T12:40:40Z
updated_at: 2026-05-31T20:41:56Z
assignee: null
legacy_ticket: null
---
## Background
During dogfooding, long-running TUI/Pod processes can keep executing an old `target/debug/insomnia` inode after `cargo build` or `cargo clean` replaces/removes the binary. On Linux, `std::env::current_exe()` then returns a path like:
```text
/home/.../target/debug/insomnia (deleted)
```
Current Pod spawn/restore paths use `current_exe() + ["pod"]` as the runtime command, so a long-running process can try to spawn `.../insomnia (deleted) pod` and fail with `No such file or directory`.
This is primarily a development/dogfooding problem. The previous general-purpose `INSOMNIA_POD_COMMAND` override was intentionally removed; do not restore that semantics. Instead, add a narrow development escape hatch that replaces only the executable used in the standard `insomnia pod ...` runtime command.
## Requirements
- Add a narrowly scoped development override environment variable, tentatively `INSOMNIA_POD_RUNTIME_COMMAND`.
- Semantics:
- if unset/empty, default remains `current_exe() + ["pod"]`;
- if set, the value is the executable path used instead of `current_exe()`;
- the `pod` prefix argument is still automatically added;
- the value is not shell-parsed and cannot include arguments.
- Keep this as a development escape hatch, not normal user configuration.
- Update diagnostics so a failed spawn shows the resolved runtime command clearly enough to debug deleted-path issues.
- Document the variable in `docs/environment.md` under a clearly development-only section.
- Do not reintroduce `INSOMNIA_POD_COMMAND` or its old executable-without-prefix semantics.
- Do not change Pod runtime flags/profile/manifest/protocol semantics.
- This ticket may be implemented before or folded into `insomnia-crate-cli-owner`, but must not create `pod -> insomnia` or `tui -> insomnia` architectural coupling beyond the current transitional state.
## Non-goals
- Making environment variables a general configuration mechanism.
- Adding shell-string command parsing.
- Supporting wrapper commands with arguments.
- Solving all hot-reload/self-rebuild lifecycle issues.
- Reintroducing the removed `insomnia-pod` binary/alias.
## Acceptance criteria
- Setting `INSOMNIA_POD_RUNTIME_COMMAND=/path/to/insomnia` causes spawn/restore paths to execute `/path/to/insomnia pod ...`.
- Unset or empty `INSOMNIA_POD_RUNTIME_COMMAND` preserves the default `current_exe() + ["pod"]` behavior.
- Tests cover override, empty-as-unset, and no shell parsing/argument splitting semantics.
- Active code/docs do not reference the old `INSOMNIA_POD_COMMAND` name except historical work items.
- `docs/environment.md` explains this as a development-only escape hatch for dogfooding/rebuild deleted-exe cases.
- `cargo fmt --check`, focused tests for the runtime command helper/spawn paths, `cargo check` for affected crates, `./tickets.sh doctor`, and `git diff --check` pass.

View File

@ -0,0 +1,23 @@
Added `INSOMNIA_POD_RUNTIME_COMMAND` as a narrow development escape hatch for dogfooding/self-rebuild cases where `current_exe()` can point at a deleted debug binary.
Implementation:
- Added the override in `client::PodRuntimeCommand`.
- Unset/empty values preserve the default `current_exe() + ["pod"]` behavior.
- Non-empty values replace only the executable path and still automatically receive the `pod` prefix argument.
- The value is not shell-parsed and is not argument-split.
- Spawn/restore failure diagnostics include the resolved runtime command.
- Documented the variable in `docs/environment.md` as development-only, not normal user configuration.
- Did not reintroduce `INSOMNIA_POD_COMMAND` or old executable-without-prefix semantics.
Review:
- External reviewer `dev-pod-runtime-env-reviewer-20260531` approved implementation commit `0031953ed352ba7fae9e798b6aeee1e8ea080816`.
- Reviewer noted a non-blocking future improvement: display formatting for runtime commands could quote argv pieces with spaces more clearly.
Validation after merge:
- `cargo fmt --check`
- `cargo test -p client runtime_command`
- `cargo test -p client`
- `cargo check -p client -p pod -p tui -p insomnia` (passed with existing dead-code warnings)
- `./tickets.sh doctor`
- `git diff --check`
- `git grep -n "INSOMNIA_POD_COMMAND" -- ':!work-items' || true` produced no active references.

View File

@ -0,0 +1,72 @@
<!-- event: create author: tickets.sh at: 2026-05-31T12:40:40Z -->
## Created
Created by tickets.sh create.
---
<!-- event: review author: hare at: 2026-05-31T20:41:28Z status: approve -->
## Review: approve
External reviewer: `dev-pod-runtime-env-reviewer-20260531`
Reviewed implementation commit: `0031953ed352ba7fae9e798b6aeee1e8ea080816` (`dev: add pod runtime command override`)
Verdict: approve
Summary:
- Added `INSOMNIA_POD_RUNTIME_COMMAND` as a narrow development-only executable override for the standard `insomnia pod ...` runtime command.
- Unset/empty values preserve `current_exe() + ["pod"]`.
- Non-empty values replace only the executable path; the `pod` prefix argument is still automatically added.
- The value is passed as a single `PathBuf`/program and is not shell-parsed or argument-split.
- Diagnostics for failed client spawn/restore include the resolved runtime command.
Requirements mapping:
- Override implementation lives in `client::PodRuntimeCommand`, preserving the post-CLI-owner dependency boundary.
- Spawn/restore paths continue to use typed command construction with `Command::new(program)` and prefix args.
- `docs/environment.md` documents the env var as a development-only escape hatch, not normal configuration.
- Old `INSOMNIA_POD_COMMAND` was not reintroduced and has no active non-work-item refs.
- No Pod runtime flag/profile/manifest/protocol semantic changes were found.
Blockers: none.
Non-blocking follow-up:
- `PodRuntimeCommand::Display` joins argv-like pieces with spaces, so diagnostics can be visually ambiguous when an executable path contains spaces. It is sufficient for this ticket, but a future quoted/structured argv display would be clearer.
Validation adequacy:
- Coder validation covered fmt, focused/full client tests, affected crate check, doctor, diff-check, and old env-name grep.
- Reviewer performed read-only diff/source/docs/grep review and did not rerun Cargo tests.
---
<!-- event: close author: hare at: 2026-05-31T20:41:56Z status: closed -->
## Closed
Added `INSOMNIA_POD_RUNTIME_COMMAND` as a narrow development escape hatch for dogfooding/self-rebuild cases where `current_exe()` can point at a deleted debug binary.
Implementation:
- Added the override in `client::PodRuntimeCommand`.
- Unset/empty values preserve the default `current_exe() + ["pod"]` behavior.
- Non-empty values replace only the executable path and still automatically receive the `pod` prefix argument.
- The value is not shell-parsed and is not argument-split.
- Spawn/restore failure diagnostics include the resolved runtime command.
- Documented the variable in `docs/environment.md` as development-only, not normal user configuration.
- Did not reintroduce `INSOMNIA_POD_COMMAND` or old executable-without-prefix semantics.
Review:
- External reviewer `dev-pod-runtime-env-reviewer-20260531` approved implementation commit `0031953ed352ba7fae9e798b6aeee1e8ea080816`.
- Reviewer noted a non-blocking future improvement: display formatting for runtime commands could quote argv pieces with spaces more clearly.
Validation after merge:
- `cargo fmt --check`
- `cargo test -p client runtime_command`
- `cargo test -p client`
- `cargo check -p client -p pod -p tui -p insomnia` (passed with existing dead-code warnings)
- `./tickets.sh doctor`
- `git diff --check`
- `git grep -n "INSOMNIA_POD_COMMAND" -- ':!work-items' || true` produced no active references.
---

View File

@ -1,64 +1,155 @@
---
id: 20260529-145355-manifest-profile-encrypted-secrets
slug: manifest-profile-encrypted-secrets
title: Encrypted secret store for manifest profiles
title: Manifest/Profile: local key-value secret store
status: open
kind: feature
priority: P2
labels: [manifest, profiles, secrets, security]
labels: [manifest, profiles, secrets, security, cli, tui]
created_at: 2026-05-29T14:53:55Z
updated_at: 2026-05-29T14:53:55Z
updated_at: 2026-05-31T21:23:46Z
assignee: null
legacy_ticket: null
---
## Background
WebSearch/WebFetch made API keys more visible as a UX problem: `WebSearch` currently expects `web.search.api_key_env`, so users must export `BRAVE_SEARCH_API_KEY` before starting the Pod/TUI process. That is inconvenient for long-lived Pods, profile switching, and per-project/provider configuration.
Credential configuration still relies on process environment variables in important paths:
This should not be solved by adding `.env` loading as an implicit side effect. `.env` files are easy to leak into projects, do not solve profile-specific credential selection cleanly, and still expose secrets through process environments. Instead, when manifest profiles are designed/implemented, add a first-class encrypted secret store that manifests/profiles can reference.
- provider API keys use `AuthRef::ApiKey { env, file }` and provider-default `INSOMNIA_API_KEY_*` names;
- WebSearch currently uses `web.search.api_key_env`;
- normal runtime intentionally does not load `.env` files.
Related work item: `work-items/open/20260527-000022-manifest-profiles/item.md`.
This should not be solved by implicit `.env` loading. `.env` files are easy to leak into projects, do not solve profile-specific credential selection cleanly, and still expose secrets through process environments.
The desired replacement is a local key-value secret store plus explicit references from manifest/profile/tool configuration.
The security target is intentionally modest. This is not a high-assurance password manager. The goal is to avoid casual plaintext exposure and generic environment-variable scraping, not to defend against a local attacker with the user's account or process memory access.
## Intent
Implement a provider-independent local key-value secret store and use it as the normal credential path for provider and WebSearch credentials.
The logical store model is just:
```text
{
"anthropic/default" = "sk-..."
"web/brave/default" = "..."
}
```
The store must not know that a key is Anthropic, Brave, OpenAI, or any other provider-specific kind. Provider/model/tool configuration chooses which key to reference.
## Requirements
- Design a typed secret reference format for manifest/profile fields that need credentials.
- Add a new encrypted-store reference form, e.g. `api_key_secret = "brave.search.default"` or a more general `SecretRef` enum.
- Existing env references such as `api_key_env = "BRAVE_SEARCH_API_KEY"` may be supported only as a migration/compatibility input during the transition; the target state is to remove credential environment-variable configuration rather than keep it as a normal fallback.
- Secret references must be explicit in resolved config; do not silently read arbitrary `.env` files.
- Add an encrypted local secret store suitable for API keys/tokens.
- Store secrets outside tracked project files by default, under the user data/config directory.
- Use authenticated encryption and atomic writes.
- Do not log plaintext secrets, include them in session logs, expose them to model context, or return them through normal tool output.
- Keep encrypted blobs out of git-managed work-items/memory/session records.
- Integrate with manifest profiles.
- Profiles should be able to select different secret names for different roles/providers, e.g. Orchestrator/Coder/Researcher or web search provider variants.
- Profile resolution should validate that referenced secrets exist or produce a clear startup/tool diagnostic.
- A profile switch must not require restarting the shell just to change API keys.
- Provide a small CLI/TUI management surface.
- Add/update/list/delete secrets without printing plaintext by default.
- Support non-interactive set from stdin for scripts.
- Show references and metadata, not secret values.
- Consider migration helpers from existing env-var based configuration, but keep migration optional.
- Update credential consumers.
- WebSearch should use encrypted secret refs instead of requiring env vars.
- Provider API keys/tokens and future hosted/search credentials should use the same mechanism.
- Remove env-var credential configuration from the normal supported path once encrypted secret refs and migration diagnostics exist.
- Security and UX constraints.
- Fail closed when a referenced secret is missing or cannot be decrypted.
- Diagnostics should name the missing reference, not the secret value.
- Do not add hidden context injection or history mutation for secret resolution.
- Document the threat model and limitations, including OS account access and backup implications.
### Store model
- Add a local secret/key store that maps a validated string id to a secret string value.
- Keep the user-visible logical schema provider-independent: `id -> value`.
- Do not add provider-specific slots, credential kinds, or required metadata to the store schema.
- Technical envelope fields needed for versioning/nonce/ciphertext/checksum are allowed, but they must not become user-facing semantic metadata.
- Store data outside the repository, under the user data directory, e.g. `<data_dir>/secrets/store.json` or equivalent.
- Use atomic writes.
- Validate ids:
- reject empty ids;
- reject path traversal / absolute-path-like ids;
- reject control characters;
- bound length;
- allow a conservative useful set such as ASCII alnum plus `._/-`.
### Obfuscation / encryption stance
- Apply lightweight encryption or obfuscation at rest so the file is not a casual plaintext key dump.
- Do not claim strong local security guarantees.
- Do not introduce OS keychain dependency or interactive passphrase UX in this ticket.
- Do not store plaintext values in logs, work items, session history, diagnostics, or normal command output.
- Decryption/decoding failures must fail closed and name only the key id, not the value.
### `insomnia keys` TUI management
- Add `insomnia keys` as an interactive TUI key manager.
- The product CLI owner (`insomnia` crate) routes the subcommand.
- Use the TUI implementation crate for the terminal screen if practical.
- Minimum UI features:
- list key ids;
- add/set a key;
- delete a key with confirmation;
- quit.
- Do not display plaintext values in the list or normal screen output.
- During add/set, mask the value input or otherwise avoid echoing plaintext.
- Scriptable commands such as `insomnia keys set <id> --stdin` may be added if convenient, but the required user surface is the TUI manager.
### Config references / consumers
- Use explicit secret references from configuration.
- Existing `AuthRef::SecretRef { ref_ }` in manifest model should resolve through the new store for provider API keys.
- WebSearch must gain an explicit secret reference path so Brave search can be configured without `BRAVE_SEARCH_API_KEY` / `api_key_env`.
- Prefer a generic auth/secret-ref shape if it stays small.
- A focused `api_key_secret = "web/brave/default"` field is acceptable if it avoids a broad schema redesign.
- Secret refs are resolved at the consumer/runtime boundary only; resolved config/debug output must not contain plaintext.
- The store must not implicitly choose default keys based on provider name. No ambient lookup like "anthropic automatically reads anthropic/default" unless the profile/config explicitly references it.
### Env credential removal
- Do not load `.env` files.
- Do not add new credential environment variables.
- Do not keep migration/backward-compatibility behavior for credential env config in the normal profile path.
- Remove credential env configuration from normal provider/WebSearch use as part of this ticket.
- Docs and diagnostics should point users to `insomnia keys` + secret refs as the credential path.
### Codex OAuth relationship
- Codex OAuth is not part of this key-value secret store in this ticket.
- Current Codex OAuth intentionally interoperates with Codex CLI's `auth.json` file and refresh behavior; that file contains a structured token bundle, not a single provider API key string.
- Do not store or refresh Codex OAuth token bundles through the key-value store as part of this ticket.
- Do not change `CODEX_HOME` / `$HOME/.codex` lookup behavior in this ticket.
- A future Insomnia-owned Codex login/token store could be designed separately if needed, but it should be a dedicated OAuth token-store design, not an implicit use of the simple key-value API-key store.
## Phases within this ticket
1. Core store
- key-value store API;
- id validation;
- lightweight encrypted/obfuscated file format;
- atomic load/save;
- focused tests.
2. `insomnia keys` TUI manager
- list/add/delete;
- masked input;
- no plaintext display.
3. Provider integration
- implement provider `AuthRef::SecretRef` resolution through the store;
- keep plaintext in memory only;
- fail closed on missing/invalid/decode failures.
4. WebSearch integration
- add a secret-ref credential path;
- make Brave search usable without env credentials.
5. Docs and env removal
- update `docs/environment.md` and manifest/profile docs;
- document the modest security target honestly;
- point users to `insomnia keys` and secret refs as the credential path;
- remove credential env configuration from normal provider/WebSearch docs and code paths.
## Non-goals
- A high-assurance password manager.
- OS keychain integration.
- Passphrase prompt UX.
- Provider-specific secret-store schema.
- Automatic provider-name-to-secret-id lookup.
- Loading `.env` files.
- Changing Codex OAuth behavior. Codex OAuth remains an external structured token-source integration in this ticket.
- Reworking model/provider catalog ownership.
## Acceptance criteria
- Manifest/profile schema has a typed credential reference for encrypted secret-store entries; env-var credential inputs are at most transitional migration inputs, not the final supported configuration path.
- Encrypted secret-store files are created outside the repository by default and use authenticated encryption with atomic update behavior.
- A user can add/list/delete a Brave Search API key in the secret store and configure `WebSearch` to use it without exporting an environment variable.
- Resolved configuration and diagnostics never display plaintext secrets.
- Missing/decryption-failed secrets produce clear fail-closed errors.
- Existing env-var based credential configuration is either removed or produces an explicit migration diagnostic after encrypted secret references are available.
- Documentation explains how profiles reference secrets, how to manage them, and why credential env vars are no longer the normal path.
- Focused tests cover config parsing/resolution, missing secret diagnostics, no-plaintext serialization/logging paths, and WebSearch secret resolution.
- `cargo fmt --check`
- Relevant manifest/provider/tools/pod tests pass.
- A user can run `insomnia keys` and manage key ids interactively.
- The store persists key-value entries under the user data directory without plaintext values in the on-disk file.
- Store id validation rejects unsafe ids.
- Provider `AuthRef::SecretRef` resolves through the store and does not print/serialize plaintext.
- WebSearch can use a configured secret ref without exporting an environment variable.
- Missing key, invalid id, unreadable store, and decode/decrypt failure produce clear fail-closed errors naming only the key id.
- `docs/environment.md` no longer presents credential env vars as the normal path, removes normal provider/WebSearch credential env configuration, and documents the limited protection goal.
- Focused tests cover store round-trip, id validation, decode failure, provider secret-ref resolution, WebSearch secret-ref resolution, and no-plaintext debug/serialization paths where applicable.
- `cargo fmt --check`, relevant crate tests/checks, `./tickets.sh doctor`, and `git diff --check` pass.

View File

@ -4,4 +4,203 @@
Created by tickets.sh create.
---
<!-- event: plan author: hare at: 2026-05-31T20:58:00Z -->
## Plan
Preflight/detailing status: requirements-sync-needed before implementation.
This ticket is the remaining credential/env cleanup boundary. It should not go directly to coding until the secret-store key-management decision is settled. The current code already has some typed-reference groundwork, but no runtime secret store exists.
Current code map:
- `crates/manifest/src/model.rs`
- `AuthRef::SecretRef { ref_ }` already exists in the manifest model and serializes as `auth = { kind = "secret_ref", ref = "..." }`.
- `AuthRef::ApiKey { env, file }` still documents and models env/file API-key sources.
- `SchemeKind::default_env_var()` still provides `INSOMNIA_API_KEY_*` defaults.
- `crates/provider/src/lib.rs`
- `AuthRef::ApiKey` resolves env first, then `auth.file`.
- `AuthRef::SecretRef` currently errors with "secret store references are not implemented yet".
- `crates/tools/src/web.rs`
- Brave WebSearch currently requires `web.search.api_key_env` and reads process env directly.
- `docs/environment.md`
- credential env vars are documented as migration compatibility, not the target supported configuration path.
Proposed target architecture:
- Add a small secret-store boundary independent from `manifest`, `provider`, and `tools` cycles.
- Candidate crate name: `secret-store` or `secrets`.
- Responsibilities: secret id validation, encrypted store read/write, metadata listing, redacted diagnostics, and test-only in-memory/key fixtures.
- Non-responsibilities: provider-specific auth semantics, profile selection, tool networking.
- Keep manifest/profile config as references only.
- Model auth should use the existing `AuthRef::SecretRef { ref_ }` shape rather than adding another model-auth syntax.
- WebSearch needs an explicit secret reference field, e.g. `web.search.api_key_secret = "web/brave/default"` or a typed `web.search.auth = { kind = "secret_ref", ref = "..." }`. Prefer the latter if it avoids another provider-specific one-off, but choose based on minimal config churn.
- Secret IDs should be logical names, not paths.
- Allow a conservative character set such as `[A-Za-z0-9._/-]`.
- Reject empty ids, absolute/path-traversal-like ids, control characters, and extremely long ids.
- IDs are diagnostics-safe; values are not.
- Secret values are resolved only at consumer/runtime boundary.
- Resolved manifests/profiles may contain secret refs, not plaintext.
- Provider/WebSearch consumers receive plaintext only in memory and must not serialize/log it.
Key-management decision required before implementation:
- Do not store an encryption key next to the encrypted store; that provides obfuscation, not meaningful encryption.
- Preferred direction to evaluate: OS keychain/credential manager as the wrapping-key source where available.
- Linux Secret Service / macOS Keychain / Windows Credential Manager through a maintained crate such as `keyring`, if acceptable for dependencies/packaging/headless use.
- If no OS key provider is available, fail closed with clear diagnostics rather than silently writing plaintext or adjacent keys.
- Tests should use an explicit test-only key provider/in-memory store without process-env mutation.
- If a passphrase fallback is desired later, make it explicit interactive/CLI UX; do not read passphrases from ambient env.
Suggested store layout:
- Store encrypted blobs outside the repository, under the user data directory, e.g. `<data_dir>/secrets/store.json` or `<data_dir>/secrets/<id>.json`.
- Use atomic write + fsync where the project already has or can add an atomic-write helper.
- Store metadata needed for listing without plaintext values:
- id
- created_at / updated_at
- optional description/provider/kind
- encryption metadata: algorithm, nonce, key provider/version
- Do not put encrypted blobs under work-items, memory, session logs, project `.insomnia/`, or generated reports.
Encryption requirements:
- Authenticated encryption only (AEAD), e.g. XChaCha20-Poly1305 or AES-GCM depending on dependency choice.
- Unique nonce per encryption.
- Include associated data that binds ciphertext to secret id and store metadata version.
- Decryption/auth failure is a fail-closed error naming the secret id only.
CLI/TUI management scope:
- Initial scope can be CLI-first under `insomnia secrets ...`; TUI management can be follow-up unless UX is trivial.
- Minimum CLI:
- `insomnia secrets set <id> --stdin [--description ...]`
- `insomnia secrets list`
- `insomnia secrets delete <id>`
- optionally `insomnia secrets rename <old> <new>` later, not required for MVP.
- Do not print plaintext by default. Avoid adding `show` unless protected by an explicit `--reveal` decision in a later ticket.
- Non-interactive `set --stdin` is required so scripts can load keys without shell env exports.
Consumer migration plan:
1. Implement secret store + model `AuthRef::SecretRef` resolution in `provider`.
2. Add WebSearch secret reference support and tests.
3. Add CLI management commands.
4. Update docs and examples to use secret refs as the normal path.
5. Convert env-var credential paths into migration diagnostics or compatibility-only one-file/debug behavior, then remove from normal profile path.
Important migration constraints:
- Do not load `.env` files.
- Do not keep env as a normal fallback once secret refs are available.
- If env inputs remain temporarily, diagnostics should say they are migration compatibility and point to `insomnia secrets set ...` / profile secret refs.
- Avoid changing Codex OAuth in this ticket unless a clear secret-store integration is needed; `CODEX_HOME` remains external compatibility.
Acceptance criteria additions:
- `AuthRef::SecretRef` can resolve through the encrypted store for provider API keys without exposing plaintext in resolved config/debug output.
- WebSearch can use a secret ref without `BRAVE_SEARCH_API_KEY` or `web.search.api_key_env`.
- CLI can add/list/delete a secret without printing plaintext.
- Missing secret, invalid id, unavailable key provider, and decryption/auth failure produce clear fail-closed errors naming only the reference.
- Tests cover:
- secret id validation;
- encrypted round-trip with test key provider;
- wrong-key/auth-failure diagnostics;
- provider `AuthRef::SecretRef` resolution;
- WebSearch secret ref resolution;
- no plaintext in `Debug`/serialization paths checked by focused assertions.
Recommended next step:
- Create a short spike/preflight sub-ticket specifically for key provider/dependency choice (`keyring` vs explicit passphrase vs other OS-backed provider). Once that decision is recorded, the implementation ticket can be split into:
1. secret-store crate + CLI management;
2. provider `AuthRef::SecretRef` integration;
3. WebSearch secret ref integration and env migration docs.
---
<!-- event: decision author: hare at: 2026-05-31T21:04:45Z -->
## Decision
Decision: keep the secret store as a provider-independent key-value store.
User decision:
- The secret store path should be completely limited to a simple key-value store.
- Do not hard-code provider-specific slots or provider-specific semantics into the store.
- Do not require metadata in the store schema.
- Conceptual model is:
```text
{
"anthropic/default" = "sk-..."
}
```
- Provider/model/tool configuration is responsible for choosing which secret key to reference.
- The store does not know whether a value is an Anthropic key, Brave key, OpenAI key, token, or anything else.
- Any provider-aware UX should be a higher-level helper that writes ordinary key-value entries and/or config references; it must not change the store schema.
Security stance:
- Use light encryption/obfuscation at rest if practical, but do not claim strong security guarantees.
- The goal is to avoid casual plaintext exposure in files, logs, work items, and accidental grep/cat output, not to defend against a local attacker with access to the user account.
- Avoid complicated key-management requirements such as OS keychain dependency as a prerequisite for this ticket unless a later explicit decision changes the security target.
- Documentation and diagnostics should be honest: this is an obfuscated/encrypted local key-value store with limited protection, not a high-assurance secret manager.
Implications for implementation planning:
- Remove the previous requirement for metadata such as provider/kind/description/created_at/updated_at unless the implementation needs internal versioning/encryption fields.
- Store format may still need technical envelope fields for version/nonce/ciphertext/checksum, but the user-visible logical schema is only `id -> value`.
- Secret id validation remains useful because ids are referenced from manifest/profile/tool config and diagnostics.
- Provider/WebSearch integration should resolve `secret_ref` by direct key lookup only.
---
<!-- event: plan author: hare at: 2026-05-31T21:19:29Z -->
## Plan
Preflight update: implementation-ready as a single phased ticket.
Finalized intent:
- Build a provider-independent local key-value secret store and wire it into provider/WebSearch credential resolution.
- Add `insomnia keys` as the required interactive TUI management surface.
- Keep the security claim modest: avoid casual plaintext exposure and generic env scraping; do not claim strong local security.
Settled decisions:
- Store model is user-visible `id -> value`; no provider-specific slots and no required metadata.
- Store may use technical envelope fields for version/nonce/ciphertext/checksum, but those are implementation details.
- No OS keychain, passphrase UX, or high-assurance key-management dependency in this ticket.
- No automatic provider-name-to-secret-id lookup. Config must explicitly reference a key id.
- This is one ticket with phases, not separate tickets.
Suggested implementation order:
1. Add a focused secret store crate/module with id validation, obfuscated/encrypted persistence, atomic writes, and tests.
2. Add `insomnia keys` TUI manager for list/add/delete with masked input.
3. Implement provider `AuthRef::SecretRef` resolution through the store.
4. Add WebSearch secret-ref configuration path.
5. Update docs and migration messaging.
Critical risks for coder/reviewer:
- Accidentally turning the store into provider-aware schema.
- Displaying plaintext in Debug/errors/TUI list output.
- Overstating security guarantees in docs.
- Adding ambient defaults that recreate the env-var problem in secret-store form.
- Expanding into OS keychain/passphrase design despite the settled modest threat model.
Validation should include focused tests for id validation, store round-trip, decode failure, provider secret-ref resolution, WebSearch secret-ref resolution, and no plaintext in display/debug paths where applicable.
---
<!-- event: decision author: hare at: 2026-05-31T21:23:46Z -->
## Decision
Decision update:
- Migration/backward compatibility for credential environment variables is not required.
- The implementation should remove credential env configuration from the normal provider/WebSearch path rather than keep env fallback as a compatibility layer.
- Docs should point to `insomnia keys` + explicit secret refs as the credential path.
Codex OAuth relationship:
- Codex OAuth should not be connected to this key-value API-key store in this ticket.
- The current Codex integration interoperates with Codex CLI's structured `auth.json` token bundle and refresh behavior.
- That is a different credential shape than `id -> secret string` API keys.
- Keep `CODEX_HOME` / `$HOME/.codex` behavior unchanged for this ticket.
- If Insomnia later owns Codex login/token storage, design it as a separate OAuth token-store feature, not as an implicit use of the simple key-value store.
---

View File

@ -1,7 +0,0 @@
<!-- event: create author: tickets.sh at: 2026-05-31T07:42:58Z -->
## Created
Created by tickets.sh create.
---

View File

@ -1,7 +0,0 @@
<!-- event: create author: tickets.sh at: 2026-05-31T07:42:58Z -->
## Created
Created by tickets.sh create.
---

View File

@ -1,7 +0,0 @@
<!-- event: create author: tickets.sh at: 2026-05-31T07:42:58Z -->
## Created
Created by tickets.sh create.
---

View File

@ -1,7 +0,0 @@
<!-- event: create author: tickets.sh at: 2026-05-31T08:59:59Z -->
## Created
Created by tickets.sh create.
---

View File

@ -1,7 +0,0 @@
<!-- event: create author: tickets.sh at: 2026-05-31T08:59:59Z -->
## Created
Created by tickets.sh create.
---