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
51 changes: 46 additions & 5 deletions src/handlers/entry/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,17 @@ fn audit_binding_sources(
.collect()
}

/// The line an `agent run --remote` prints once the control plane has opened
/// its session. The binding values stay remote, so it counts bindings, not secrets.
fn remote_session_started_message(binding_count: usize) -> String {
let noun = if binding_count == 1 {
"binding"
} else {
"bindings"
};
format!("Remote agent session started ({binding_count} {noun})")
}

/// Returns the canonical source variables that must be removed from the child
/// environment before binding placeholders are installed.
fn remote_source_env_names(bindings: &[crate::api::remote_proxy::RemoteBinding]) -> Vec<String> {
Expand Down Expand Up @@ -1146,12 +1157,30 @@ pub async fn handle_cli(args: Cli) -> Exit {
.unwrap_or(crate::handlers::run::routing::RemoteMode::Credential),
),
};
let session = crate::api::remote_proxy::create_session(&session_request, raw_output)
.await
let mut session_spinner = (!silent).then(|| {
crate::utils::spinner::new_spinner(
"Starting remote agent session...",
crate::utils::spinner::Streams::Stderr,
)
});
let session_result =
crate::api::remote_proxy::create_session(&session_request, raw_output)
.await;
if let Some(mut spinner) = session_spinner.take() {
spinner.clear();
}
let session = session_result
// The Agent Proxy setup follows startup warnings. Keep its
// formatted API error visually distinct without changing
// output spacing for every other CLI command.
.map_err(|error| anyhow::anyhow!("\n{error}"))?;
if !silent {
eprintln!(
"{} {}",
"✓".green_if_tty_stderr(),
remote_session_started_message(bindings.len())
);
}
let token = session.session_token.clone();
let routing = match crate::handlers::run::routing::resolve_session_routing(
&session,
Expand Down Expand Up @@ -2353,9 +2382,9 @@ mod tests {
dependency_hooks_enabled, directory_profile_git_warning,
ensure_replacement_session_is_compatible, exit_for, infer_remote_agent_type,
is_bare_agent_hook, remote_bindings, remote_codex_command_with_mcp_binding_headers,
remote_session_rotation_delay_for, remote_session_transport_identity,
remote_source_env_names, secret_child_name, summarize_audit_events,
uses_local_dependency_hook_broker_mode,
remote_session_rotation_delay_for, remote_session_started_message,
remote_session_transport_identity, remote_source_env_names, secret_child_name,
summarize_audit_events, uses_local_dependency_hook_broker_mode,
};
use crate::api::remote_proxy::{RemoteBinding, RemoteBindingSource};
use crate::cmd::root::Cli;
Expand Down Expand Up @@ -2580,6 +2609,18 @@ mod tests {
}
}

#[test]
fn remote_session_started_message_counts_bindings() {
assert_eq!(
remote_session_started_message(1),
"Remote agent session started (1 binding)"
);
assert_eq!(
remote_session_started_message(3),
"Remote agent session started (3 bindings)"
);
}

#[test]
fn remote_bindings_compile_mixed_sources_with_value_templates() {
let binding = |from: Option<&str>| AgentBindingProfile {
Expand Down
43 changes: 29 additions & 14 deletions src/handlers/run/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -965,7 +965,7 @@ pub async fn handle_load_env_run(args: HandleRunArgs) -> anyhow::Result<()> {

let mut spinner = if !silent {
Some(crate::utils::spinner::new_spinner(
"Loading environment...",
loading_message(proxy_policy.is_some()),
Streams::Stderr,
))
} else {
Expand Down Expand Up @@ -1826,20 +1826,25 @@ fn apply_secret_bindings(
}
}

/// Combines the remote fallback with local overrides. Names outside the
/// profile's requested source set are discarded before a child can receive them.
/// The spinner `run` shows while loading. An agent run loads only its
/// profile's bindings, so it names those rather than a whole environment.
fn loading_message(agent_run: bool) -> &'static str {
if agent_run {
"Loading bindings..."
} else {
"Loading environment..."
}
}

/// The line `run` prints once the secrets are loaded. "Egress-only profile"
/// describes an agent profile, so plain `stashbase run` never says it.
fn loaded_message(secret_count: usize, agent_run: bool) -> String {
let label = if agent_run && secret_count == 0 {
"Egress-only profile"
} else {
"Environment loaded"
};
let noun = if secret_count == 1 {
"secret"
} else {
"secrets"
let (label, noun) = match (agent_run, secret_count) {
(true, 0) => ("Egress-only profile", "secrets"),
(true, 1) => ("Bindings loaded", "binding"),
(true, _) => ("Bindings loaded", "bindings"),
(false, 1) => ("Environment loaded", "secret"),
(false, _) => ("Environment loaded", "secrets"),
};
format!("{label} ({secret_count} {noun})")
}
Expand All @@ -1858,6 +1863,8 @@ fn needs_remote_fetch(only: &[String], remote_only: &[String], agent_run: bool)
}
}

/// Combines the remote fallback with local overrides. Names outside the
/// profile's requested source set are discarded before a child can receive them.
fn merge_remote_and_local_secrets(
remote: Vec<SecretWithoutComment>,
local: Vec<SecretWithoutComment>,
Expand Down Expand Up @@ -1898,7 +1905,7 @@ fn missing_secret_labels(
#[cfg(test)]
mod tests {
use super::{
apply_secret_bindings, load_run_secrets_from_file, loaded_message,
apply_secret_bindings, load_run_secrets_from_file, loaded_message, loading_message,
merge_remote_and_local_secrets, missing_secret_labels, needs_remote_fetch,
prepare_local_run_secrets,
};
Expand Down Expand Up @@ -2160,10 +2167,18 @@ mod tests {
assert_eq!(missing, ["GH_TOKEN (from GITHUB_TOKEN)"]);
}

#[test]
fn an_agent_run_loads_bindings_rather_than_an_environment() {
assert_eq!(loading_message(true), "Loading bindings...");
assert_eq!(loading_message(false), "Loading environment...");
}

#[test]
fn only_an_agent_run_without_secrets_is_called_egress_only() {
assert_eq!(loaded_message(0, true), "Egress-only profile (0 secrets)");
assert_eq!(loaded_message(1, true), "Environment loaded (1 secret)");
assert_eq!(loaded_message(1, true), "Bindings loaded (1 binding)");
assert_eq!(loaded_message(3, true), "Bindings loaded (3 bindings)");
assert_eq!(loaded_message(1, false), "Environment loaded (1 secret)");
assert_eq!(loaded_message(0, false), "Environment loaded (0 secrets)");
assert_eq!(loaded_message(10, false), "Environment loaded (10 secrets)");
}
Expand Down
Loading