Skip to content

Commit 8746e82

Browse files
Keep env/cwd on ClientOptions to avoid breaking out-of-proc transports
Reverts the source-breaking API changes the in-process transport work introduced for existing Stdio/Tcp consumers: - Restore `Transport::Stdio` as a unit variant and `Transport::Tcp` to `{ port, connection_token }` (drop the per-transport `env` field). Env stays on `ClientOptions::env`/`env_remove`, which still applies to the child process and is already rejected for `Transport::InProcess`. - Restore `ClientOptions::working_directory` to `PathBuf` (empty = unset, resolved to the process cwd at start). The InProcess guard now rejects a non-empty working_directory. - Un-deprecate `env`/`env_remove`/`with_env`/`with_env_remove` (their notes pointed at the removed transport-level env). `Transport::Default`, `Transport::InProcess`, and COPILOT_SDK_DEFAULT_CONNECTION are additive and unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 95ad333b-ef14-474c-9908-75ed3e21e06f
1 parent 7156088 commit 8746e82

10 files changed

Lines changed: 36 additions & 141 deletions

rust/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,9 @@ client.stop().await?;
7676
| ------------------- | --------------------------- | ----------------------------------------------------------------- |
7777
| `program` | `CliProgram` | `Resolve` (default: auto-detect) or `Path(PathBuf)` (explicit) |
7878
| `prefix_args` | `Vec<OsString>` | Args before `--server` (e.g. script path for node) |
79-
| `working_directory` | `Option<PathBuf>` | Working directory for CLI process |
80-
| `env` | `Vec<(OsString, OsString)>` | Deprecated; use the child-process transport's `env` option |
81-
| `env_remove` | `Vec<OsString>` | Deprecated; omit variables from the transport replacement env |
79+
| `working_directory` | `PathBuf` | Working directory for CLI process (empty = host process's cwd) |
80+
| `env` | `Vec<(OsString, OsString)>` | Environment variables for CLI process |
81+
| `env_remove` | `Vec<OsString>` | Environment variables to remove |
8282
| `extra_args` | `Vec<String>` | Extra CLI flags |
8383
| `transport` | `Transport` | `Default`, `Stdio`, `InProcess`, `Tcp`, or `External` |
8484

@@ -622,7 +622,7 @@ opts.telemetry = Some(telem);
622622
let client = Client::start(opts).await?;
623623
```
624624

625-
The SDK injects the appropriate environment variables (`COPILOT_OTEL_EXPORTER_TYPE`, `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_PROTOCOL`, ...) into the spawned CLI process. The SDK takes no OpenTelemetry dependency; the CLI itself owns the exporter pipeline. A transport-level replacement environment is applied first, followed by SDK-managed authentication and telemetry variables. Deprecated `ClientOptions::env` entries retain their previous override behavior.
625+
The SDK injects the appropriate environment variables (`COPILOT_OTEL_EXPORTER_TYPE`, `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_PROTOCOL`, ...) into the spawned CLI process. The SDK takes no OpenTelemetry dependency; the CLI itself owns the exporter pipeline. Caller-supplied `ClientOptions::env` entries override telemetry-injected values.
626626

627627
### Progress Reporting (`send_and_wait`)
628628

rust/src/lib.rs

Lines changed: 30 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,7 @@ pub enum Transport {
121121
#[default]
122122
Default,
123123
/// Communicate over stdin/stdout pipes (default).
124-
Stdio {
125-
/// Environment passed to the child process, replacing its inherited
126-
/// environment. SDK-managed variables are applied afterward.
127-
env: Option<Vec<(OsString, OsString)>>,
128-
},
124+
Stdio,
129125
/// Host the runtime in-process over FFI (no child process).
130126
///
131127
/// Loads the native runtime library next to the resolved CLI entrypoint
@@ -148,9 +144,6 @@ pub enum Transport {
148144
/// the CLI, the SDK auto-generates a 128-bit hex token so the
149145
/// loopback listener is safe by default.
150146
connection_token: Option<String>,
151-
/// Environment passed to the child process, replacing its inherited
152-
/// environment. SDK-managed variables are applied afterward.
153-
env: Option<Vec<(OsString, OsString)>>,
154147
},
155148
/// Connect to an already-running CLI server (no process spawning).
156149
External {
@@ -240,20 +233,11 @@ pub struct ClientOptions {
240233
pub prefix_args: Vec<OsString>,
241234
/// Working directory for the CLI process.
242235
///
243-
/// `None` uses the host process's current directory. Setting this option is
244-
/// not supported with [`Transport::InProcess`].
245-
pub working_directory: Option<PathBuf>,
236+
/// Setting this option is not supported with [`Transport::InProcess`].
237+
pub working_directory: PathBuf,
246238
/// Environment variables set on the child process.
247-
#[deprecated(
248-
since = "0.1.0",
249-
note = "set `env` on `Transport::Stdio` or `Transport::Tcp` instead"
250-
)]
251239
pub env: Vec<(OsString, OsString)>,
252240
/// Environment variable names to remove from the child process.
253-
#[deprecated(
254-
since = "0.1.0",
255-
note = "use a transport-level replacement environment that omits these variables"
256-
)]
257241
pub env_remove: Vec<OsString>,
258242
/// Extra CLI flags appended after the transport-specific arguments.
259243
pub extra_args: Vec<String>,
@@ -362,7 +346,6 @@ pub struct ClientOptions {
362346
pub mode: ClientMode,
363347
}
364348

365-
#[allow(deprecated)]
366349
impl std::fmt::Debug for ClientOptions {
367350
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368351
f.debug_struct("ClientOptions")
@@ -627,13 +610,12 @@ impl TelemetryConfig {
627610
}
628611
}
629612

630-
#[allow(deprecated)]
631613
impl Default for ClientOptions {
632614
fn default() -> Self {
633615
Self {
634616
program: CliProgram::Resolve,
635617
prefix_args: Vec::new(),
636-
working_directory: None,
618+
working_directory: PathBuf::new(),
637619
env: Vec::new(),
638620
env_remove: Vec::new(),
639621
extra_args: Vec::new(),
@@ -656,7 +638,6 @@ impl Default for ClientOptions {
656638
}
657639
}
658640

659-
#[allow(deprecated)]
660641
impl ClientOptions {
661642
/// Construct a new [`ClientOptions`] with default values.
662643
///
@@ -695,15 +676,11 @@ impl ClientOptions {
695676

696677
/// Working directory for the CLI process.
697678
pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
698-
self.working_directory = Some(cwd.into());
679+
self.working_directory = cwd.into();
699680
self
700681
}
701682

702683
/// Environment variables to set on the child process.
703-
#[deprecated(
704-
since = "0.1.0",
705-
note = "set `env` on `Transport::Stdio` or `Transport::Tcp` instead"
706-
)]
707684
pub fn with_env<I, K, V>(mut self, env: I) -> Self
708685
where
709686
I: IntoIterator<Item = (K, V)>,
@@ -715,10 +692,6 @@ impl ClientOptions {
715692
}
716693

717694
/// Environment variable names to remove from the child process.
718-
#[deprecated(
719-
since = "0.1.0",
720-
note = "use a transport-level replacement environment that omits these variables"
721-
)]
722695
pub fn with_env_remove<I, S>(mut self, names: I) -> Self
723696
where
724697
I: IntoIterator<Item = S>,
@@ -910,7 +883,6 @@ fn generate_connection_token() -> String {
910883
const DEFAULT_CONNECTION_ENV_VAR: &str = "COPILOT_SDK_DEFAULT_CONNECTION";
911884

912885
/// Resolve a transport override from [`DEFAULT_CONNECTION_ENV_VAR`].
913-
#[allow(deprecated)]
914886
fn resolve_default_transport(options: &ClientOptions) -> Result<Transport> {
915887
let configured = options
916888
.env
@@ -926,10 +898,8 @@ fn resolve_default_transport(options: &ClientOptions) -> Result<Transport> {
926898

927899
fn resolve_default_transport_value(value: Option<&str>) -> Result<Transport> {
928900
match value {
929-
None => Ok(Transport::Stdio { env: None }),
930-
Some(v) if v.is_empty() || v.eq_ignore_ascii_case("stdio") => {
931-
Ok(Transport::Stdio { env: None })
932-
}
901+
None => Ok(Transport::Stdio),
902+
Some(v) if v.is_empty() || v.eq_ignore_ascii_case("stdio") => Ok(Transport::Stdio),
933903
Some(v) if v.eq_ignore_ascii_case("inprocess") => Ok(Transport::InProcess),
934904
Some(v) => Err(Error::with_message(
935905
ErrorKind::InvalidConfig,
@@ -942,9 +912,8 @@ fn resolve_default_transport_value(value: Option<&str>) -> Result<Transport> {
942912
}
943913

944914
#[cfg(any(feature = "bundled-in-process", test))]
945-
#[allow(deprecated)]
946915
fn validate_inprocess_options(options: &ClientOptions) -> Result<()> {
947-
let unsupported = if options.working_directory.is_some() {
916+
let unsupported = if !options.working_directory.as_os_str().is_empty() {
948917
Some("working_directory")
949918
} else if !options.env.is_empty() {
950919
Some("env")
@@ -973,23 +942,6 @@ fn validate_inprocess_options(options: &ClientOptions) -> Result<()> {
973942
Ok(())
974943
}
975944

976-
#[allow(deprecated)]
977-
fn validate_transport_environment(options: &ClientOptions) -> Result<()> {
978-
let transport_env_is_set = matches!(
979-
&options.transport,
980-
Transport::Stdio { env: Some(_) } | Transport::Tcp { env: Some(_), .. }
981-
);
982-
if transport_env_is_set && (!options.env.is_empty() || !options.env_remove.is_empty()) {
983-
return Err(Error::with_message(
984-
ErrorKind::InvalidConfig,
985-
"set child-process environment variables via either the transport-level `env` \
986-
option or the deprecated ClientOptions::env/ClientOptions::env_remove options, \
987-
not both",
988-
));
989-
}
990-
Ok(())
991-
}
992-
993945
/// Connection to a GitHub Copilot CLI server (stdio, TCP, or external).
994946
///
995947
/// Cheaply cloneable — cloning shares the underlying connection.
@@ -1064,7 +1016,6 @@ impl Client {
10641016
if matches!(options.transport, Transport::Default) {
10651017
options.transport = resolve_default_transport(&options)?;
10661018
}
1067-
validate_transport_environment(&options)?;
10681019
if matches!(options.transport, Transport::InProcess) {
10691020
#[cfg(not(feature = "bundled-in-process"))]
10701021
{
@@ -1132,7 +1083,7 @@ impl Client {
11321083
// default.
11331084
let effective_connection_token: Option<String> = match &mut options.transport {
11341085
Transport::Default => unreachable!("default transport resolved above"),
1135-
Transport::Stdio { .. } | Transport::InProcess => None,
1086+
Transport::Stdio | Transport::InProcess => None,
11361087
Transport::Tcp {
11371088
connection_token, ..
11381089
} => Some(
@@ -1176,10 +1127,14 @@ impl Client {
11761127
resolved
11771128
}
11781129
};
1179-
let working_directory = options
1180-
.working_directory
1181-
.clone()
1182-
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
1130+
let working_directory = {
1131+
let cwd = options.working_directory.clone();
1132+
if cwd.as_os_str().is_empty() {
1133+
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
1134+
} else {
1135+
cwd
1136+
}
1137+
};
11831138

11841139
let client = match options.transport {
11851140
Transport::Default => unreachable!("default transport resolved above"),
@@ -1215,7 +1170,6 @@ impl Client {
12151170
Transport::Tcp {
12161171
port,
12171172
connection_token: _,
1218-
env: _,
12191173
} => {
12201174
let (mut child, actual_port) =
12211175
Self::spawn_tcp(&program, &options, &working_directory, port).await?;
@@ -1242,7 +1196,7 @@ impl Client {
12421196
options.mode,
12431197
)?
12441198
}
1245-
Transport::Stdio { env: _ } => {
1199+
Transport::Stdio => {
12461200
let mut child = Self::spawn_stdio(&program, &options, &working_directory)?;
12471201
let stdin = child.stdin.take().expect("stdin is piped");
12481202
let stdout = child.stdout.take().expect("stdout is piped");
@@ -1580,23 +1534,18 @@ impl Client {
15801534
});
15811535
}
15821536

1583-
#[allow(deprecated)]
15841537
fn build_command(program: &Path, options: &ClientOptions, working_directory: &Path) -> Command {
15851538
let mut command = Command::new(program);
15861539
for arg in &options.prefix_args {
15871540
command.arg(arg);
15881541
}
1589-
let transport_env = match &options.transport {
1590-
Transport::Stdio { env } | Transport::Tcp { env, .. } => env.as_ref(),
1591-
_ => None,
1592-
};
1593-
if let Some(env) = transport_env {
1594-
command.env_clear();
1595-
command.envs(env.iter().map(|(key, value)| (key, value)));
1596-
}
1542+
// Inject the SDK auth token first so explicit `env` / `env_remove`
1543+
// entries can override or strip it.
15971544
if let Some(token) = &options.github_token {
15981545
command.env("COPILOT_SDK_AUTH_TOKEN", token);
15991546
}
1547+
// Inject telemetry env vars before user env so callers can still
1548+
// override individual variables via `options.env`.
16001549
if let Some(telemetry) = &options.telemetry {
16011550
command.env("COPILOT_OTEL_ENABLED", "true");
16021551
if let Some(endpoint) = &telemetry.otlp_endpoint {
@@ -1636,13 +1585,11 @@ impl Client {
16361585
{
16371586
command.env("COPILOT_CONNECTION_TOKEN", token);
16381587
}
1639-
if transport_env.is_none() {
1640-
for (key, value) in &options.env {
1641-
command.env(key, value);
1642-
}
1643-
for key in &options.env_remove {
1644-
command.env_remove(key);
1645-
}
1588+
for (key, value) in &options.env {
1589+
command.env(key, value);
1590+
}
1591+
for key in &options.env_remove {
1592+
command.env_remove(key);
16461593
}
16471594
command
16481595
.current_dir(working_directory)
@@ -2486,7 +2433,6 @@ impl Drop for ClientInner {
24862433
}
24872434

24882435
#[cfg(test)]
2489-
#[allow(deprecated)]
24902436
mod tests {
24912437
use super::*;
24922438

@@ -2530,7 +2476,7 @@ mod tests {
25302476
.with_enable_remote_sessions(true);
25312477
assert!(matches!(opts.program, CliProgram::Path(_)));
25322478
assert_eq!(opts.prefix_args, vec![std::ffi::OsString::from("node")]);
2533-
assert_eq!(opts.working_directory, Some(PathBuf::from("/tmp")));
2479+
assert_eq!(opts.working_directory, PathBuf::from("/tmp"));
25342480
assert_eq!(
25352481
opts.env,
25362482
vec![(
@@ -2551,11 +2497,11 @@ mod tests {
25512497
fn default_transport_values_resolve_without_process_state() {
25522498
assert!(matches!(
25532499
resolve_default_transport_value(None).unwrap(),
2554-
Transport::Stdio { env: None }
2500+
Transport::Stdio
25552501
));
25562502
assert!(matches!(
25572503
resolve_default_transport_value(Some("stdio")).unwrap(),
2558-
Transport::Stdio { env: None }
2504+
Transport::Stdio
25592505
));
25602506
assert!(matches!(
25612507
resolve_default_transport_value(Some("INPROCESS")).unwrap(),
@@ -2593,49 +2539,6 @@ mod tests {
25932539
assert!(validate_inprocess_options(&options).is_ok());
25942540
}
25952541

2596-
#[test]
2597-
fn transport_environment_conflicts_with_legacy_environment() {
2598-
let options = ClientOptions::new()
2599-
.with_transport(Transport::Stdio {
2600-
env: Some(vec![("NEW".into(), "value".into())]),
2601-
})
2602-
.with_env([("OLD", "value")]);
2603-
2604-
assert!(validate_transport_environment(&options).is_err());
2605-
}
2606-
2607-
#[test]
2608-
fn legacy_environment_remains_supported_without_transport_environment() {
2609-
let options = ClientOptions::new()
2610-
.with_transport(Transport::Stdio { env: None })
2611-
.with_env([("OLD", "value")])
2612-
.with_env_remove(["REMOVED"]);
2613-
2614-
assert!(validate_transport_environment(&options).is_ok());
2615-
}
2616-
2617-
#[test]
2618-
fn transport_environment_is_applied_before_sdk_variables() {
2619-
let options = ClientOptions::new()
2620-
.with_transport(Transport::Stdio {
2621-
env: Some(vec![
2622-
("CUSTOM".into(), "value".into()),
2623-
("COPILOT_SDK_AUTH_TOKEN".into(), "old".into()),
2624-
]),
2625-
})
2626-
.with_github_token("new");
2627-
let command = Client::build_command(Path::new("/bin/echo"), &options, Path::new("/tmp"));
2628-
2629-
assert_eq!(
2630-
env_value(&command, "CUSTOM"),
2631-
Some(std::ffi::OsStr::new("value"))
2632-
);
2633-
assert_eq!(
2634-
env_value(&command, "COPILOT_SDK_AUTH_TOKEN"),
2635-
Some(std::ffi::OsStr::new("new"))
2636-
);
2637-
}
2638-
26392542
#[cfg(not(feature = "bundled-in-process"))]
26402543
#[tokio::test]
26412544
async fn inprocess_requires_cargo_feature() {
@@ -2892,7 +2795,6 @@ mod tests {
28922795
let opts = ClientOptions::new().with_transport(Transport::Tcp {
28932796
port: 0,
28942797
connection_token: Some("secret-token".to_string()),
2895-
env: None,
28962798
});
28972799
let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp"));
28982800
assert_eq!(
@@ -2911,7 +2813,6 @@ mod tests {
29112813
.with_transport(Transport::Tcp {
29122814
port: 0,
29132815
connection_token: Some(String::new()),
2914-
env: None,
29152816
})
29162817
.with_program(CliProgram::Path(PathBuf::from("/bin/echo")));
29172818
let err = Client::start(opts).await.unwrap_err();

rust/tests/e2e/client.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ async fn should_start_ping_and_stop_tcp_client() {
3030
let client = Client::start(ctx.client_options_with_transport(Transport::Tcp {
3131
port: 0,
3232
connection_token: Some("tcp-e2e-token".to_string()),
33-
env: None,
3433
}))
3534
.await
3635
.expect("start TCP client");

rust/tests/e2e/client_options.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ impl FakeCli {
347347
])
348348
.with_github_token(token)
349349
.with_use_logged_in_user(false)
350-
.with_transport(Transport::Stdio { env: None })
350+
.with_transport(Transport::Stdio)
351351
}
352352

353353
fn path(&self, name: &str) -> PathBuf {

0 commit comments

Comments
 (0)