Skip to content
Closed
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
24 changes: 24 additions & 0 deletions docs/BUILDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,27 @@ cd apps/desktop-tauri && pnpm exec tsc --noEmit
cargo clippy --all-targets -- -D warnings
cargo fmt --all --check
```

## Stored process environments

A process environment carries API keys, tokens and cookies. Every struct, union or enum-variant
field that stores one uses `codexbar::process_environment::ProcessEnvironment`, including
optional maps and `(OsString, OsString)` pair lists. Its `Debug` output is
`ProcessEnvironment(N entries; redacted)`, so `{:?}`, `dbg!`, `tracing` field captures and
`assert_eq!` failures never print variable names or values. Wrapping an `Option` keeps `None`
distinct from an empty map, and equality still compares the original contents.

Deref, iteration, `get` and `insert` still return the original values for the child process,
so never log what they return. Function parameters and locals are transient and stay unwrapped.

The `process_environment::storage_guard` test scans `rust/src` and
`apps/desktop-tauri/src-tauri/src` for environment-named fields whose type is a string map or a
string pair list, through `Option`, `Box`, `Arc`, references, slices and local `type` aliases,
and fails on any that is not wrapped. Each entry in its `REVIEWED_EXCEPTIONS` list must match
exactly one declaration, so stale or duplicate exceptions fail too. The guard is a lexical
tripwire, not a type checker: generic parameters, differently named fields and explicit logging
of an unwrapped map still need code review.

```bash
cargo test -p codexbar process_environment
```
2 changes: 1 addition & 1 deletion rust/src/agent_sessions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,7 @@ impl LocalAgentSessionScanner {
let pi_input = pi_family::PiFamilyScanInput {
processes: &pi_processes,
cwd_by_pid: std::collections::HashMap::new(),
environment: pi_family::PiFamilySessionScanner::scan_environment(),
environment: pi_family::PiFamilySessionScanner::scan_environment().into(),
now,
host: host.to_string(),
config: self.config,
Expand Down
3 changes: 2 additions & 1 deletion rust/src/agent_sessions/pi_family/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use super::{
AgentSessionProvider, AgentSessionSource, AgentSessionState, AgentSessionWorkspace,
PiSessionDialect, SessionScanConfig,
};
use crate::process_environment::ProcessEnvironment;

/// Bounded transcript prefix used for header/first-title parsing (upstream
/// `maximumReadSize`).
Expand Down Expand Up @@ -132,7 +133,7 @@ pub struct PiFamilyScanInput<'a> {
/// rows result until cwd capture lands).
pub cwd_by_pid: CwdByPid,
/// Environment slice (HOME/USERPROFILE/PI_*/OMP_*).
pub environment: EnvMap,
pub environment: ProcessEnvironment<EnvMap>,
pub now: DateTime<Utc>,
pub host: String,
pub config: SessionScanConfig,
Expand Down
2 changes: 1 addition & 1 deletion rust/src/agent_sessions/pi_family_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ mod pi_family_tests {
let input = PiFamilyScanInput {
processes,
cwd_by_pid,
environment,
environment: environment.into(),
now,
host: "fixture-host".to_string(),
config: SessionScanConfig::default(),
Expand Down
7 changes: 4 additions & 3 deletions rust/src/cli/tty_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
reason = "TTY runner types reserved for future interactive session management"
)]

use crate::process_environment::ProcessEnvironment;
use regex_lite::Regex;
use std::collections::HashMap;
use std::io::{Read, Write};
Expand Down Expand Up @@ -69,8 +70,8 @@ pub struct TtyCommandOptions {
pub stop_on_substrings: Vec<String>,
/// Settle time after stopping (default: 0.25s)
pub settle_after_stop_secs: f64,
/// Environment variables to set
pub env: HashMap<String, String>,
/// Environment variables to set (`Debug` renders only the entry count)
pub env: ProcessEnvironment<HashMap<String, String>>,
}

impl Default for TtyCommandOptions {
Expand All @@ -90,7 +91,7 @@ impl Default for TtyCommandOptions {
stop_on_url: false,
stop_on_substrings: Vec::new(),
settle_after_stop_secs: 0.25,
env: HashMap::new(),
env: ProcessEnvironment::default(),
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions rust/src/core/token_accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! Supports parallel fetching and account switching.

use crate::core::ProviderId;
use crate::process_environment::ProcessEnvironment;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
Expand Down Expand Up @@ -752,8 +753,8 @@ pub struct TokenAccountOverride {
pub provider: ProviderId,
/// The account being used
pub account: TokenAccount,
/// Environment variables to set
pub env_override: Option<HashMap<String, String>>,
/// Environment variables to set (`Debug` renders only the entry count)
pub env_override: ProcessEnvironment<Option<HashMap<String, String>>>,
/// Cookie header to use
pub cookie_header: Option<String>,
}
Expand All @@ -774,7 +775,7 @@ impl TokenAccountOverride {
Self {
provider,
account,
env_override,
env_override: env_override.into(),
cookie_header,
}
}
Expand Down
6 changes: 4 additions & 2 deletions rust/src/host/command_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ use std::process::{Child, ChildStderr, ChildStdout, Command, Stdio};
use std::sync::mpsc::{self, RecvTimeoutError};
use std::time::{Duration, Instant};

use crate::process_environment::ProcessEnvironment;

/// Command runner configuration
#[derive(Debug, Clone)]
pub struct CommandOptions {
Expand Down Expand Up @@ -110,7 +112,7 @@ impl std::error::Error for CommandError {}
/// Command runner for executing CLI tools
pub struct CommandRunner {
/// Environment variables to add.
env_additions: HashMap<String, String>,
env_additions: ProcessEnvironment<HashMap<String, String>>,
/// Whether the child inherits the ambient process environment.
inherit_environment: bool,
}
Expand All @@ -122,7 +124,7 @@ impl CommandRunner {

pub fn new() -> Self {
Self {
env_additions: HashMap::new(),
env_additions: ProcessEnvironment::default(),
inherit_environment: true,
}
}
Expand Down
1 change: 1 addition & 0 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub mod login;
#[cfg(windows)]
pub mod managed_process;
pub mod notifications;
pub mod process_environment;
pub mod providers;
pub mod secure_file;
pub mod settings;
Expand Down
10 changes: 6 additions & 4 deletions rust/src/managed_process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ use windows::Win32::System::Threading::{
};
use windows::core::{PCWSTR, PWSTR};

use crate::process_environment::ProcessEnvironment;

/// Maximum number of terminal cursor-position replies sent to one child.
const MAX_CURSOR_REPLIES: usize = 32;

Expand All @@ -69,8 +71,8 @@ pub struct ManagedProcessConfig {
pub program: PathBuf,
/// Arguments passed to the executable.
pub args: Vec<OsString>,
/// Additional environment variables for the child.
pub env: Vec<(OsString, OsString)>,
/// Additional environment variables for the child (`Debug` renders only the entry count).
pub env: ProcessEnvironment<Vec<(OsString, OsString)>>,
/// Working directory, when the provider wants to pin one.
pub cwd: Option<PathBuf>,
/// PTY geometry.
Expand Down Expand Up @@ -841,7 +843,7 @@ mod tests {
OsString::from("-Command"),
OsString::from("Start-Sleep -Seconds 30"),
],
env: Vec::new(),
env: ProcessEnvironment::default(),
cwd: None,
pty_rows: 30,
pty_cols: 120,
Expand Down Expand Up @@ -1082,7 +1084,7 @@ mod tests {
OsString::from("-Command"),
OsString::from(script),
],
env: Vec::new(),
env: ProcessEnvironment::default(),
cwd: None,
pty_rows: 30,
pty_cols: 120,
Expand Down
121 changes: 121 additions & 0 deletions rust/src/process_environment.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//! Count-only diagnostics for stored process environments (upstream 0.70.0, #4106).
//!
//! A process environment carries API keys, tokens and cookies. When a struct that stores one
//! derives `Debug`, `{:?}`, `dbg!`, a `tracing` field capture or an `assert_eq!` failure prints
//! every value. [`ProcessEnvironment`] keeps the original values for the child process and
//! renders only how many entries it holds.
//!
//! Every stored environment uses the wrapper. The `storage_guard` test scans the shipped Rust
//! sources and fails when an environment-named field holds a plain string map or string pair
//! list.

use std::collections::{BTreeMap, HashMap};
use std::fmt;
use std::ops::{Deref, DerefMut};

/// A stored process environment whose `Debug` output is `ProcessEnvironment(N entries; redacted)`.
///
/// Explicit access (deref, iteration, `get`, `insert`) still returns the original values for
/// subprocess use, so never log what it returns. Wrapping an `Option` keeps `None` distinct from
/// an empty map, and equality compares the original contents.
#[derive(Clone, Default, PartialEq, Eq)]
pub struct ProcessEnvironment<T>(T);

impl<T> ProcessEnvironment<T> {
/// Wraps an environment for storage.
pub const fn new(value: T) -> Self {
Self(value)
}

/// Returns the original environment, for handing to a child process.
pub fn into_inner(self) -> T {
self.0
}
}

impl<T> From<T> for ProcessEnvironment<T> {
fn from(value: T) -> Self {
Self(value)
}
}

impl<T> Deref for ProcessEnvironment<T> {
type Target = T;

fn deref(&self) -> &T {
&self.0
}
}

impl<T> DerefMut for ProcessEnvironment<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.0
}
}

impl<T: EnvironmentEntries> fmt::Debug for ProcessEnvironment<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"ProcessEnvironment({} entries; redacted)",
self.0.environment_entries()
)
}
}

impl<T: IntoIterator> IntoIterator for ProcessEnvironment<T> {
type Item = T::Item;
type IntoIter = T::IntoIter;

fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}

impl<'a, T> IntoIterator for &'a ProcessEnvironment<T>
where
&'a T: IntoIterator,
{
type Item = <&'a T as IntoIterator>::Item;
type IntoIter = <&'a T as IntoIterator>::IntoIter;

fn into_iter(self) -> Self::IntoIter {
(&self.0).into_iter()
}
}

/// The entry count of a stored environment, the only detail [`ProcessEnvironment`] renders.
pub trait EnvironmentEntries {
/// Number of variables held; an absent environment counts as zero.
fn environment_entries(&self) -> usize;
}

impl<K, V, S> EnvironmentEntries for HashMap<K, V, S> {
fn environment_entries(&self) -> usize {
self.len()
}
}

impl<K, V> EnvironmentEntries for BTreeMap<K, V> {
fn environment_entries(&self) -> usize {
self.len()
}
}

impl<K, V> EnvironmentEntries for Vec<(K, V)> {
fn environment_entries(&self) -> usize {
self.len()
}
}

impl<T: EnvironmentEntries> EnvironmentEntries for Option<T> {
fn environment_entries(&self) -> usize {
self.as_ref()
.map_or(0, EnvironmentEntries::environment_entries)
}
}

#[cfg(test)]
mod storage_guard;
#[cfg(test)]
mod tests;
Loading