Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,12 @@ await api.invoke('your_command', { request: { ... } });
- Desktop-only host adapters belong in `src/apps/desktop`, then flow through typed capability interfaces and, when event delivery is needed, the production transport adapter.
- In shared core, avoid host-specific APIs such as `tauri::AppHandle`; use shared abstractions such as `bitfun_events::EventEmitter`.

#### Child processes in GUI hosts
#### Non-interactive child processes

- GUI hosts such as Desktop and Installer must not spawn child processes with bare
`std::process::Command` or `tokio::process::Command`. Prefer
- Any non-interactive child process that may run under a GUI, headless,
background, or redirected host must not use bare `std::process::Command` or
`tokio::process::Command`, including CLI modes that another host can invoke.
Prefer
`bitfun_services_core::process_manager::{create_command, create_tokio_command}`
or the existing facade for that layer. If a direct command is unavoidable,
Windows code must explicitly apply `CREATE_NO_WINDOW`; Node child processes
Expand Down
3 changes: 1 addition & 2 deletions src/apps/cli/src/dispatch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ mod worker;
mod workspace;

use std::path::{Path, PathBuf};
use std::process::Command;

use anyhow::{anyhow, bail, Context, Result};
use bitfun_core::infrastructure::ai::AIClientFactory;
Expand Down Expand Up @@ -853,7 +852,7 @@ fn classify_repository_probe(result: Result<GitProbeOutput, GitProbeOutput>) ->
/// below matches Git's English prose, and a localized host would otherwise make
/// an ownership rejection unrecognizable.
fn git_probe(workspace: &Path, args: &[&str]) -> Result<GitProbeOutput, GitProbeOutput> {
let output = Command::new("git")
let output = bitfun_services_core::process_manager::create_command("git")
.env("LC_ALL", "C")
.arg("-C")
.arg(workspace)
Expand Down
34 changes: 18 additions & 16 deletions src/apps/cli/src/dispatch/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2916,14 +2916,14 @@ mod tests {
let executable = std::env::current_exe().expect("test executable");
let test_name =
"dispatch::store::tests::cross_process_reader_and_writer_remain_consistent_during_rotation";
let mut reader = std::process::Command::new(&executable)
let mut reader = bitfun_services_core::process_manager::create_command(&executable)
.args(["--exact", test_name, "--nocapture"])
.env(MODE_ENV, "reader")
.env(ROOT_ENV, &root)
.env(DONE_ENV, &done)
.spawn()
.expect("spawn stress reader");
let writer = std::process::Command::new(&executable)
let writer = bitfun_services_core::process_manager::create_command(&executable)
.args(["--exact", test_name, "--nocapture"])
.env(MODE_ENV, "writer")
.env(ROOT_ENV, &root)
Expand Down Expand Up @@ -3076,20 +3076,22 @@ mod tests {
let dir = tempfile::tempdir().expect("tempdir");
let bitfun_home = dir.path().join("bitfun-home");
let user_root = dir.path().join("user-root");
let output = std::process::Command::new(std::env::current_exe().expect("test executable"))
.args([
"--exact",
"dispatch::store::tests::default_store_honors_path_manager_storage_overrides",
"--nocapture",
])
.env(CHILD_ENV, &bitfun_home)
.env("BITFUN_HOME", &bitfun_home)
.env("BITFUN_USER_ROOT", &user_root)
.env("BITFUN_E2E_STORAGE_GUARD", "1")
.env_remove("BITFUN_E2E_HOME")
.env_remove("BITFUN_E2E_USER_ROOT")
.output()
.expect("run isolated path test");
let output = bitfun_services_core::process_manager::create_command(
std::env::current_exe().expect("test executable"),
)
.args([
"--exact",
"dispatch::store::tests::default_store_honors_path_manager_storage_overrides",
"--nocapture",
])
.env(CHILD_ENV, &bitfun_home)
.env("BITFUN_HOME", &bitfun_home)
.env("BITFUN_USER_ROOT", &user_root)
.env("BITFUN_E2E_STORAGE_GUARD", "1")
.env_remove("BITFUN_E2E_HOME")
.env_remove("BITFUN_E2E_USER_ROOT")
.output()
.expect("run isolated path test");
assert!(
output.status.success(),
"isolated child failed:\nstdout:\n{}\nstderr:\n{}",
Expand Down
32 changes: 17 additions & 15 deletions src/apps/cli/src/dispatch/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1573,7 +1573,7 @@ fn commit_exists(repo: &Path, commit: &str) -> Result<bool> {
}

fn git_command(dir: &Path) -> Command {
let mut command = Command::new("git");
let mut command = bitfun_services_core::process_manager::create_command("git");
command
.current_dir(dir)
// A detached dispatch worker has nobody to answer a credential or
Expand Down Expand Up @@ -2419,20 +2419,22 @@ mod tests {
let dir = tempfile::tempdir().expect("tempdir");
let bitfun_home = dir.path().join("bitfun-home");
let user_root = dir.path().join("user-root");
let output = std::process::Command::new(std::env::current_exe().expect("test executable"))
.args([
"--exact",
"dispatch::workspace::tests::completed_clean_sync_poll_returns_the_durable_result",
"--nocapture",
])
.env(CHILD_ENV, &bitfun_home)
.env("BITFUN_HOME", &bitfun_home)
.env("BITFUN_USER_ROOT", &user_root)
.env("BITFUN_E2E_STORAGE_GUARD", "1")
.env_remove("BITFUN_E2E_HOME")
.env_remove("BITFUN_E2E_USER_ROOT")
.output()
.expect("run isolated clean-sync poll test");
let output = bitfun_services_core::process_manager::create_command(
std::env::current_exe().expect("test executable"),
)
.args([
"--exact",
"dispatch::workspace::tests::completed_clean_sync_poll_returns_the_durable_result",
"--nocapture",
])
.env(CHILD_ENV, &bitfun_home)
.env("BITFUN_HOME", &bitfun_home)
.env("BITFUN_USER_ROOT", &user_root)
.env("BITFUN_E2E_STORAGE_GUARD", "1")
.env_remove("BITFUN_E2E_HOME")
.env_remove("BITFUN_E2E_USER_ROOT")
.output()
.expect("run isolated clean-sync poll test");
assert!(
output.status.success(),
"isolated child failed:\nstdout:\n{}\nstderr:\n{}",
Expand Down
10 changes: 4 additions & 6 deletions src/apps/cli/src/modes/exec/tests.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::process::Command;

use super::lifecycle::{
completed_turn_failure, drain_interrupted_turn_events, effective_event_invocation,
event_belongs_to_exec_turn, event_turn_id, is_exec_terminal,
Expand Down Expand Up @@ -103,7 +101,7 @@ fn git_patch_includes_staged_and_untracked_files_from_a_repo_subdirectory() {
let temp = tempfile::tempdir().expect("tempdir");
let repo = temp.path();
let run_git = |args: &[&str]| {
let output = Command::new("git")
let output = bitfun_core::util::process_manager::create_command("git")
.args(args)
.current_dir(repo)
.output()
Expand Down Expand Up @@ -141,7 +139,7 @@ fn git_patch_excludes_a_preexisting_output_artifact_inside_the_repository() {
let temp = tempfile::tempdir().expect("tempdir");
let repo = temp.path();
let run_git = |args: &[&str]| {
let output = Command::new("git")
let output = bitfun_core::util::process_manager::create_command("git")
.args(args)
.current_dir(repo)
.output()
Expand Down Expand Up @@ -181,7 +179,7 @@ fn git_patch_excludes_a_tracked_output_artifact_inside_the_repository() {
let temp = tempfile::tempdir().expect("tempdir");
let repo = temp.path();
let run_git = |args: &[&str]| {
let output = Command::new("git")
let output = bitfun_core::util::process_manager::create_command("git")
.args(args)
.current_dir(repo)
.output()
Expand Down Expand Up @@ -702,7 +700,7 @@ fn deferred_exec_event_projects_effective_name_and_input() {
}

fn run_git(workspace: &std::path::Path, args: &[&str]) {
let output = Command::new("git")
let output = bitfun_core::util::process_manager::create_command("git")
.args(args)
.current_dir(workspace)
.output()
Expand Down
4 changes: 2 additions & 2 deletions src/apps/cli/src/modes/exec/verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,11 @@ pub(super) async fn run_verifier(
retries_used: u32,
) -> VerifyOutcome {
let mut process = if cfg!(windows) {
let mut process = tokio::process::Command::new("cmd");
let mut process = bitfun_core::util::process_manager::create_tokio_command("cmd");
process.arg("/C").arg(command);
process
} else {
let mut process = tokio::process::Command::new("sh");
let mut process = bitfun_core::util::process_manager::create_tokio_command("sh");
process.arg("-c").arg(command);
process
};
Expand Down
4 changes: 3 additions & 1 deletion src/apps/cli/tests/acp_stdio_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ struct AcpProcess {

impl AcpProcess {
async fn spawn(environment: &CliTestEnvironment) -> Self {
let mut command = tokio::process::Command::new(env!("CARGO_BIN_EXE_bitfun"));
let mut command = bitfun_services_core::process_manager::create_tokio_command(env!(
"CARGO_BIN_EXE_bitfun"
));
command
.arg("acp")
.stdin(Stdio::piped())
Expand Down
42 changes: 24 additions & 18 deletions src/apps/cli/tests/cli_command_contracts/compat_entrypoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,16 @@ fn run_freshly_written(command: &mut Command) -> std::io::Result<Output> {

#[test]
fn legacy_version_matches_primary_and_warns_only_on_stderr() {
let primary = Command::new(env!("CARGO_BIN_EXE_bitfun"))
.arg("--version")
.output()
.expect("run bitfun --version");
let legacy = Command::new(env!("CARGO_BIN_EXE_bitfun-cli"))
.arg("--version")
.output()
.expect("run deprecated bitfun-cli --version");
let primary =
bitfun_services_core::process_manager::create_command(env!("CARGO_BIN_EXE_bitfun"))
.arg("--version")
.output()
.expect("run bitfun --version");
let legacy =
bitfun_services_core::process_manager::create_command(env!("CARGO_BIN_EXE_bitfun-cli"))
.arg("--version")
.output()
.expect("run deprecated bitfun-cli --version");

assert!(primary.status.success());
assert!(legacy.status.success());
Expand All @@ -45,14 +47,16 @@ fn legacy_version_matches_primary_and_warns_only_on_stderr() {

#[test]
fn legacy_forwards_clap_failure_exit_code() {
let primary = Command::new(env!("CARGO_BIN_EXE_bitfun"))
.arg("--not-a-real-option")
.output()
.expect("run invalid primary command");
let legacy = Command::new(env!("CARGO_BIN_EXE_bitfun-cli"))
.arg("--not-a-real-option")
.output()
.expect("run invalid legacy command");
let primary =
bitfun_services_core::process_manager::create_command(env!("CARGO_BIN_EXE_bitfun"))
.arg("--not-a-real-option")
.output()
.expect("run invalid primary command");
let legacy =
bitfun_services_core::process_manager::create_command(env!("CARGO_BIN_EXE_bitfun-cli"))
.arg("--not-a-real-option")
.output()
.expect("run invalid legacy command");

assert_eq!(legacy.status.code(), primary.status.code());
assert!(String::from_utf8_lossy(&legacy.stderr).starts_with(DEPRECATION));
Expand All @@ -69,8 +73,10 @@ fn legacy_reports_a_missing_primary_without_recursing() {
let copied = temp.path().join(file_name);
std::fs::copy(env!("CARGO_BIN_EXE_bitfun-cli"), &copied)
.expect("copy deprecated launcher without primary sibling");
let output = run_freshly_written(Command::new(copied).arg("--version"))
.expect("run isolated deprecated launcher");
let output = run_freshly_written(
bitfun_services_core::process_manager::create_command(copied).arg("--version"),
)
.expect("run isolated deprecated launcher");
let stderr = String::from_utf8_lossy(&output.stderr);

assert!(!output.status.success());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
#[path = "../support/mod.rs"]
mod support;

use std::process::{Command, Output};
use std::process::Output;
use support::{
command_output_with_timeout, CliTestEnvironment, MockOpenAiServer, STREAM_COMPLETED_MARKER,
STREAM_START_MARKER,
};

fn run_cli(args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_bitfun"))
bitfun_services_core::process_manager::create_command(env!("CARGO_BIN_EXE_bitfun"))
.args(args)
.output()
.expect("run bitfun")
Expand Down Expand Up @@ -43,13 +43,13 @@ fn is_terminal_event(value: &serde_json::Value) -> bool {
fn command_timeout_helper_returns_within_its_failure_budget() {
#[cfg(windows)]
let mut command = {
let mut command = Command::new("powershell.exe");
let mut command = bitfun_services_core::process_manager::create_command("powershell.exe");
command.args(["-NoProfile", "-Command", "Start-Sleep -Seconds 30"]);
command
};
#[cfg(not(windows))]
let mut command = {
let mut command = Command::new("sh");
let mut command = bitfun_services_core::process_manager::create_command("sh");
command.args(["-c", "exec sleep 30"]);
command
};
Expand Down
4 changes: 2 additions & 2 deletions src/apps/cli/tests/cli_command_contracts/mcp_add_cli.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use std::path::Path;
use std::process::{Command, Output};
use std::process::Output;

fn run_cli(workspace: &Path, user_root: &Path, home_root: &Path, args: &[&str]) -> Output {
let config_root = user_root.join("host-config");
Command::new(env!("CARGO_BIN_EXE_bitfun"))
bitfun_services_core::process_manager::create_command(env!("CARGO_BIN_EXE_bitfun"))
.args(args)
.current_dir(workspace)
.env_remove("BITFUN_USER_ROOT")
Expand Down
29 changes: 15 additions & 14 deletions src/apps/cli/tests/cli_command_contracts/plugin_source_cli.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use sha2::{Digest, Sha256};
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::process::Output;

const PLUGIN_SOURCE: &[u8] = br#"
import { type Plugin, tool } from "@opencode-ai/plugin"
Expand Down Expand Up @@ -42,7 +42,7 @@ fn write_package(workspace: &Path, source: &[u8], declared_hash: &str) {

fn run_cli(workspace: &Path, user_root: &Path, home_root: &Path, args: &[&str]) -> Output {
let config_root = user_root.join("host-config");
Command::new(env!("CARGO_BIN_EXE_bitfun"))
bitfun_services_core::process_manager::create_command(env!("CARGO_BIN_EXE_bitfun"))
.args(args)
.current_dir(workspace)
.env_remove("BITFUN_USER_ROOT")
Expand Down Expand Up @@ -120,18 +120,19 @@ fn plugin_source_cli_rejects_unavailable_product_paths() {
let config_root = temp.path().join("host-config");
std::fs::create_dir_all(&workspace).expect("create workspace");

let output = Command::new(env!("CARGO_BIN_EXE_bitfun"))
.args(["plugins", "list"])
.current_dir(&workspace)
.env_remove("BITFUN_USER_ROOT")
.env_remove("BITFUN_HOME")
.env_remove("BITFUN_E2E_USER_ROOT")
.env_remove("BITFUN_E2E_HOME")
.env("BITFUN_E2E_STORAGE_GUARD", "1")
.env("APPDATA", &config_root)
.env("XDG_CONFIG_HOME", &config_root)
.output()
.expect("run bitfun");
let output =
bitfun_services_core::process_manager::create_command(env!("CARGO_BIN_EXE_bitfun"))
.args(["plugins", "list"])
.current_dir(&workspace)
.env_remove("BITFUN_USER_ROOT")
.env_remove("BITFUN_HOME")
.env_remove("BITFUN_E2E_USER_ROOT")
.env_remove("BITFUN_E2E_HOME")
.env("BITFUN_E2E_STORAGE_GUARD", "1")
.env("APPDATA", &config_root)
.env("XDG_CONFIG_HOME", &config_root)
.output()
.expect("run bitfun");

assert!(!output.status.success());
assert!(stderr(&output).contains("Configuration error"));
Expand Down
Loading
Loading