fix: hide ai agent subprocess windows
This commit is contained in:
@@ -218,7 +218,7 @@ Full agent mode — spawns the selected local CLI agent as a subprocess with too
|
||||
|
||||
1. **Frontend** (`AiPanel` + `useCliAiAgent` + `aiAgents.ts`) — streaming UI with reasoning blocks, tool action cards, response display, onboarding, and default-agent selection
|
||||
2. **Backend** (`ai_agents.rs`) — normalizes agent availability and streaming, dispatching to per-agent adapters
|
||||
3. **Agent adapters** — Claude Code still uses `claude_cli.rs` with `acceptEdits`, strict Tolaria MCP config, a file/search-only built-in tool list, and closed stdin for print-mode subprocesses so Windows launches receive EOF; Codex runs through `codex --sandbox workspace-write --ask-for-approval never exec --json`. Both app-launched paths can edit the active vault without enabling dangerous permission bypass modes.
|
||||
3. **Agent adapters** — Claude Code still uses `claude_cli.rs` with `acceptEdits`, strict Tolaria MCP config, a file/search-only built-in tool list, hidden Windows subprocess launches, and closed stdin for print-mode subprocesses so Windows launches receive EOF; Codex runs through `codex --sandbox workspace-write --ask-for-approval never exec --json` with the same hidden Windows launch policy. Both app-launched paths can edit the active vault without enabling dangerous permission bypass modes.
|
||||
4. **MCP Integration** — Claude receives the generated MCP config file path, while Codex receives the same Tolaria MCP server via transient `-c mcp_servers.tolaria.*` config overrides
|
||||
|
||||
CLI-agent availability intentionally does not depend only on the desktop app's inherited `PATH`. The detectors check the current process path, the user's login shell, and supported local/toolchain install locations such as native `~/.local/bin`, local `~/.claude/local`, Mise/asdf shims, npm-global, Homebrew, Windows `%APPDATA%\npm`/pnpm/Scoop shims, Windows `.exe` launchers, and the macOS Codex app resource path so first-run onboarding works on fresh macOS and Windows installs.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::BufRead;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::process::Stdio;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -113,7 +113,7 @@ fn availability_from_codex() -> AiAgentAvailability {
|
||||
}
|
||||
|
||||
fn version_for_binary(binary: &PathBuf) -> Option<String> {
|
||||
Command::new(binary)
|
||||
crate::hidden_command(binary)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.ok()
|
||||
@@ -138,7 +138,7 @@ fn find_codex_binary() -> Result<PathBuf, String> {
|
||||
}
|
||||
|
||||
fn find_codex_binary_on_path() -> Option<PathBuf> {
|
||||
Command::new("which")
|
||||
crate::hidden_command("which")
|
||||
.arg("codex")
|
||||
.output()
|
||||
.ok()
|
||||
@@ -165,7 +165,7 @@ fn user_shell_candidates() -> Vec<PathBuf> {
|
||||
}
|
||||
|
||||
fn command_path_from_shell(shell: &Path, command: &str) -> Option<PathBuf> {
|
||||
Command::new(shell)
|
||||
crate::hidden_command(shell)
|
||||
.arg("-lc")
|
||||
.arg(format!("command -v {command}"))
|
||||
.output()
|
||||
@@ -225,13 +225,7 @@ where
|
||||
let args = build_codex_args(&request)?;
|
||||
let prompt = build_codex_prompt(&request);
|
||||
|
||||
let mut command = Command::new(binary);
|
||||
command
|
||||
.args(args)
|
||||
.arg(prompt)
|
||||
.current_dir(&request.vault_path)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
let mut command = build_codex_command(&binary, args, prompt, &request.vault_path);
|
||||
|
||||
let mut child = command
|
||||
.spawn()
|
||||
@@ -289,6 +283,22 @@ where
|
||||
Ok(thread_id)
|
||||
}
|
||||
|
||||
fn build_codex_command(
|
||||
binary: &Path,
|
||||
args: Vec<String>,
|
||||
prompt: String,
|
||||
vault_path: &str,
|
||||
) -> std::process::Command {
|
||||
let mut command = crate::hidden_command(binary);
|
||||
command
|
||||
.args(args)
|
||||
.arg(prompt)
|
||||
.current_dir(vault_path)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
command
|
||||
}
|
||||
|
||||
fn build_codex_args(request: &AiAgentStreamRequest) -> Result<Vec<String>, String> {
|
||||
let mcp_server = crate::mcp::mcp_server_dir()?.join("index.js");
|
||||
let mcp_server_path = mcp_server
|
||||
@@ -456,6 +466,7 @@ fn map_claude_event(event: crate::claude_cli::ClaudeStreamEvent) -> Option<AiAge
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::ffi::OsStr;
|
||||
|
||||
#[test]
|
||||
fn normalize_status_contains_both_agents() {
|
||||
@@ -497,6 +508,25 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_codex_command_keeps_agent_process_contract() {
|
||||
let binary = PathBuf::from("codex");
|
||||
let args = vec!["exec".to_string(), "--json".to_string()];
|
||||
let command = build_codex_command(&binary, args, "Summarize".into(), "/tmp/vault");
|
||||
let actual_args: Vec<&OsStr> = command.get_args().collect();
|
||||
|
||||
assert_eq!(command.get_program(), OsStr::new("codex"));
|
||||
assert_eq!(
|
||||
actual_args,
|
||||
vec![
|
||||
OsStr::new("exec"),
|
||||
OsStr::new("--json"),
|
||||
OsStr::new("Summarize")
|
||||
]
|
||||
);
|
||||
assert_eq!(command.get_current_dir(), Some(Path::new("/tmp/vault")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_binary_candidates_include_supported_macos_installs() {
|
||||
let home = PathBuf::from("/Users/alex");
|
||||
|
||||
@@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::io::BufRead;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, ExitStatus, Stdio};
|
||||
use std::process::{ExitStatus, Stdio};
|
||||
|
||||
/// Status returned by `check_claude_cli`.
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
@@ -79,7 +79,7 @@ pub(crate) fn find_claude_binary() -> Result<PathBuf, String> {
|
||||
}
|
||||
|
||||
fn find_claude_binary_on_path() -> Option<PathBuf> {
|
||||
Command::new(claude_path_lookup_command())
|
||||
crate::hidden_command(claude_path_lookup_command())
|
||||
.arg("claude")
|
||||
.output()
|
||||
.ok()
|
||||
@@ -114,7 +114,7 @@ fn user_shell_candidates() -> Vec<PathBuf> {
|
||||
}
|
||||
|
||||
fn command_path_from_shell(shell: &Path, command: &str) -> Option<PathBuf> {
|
||||
Command::new(shell)
|
||||
crate::hidden_command(shell)
|
||||
.arg("-lc")
|
||||
.arg(format!("command -v {command}"))
|
||||
.output()
|
||||
@@ -193,7 +193,7 @@ pub fn check_cli() -> ClaudeCliStatus {
|
||||
}
|
||||
};
|
||||
|
||||
let version = Command::new(&bin)
|
||||
let version = crate::hidden_command(&bin)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.ok()
|
||||
@@ -327,15 +327,7 @@ fn run_claude_subprocess<F>(
|
||||
where
|
||||
F: FnMut(ClaudeStreamEvent),
|
||||
{
|
||||
let mut cmd = Command::new(bin);
|
||||
cmd.args(args)
|
||||
.env_remove("CLAUDECODE") // prevent "nested session" guard
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
if let Some(dir) = cwd {
|
||||
cmd.current_dir(dir);
|
||||
}
|
||||
let mut cmd = build_claude_command(bin, args, cwd);
|
||||
let mut child = cmd
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to spawn claude: {e}"))?;
|
||||
@@ -392,6 +384,23 @@ where
|
||||
Ok(state.session_id)
|
||||
}
|
||||
|
||||
fn build_claude_command(
|
||||
bin: &PathBuf,
|
||||
args: &[String],
|
||||
cwd: Option<&str>,
|
||||
) -> std::process::Command {
|
||||
let mut cmd = crate::hidden_command(bin);
|
||||
cmd.args(args)
|
||||
.env_remove("CLAUDECODE") // prevent "nested session" guard
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
if let Some(dir) = cwd {
|
||||
cmd.current_dir(dir);
|
||||
}
|
||||
cmd
|
||||
}
|
||||
|
||||
fn format_failed_claude_exit(stderr_output: &str, status: ExitStatus) -> String {
|
||||
if is_claude_auth_error(stderr_output) {
|
||||
return "Claude CLI is not authenticated. Run `claude auth login` in your terminal.".into();
|
||||
@@ -594,6 +603,9 @@ fn extract_tool_result_text(json: &serde_json::Value) -> Option<String> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::ffi::OsStr;
|
||||
use std::ffi::OsString;
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn check_cli_returns_status() {
|
||||
@@ -907,6 +919,33 @@ mod tests {
|
||||
|
||||
// --- run_claude_subprocess with mock scripts ---
|
||||
|
||||
#[test]
|
||||
fn build_claude_command_keeps_streaming_process_contract() {
|
||||
let bin = PathBuf::from("claude");
|
||||
let args = vec!["-p".to_string(), "hello".to_string()];
|
||||
let command = build_claude_command(&bin, &args, Some("/tmp/vault"));
|
||||
let actual_args: Vec<OsString> = command.get_args().map(OsStr::to_os_string).collect();
|
||||
let claude_code_env = command
|
||||
.get_envs()
|
||||
.find(|(key, _)| *key == OsStr::new("CLAUDECODE"))
|
||||
.map(|(_, value)| value.map(OsStr::to_os_string));
|
||||
|
||||
assert_eq!(
|
||||
(
|
||||
command.get_program().to_os_string(),
|
||||
actual_args,
|
||||
command.get_current_dir().map(Path::to_path_buf),
|
||||
claude_code_env,
|
||||
),
|
||||
(
|
||||
OsString::from("claude"),
|
||||
vec![OsString::from("-p"), OsString::from("hello")],
|
||||
Some(PathBuf::from("/tmp/vault")),
|
||||
Some(None),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn run_mock_script(script: &str) -> (Result<String, String>, Vec<ClaudeStreamEvent>) {
|
||||
run_mock_script_with_args(script, &[])
|
||||
|
||||
Reference in New Issue
Block a user