Redact secrets from app-server command execution items (#36893)

## Why

Client-facing command execution items can include recognizable secrets in the
rendered command or parsed command actions.

## What changed

- Redact secrets in `commandExecution.command` and `commandActions` for live
  items, completed items, and replayed thread history.
- Keep command approval requests backed by the original executable command and
  parsed actions while using the redacted representation for display items.
- Document that execution item commands are redacted display values rather than
  executable commands.

## Testing

- Cover redaction for command conversion, parsed search actions, legacy replay,
  approval flows, completion notifications, and rejected commands.

GitOrigin-RevId: 2ad056f21882bf5166182ad1a4ff0bf6471d9c0e
This commit is contained in:
Alec Barber
2026-08-04 13:16:58 +00:00
committed by copyberry
parent 6d4d9442c7
commit fcf636a41d
10 changed files with 238 additions and 70 deletions
+1
View File
@@ -2141,6 +2141,7 @@ dependencies = [
"codex-experimental-api-macros",
"codex-extension-items",
"codex-protocol",
"codex-secrets",
"codex-shell-command",
"codex-utils-absolute-path",
"codex-utils-cargo-bin",
+1
View File
@@ -18,6 +18,7 @@ codex-experimental-api-macros = { workspace = true }
codex-app-server-protocol-noop-macros = { workspace = true }
codex-extension-items = { workspace = true }
codex-protocol = { workspace = true }
codex-secrets = { workspace = true }
codex-shell-command = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-path-uri = { workspace = true }
@@ -35,6 +35,7 @@ use codex_protocol::protocol::PatchApplyEndEvent;
use codex_protocol::protocol::ReviewOutputEvent;
use codex_protocol::review_format::REVIEW_FALLBACK_MESSAGE;
use codex_protocol::review_format::render_review_output_text;
use codex_secrets::redact_secrets;
use codex_shell_command::parse_command::parse_command;
use codex_shell_command::parse_command::shlex_join;
use codex_utils_path_uri::PathUri;
@@ -42,6 +43,24 @@ use std::collections::HashMap;
use std::path::PathBuf;
use tracing::warn;
/// Client-facing command and parsed actions projected from a raw command.
pub struct CommandExecutionPresentation {
/// Shell-formatted command with recognizable secrets redacted.
pub command: String,
/// Parsed command actions with recognizable secrets redacted.
pub command_actions: Vec<CommandAction>,
}
impl CommandExecutionPresentation {
/// Projects a raw command into its client-facing representation.
pub fn from_raw(command: &[String], parsed_cmd: &[ParsedCommand], cwd: &PathUri) -> Self {
Self {
command: redact_secrets(shlex_join(command)),
command_actions: command_actions_for_path_uri(parsed_cmd, cwd),
}
}
}
pub(crate) fn review_output_text(output: Option<&ReviewOutputEvent>) -> String {
output
.map(render_review_output_text)
@@ -75,17 +94,18 @@ pub fn build_file_change_end_item(payload: &PatchApplyEndEvent) -> ThreadItem {
}
pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> ThreadItem {
let command_actions = command_actions_for_path_uri(&payload.parsed_cmd, &payload.cwd);
let presentation =
CommandExecutionPresentation::from_raw(&payload.command, &payload.parsed_cmd, &payload.cwd);
ThreadItem::CommandExecution {
id: payload.call_id.clone(),
plugin_id: payload.plugin_id.clone(),
script_path: payload.script_path.clone(),
command: shlex_join(&payload.command),
command: presentation.command,
cwd: payload.cwd.clone().into(),
process_id: payload.process_id.clone(),
source: payload.source.into(),
status: CommandExecutionStatus::InProgress,
command_actions,
command_actions: presentation.command_actions,
aggregated_output: None,
exit_code: None,
duration_ms: None,
@@ -99,28 +119,26 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread
Some(payload.aggregated_output.clone())
};
let duration_ms = i64::try_from(payload.duration.as_millis()).unwrap_or(i64::MAX);
let command_actions = command_actions_for_path_uri(&payload.parsed_cmd, &payload.cwd);
let presentation =
CommandExecutionPresentation::from_raw(&payload.command, &payload.parsed_cmd, &payload.cwd);
ThreadItem::CommandExecution {
id: payload.call_id.clone(),
plugin_id: payload.plugin_id.clone(),
script_path: payload.script_path.clone(),
command: shlex_join(&payload.command),
command: presentation.command,
cwd: payload.cwd.clone().into(),
process_id: payload.process_id.clone(),
source: payload.source.into(),
status: (&payload.status).into(),
command_actions,
command_actions: presentation.command_actions,
aggregated_output,
exit_code: Some(payload.exit_code),
duration_ms: Some(duration_ms),
}
}
pub(crate) fn command_actions_for_path_uri(
parsed_cmd: &[ParsedCommand],
cwd: &PathUri,
) -> Vec<CommandAction> {
fn command_actions_for_path_uri(parsed_cmd: &[ParsedCommand], cwd: &PathUri) -> Vec<CommandAction> {
parsed_cmd
.iter()
.cloned()
@@ -131,7 +149,7 @@ pub(crate) fn command_actions_for_path_uri(
// genuinely opaque cwd would require executor-native state unavailable here.
match cwd.join(path.to_string_lossy().as_ref()) {
Ok(path) => Some(CommandAction::Read {
command: cmd,
command: redact_secrets(cmd),
name,
path: path.into(),
}),
@@ -147,15 +165,18 @@ pub(crate) fn command_actions_for_path_uri(
}
}
}
ParsedCommand::ListFiles { cmd, path } => {
Some(CommandAction::ListFiles { command: cmd, path })
}
ParsedCommand::Search { cmd, query, path } => Some(CommandAction::Search {
command: cmd,
query,
ParsedCommand::ListFiles { cmd, path } => Some(CommandAction::ListFiles {
command: redact_secrets(cmd),
path,
}),
ParsedCommand::Unknown { cmd } => Some(CommandAction::Unknown { command: cmd }),
ParsedCommand::Search { cmd, query, path } => Some(CommandAction::Search {
command: redact_secrets(cmd),
query: query.map(redact_secrets),
path,
}),
ParsedCommand::Unknown { cmd } => Some(CommandAction::Unknown {
command: redact_secrets(cmd),
}),
})
.collect()
}
@@ -4,6 +4,7 @@ use serde_json::json;
#[test]
fn read_command_actions_preserve_native_and_foreign_paths() {
let api_key = "sk-abcdefghijklmnopqrstuvwxyz123456";
for (cwd_uri, relative_path, expected_path) in [
(
"file:///home/alice/repo",
@@ -38,6 +39,11 @@ fn read_command_actions_preserve_native_and_foreign_paths() {
cmd: "ls".to_string(),
path: Some("subdir".to_string()),
},
ParsedCommand::Search {
cmd: format!("rg {api_key}"),
query: Some(api_key.to_string()),
path: Some("src".to_string()),
},
ParsedCommand::Search {
cmd: "rg needle".to_string(),
query: Some("needle".to_string()),
@@ -60,6 +66,12 @@ fn read_command_actions_preserve_native_and_foreign_paths() {
"command": "ls",
"path": "subdir",
},
{
"type": "search",
"command": "rg [REDACTED_SECRET]",
"query": "[REDACTED_SECRET]",
"path": "src",
},
{
"type": "search",
"command": "rg needle",
@@ -1649,6 +1649,7 @@ mod tests {
use codex_protocol::protocol::CompactedItem;
use codex_protocol::protocol::DynamicToolCallResponseEvent;
use codex_protocol::protocol::EnteredReviewModeEvent;
use codex_protocol::protocol::ExecCommandBeginEvent;
use codex_protocol::protocol::ExecCommandEndEvent;
use codex_protocol::protocol::ExecCommandSource;
use codex_protocol::protocol::ExitedReviewModeEvent;
@@ -2111,19 +2112,28 @@ mod tests {
}
#[test]
fn preserves_command_plugin_id_across_legacy_upsert() {
fn preserves_command_plugin_id_and_redacts_secrets_across_legacy_upsert() {
let turn_id = "turn-1";
let thread_id = ThreadId::new();
let command = vec![
"git".to_string(),
"-c".to_string(),
"http.extraHeader=Authorization: Bearer example_synthetic_bearer_token_123456"
.to_string(),
"push".to_string(),
];
let parsed_cmd = vec![ParsedCommand::Unknown {
cmd: "git -c 'http.extraHeader=Authorization: Bearer example_synthetic_bearer_token_123456' push"
.to_string(),
}];
let command_item = CoreTurnItem::CommandExecution(CoreCommandExecutionItem {
id: "exec-1".to_string(),
plugin_id: Some("sample@openai-curated".to_string()),
script_path: Some("scripts/run.py".to_string()),
process_id: Some("pid-1".to_string()),
command: vec!["echo".to_string(), "hello world".to_string()],
command: command.clone(),
cwd: test_path_buf("/tmp").abs().into(),
parsed_cmd: vec![ParsedCommand::Unknown {
cmd: "echo hello world".to_string(),
}],
parsed_cmd: parsed_cmd.clone(),
source: ExecCommandSource::Agent,
interaction_input: None,
status: CoreCommandExecutionStatus::Completed,
@@ -2142,6 +2152,19 @@ mod tests {
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
call_id: "exec-1".to_string(),
plugin_id: Some("sample@openai-curated".to_string()),
script_path: Some("scripts/run.py".to_string()),
process_id: Some("pid-1".to_string()),
turn_id: turn_id.to_string(),
started_at_ms: 0,
command: command.clone(),
cwd: test_path_buf("/tmp").abs().into(),
parsed_cmd: parsed_cmd.clone(),
source: ExecCommandSource::Agent,
interaction_input: None,
}),
EventMsg::ItemCompleted(ItemCompletedEvent {
thread_id,
turn_id: turn_id.to_string(),
@@ -2156,11 +2179,9 @@ mod tests {
process_id: Some("pid-1".to_string()),
turn_id: turn_id.to_string(),
completed_at_ms: 1_000,
command: vec!["echo".to_string(), "hello world".to_string()],
command,
cwd: test_path_buf("/tmp").abs().into(),
parsed_cmd: vec![ParsedCommand::Unknown {
cmd: "echo hello world".to_string(),
}],
parsed_cmd,
source: ExecCommandSource::Agent,
interaction_input: None,
stdout: "hello world\n".to_string(),
@@ -2186,6 +2207,29 @@ mod tests {
.into_iter()
.map(RolloutItem::EventMsg)
.collect::<Vec<_>>();
assert_eq!(
build_turns_from_rollout_items(&items[..2])[0].items,
vec![ThreadItem::CommandExecution {
id: "exec-1".to_string(),
plugin_id: Some("sample@openai-curated".to_string()),
script_path: Some("scripts/run.py".to_string()),
command: "git -c 'http.extraHeader=Authorization: Bearer [REDACTED_SECRET]' push"
.to_string(),
cwd: test_path_buf("/tmp").abs().into(),
process_id: Some("pid-1".to_string()),
source: CommandExecutionSource::Agent,
status: CommandExecutionStatus::InProgress,
command_actions: vec![CommandAction::Unknown {
command:
"git -c 'http.extraHeader=Authorization: Bearer [REDACTED_SECRET]' push"
.to_string(),
}],
aggregated_output: None,
exit_code: None,
duration_ms: None,
}]
);
let turns = build_turns_from_rollout_items(&items);
assert_eq!(turns.len(), 1);
@@ -2195,13 +2239,16 @@ mod tests {
id: "exec-1".to_string(),
plugin_id: Some("sample@openai-curated".to_string()),
script_path: Some("scripts/run.py".to_string()),
command: "echo 'hello world'".to_string(),
command: "git -c 'http.extraHeader=Authorization: Bearer [REDACTED_SECRET]' push"
.to_string(),
cwd: test_path_buf("/tmp").abs().into(),
process_id: Some("pid-1".to_string()),
source: CommandExecutionSource::Agent,
status: CommandExecutionStatus::Completed,
command_actions: vec![CommandAction::Unknown {
command: "echo hello world".to_string(),
command:
"git -c 'http.extraHeader=Authorization: Bearer [REDACTED_SECRET]' push"
.to_string(),
}],
aggregated_output: Some("hello world\n".to_string()),
exit_code: Some(0),
@@ -10,7 +10,7 @@ use super::UserInput;
use super::shared::v2_enum_from_core;
use crate::JsonSchema;
use crate::TS;
use crate::protocol::item_builders::command_actions_for_path_uri;
use crate::protocol::item_builders::CommandExecutionPresentation;
use crate::protocol::item_builders::convert_patch_changes;
use crate::protocol::item_builders::review_output_text;
use codex_experimental_api_macros::ExperimentalApi;
@@ -43,7 +43,6 @@ use codex_protocol::protocol::GuardianUserAuthorization as CoreGuardianUserAutho
use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus;
use codex_protocol::protocol::ReviewDecision as CoreReviewDecision;
use codex_protocol::protocol::SubAgentActivityKind as CoreSubAgentActivityKind;
use codex_shell_command::parse_command::shlex_join;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::LegacyAppPathString;
use serde::Deserialize;
@@ -841,24 +840,31 @@ impl From<CoreTurnItem> for ThreadItem {
summary: reasoning.summary_text,
content: reasoning.raw_content,
},
CoreTurnItem::CommandExecution(command) => ThreadItem::CommandExecution {
id: command.id,
plugin_id: command.plugin_id,
script_path: command.script_path,
command: shlex_join(&command.command),
cwd: command.cwd.clone().into(),
process_id: command.process_id,
source: command.source.into(),
status: command.status.into(),
command_actions: command_actions_for_path_uri(&command.parsed_cmd, &command.cwd),
aggregated_output: command
.aggregated_output
.filter(|output| !output.is_empty()),
exit_code: command.exit_code,
duration_ms: command
.duration
.and_then(|duration| i64::try_from(duration.as_millis()).ok()),
},
CoreTurnItem::CommandExecution(command) => {
let presentation = CommandExecutionPresentation::from_raw(
&command.command,
&command.parsed_cmd,
&command.cwd,
);
ThreadItem::CommandExecution {
id: command.id,
plugin_id: command.plugin_id,
script_path: command.script_path,
command: presentation.command,
cwd: command.cwd.clone().into(),
process_id: command.process_id,
source: command.source.into(),
status: command.status.into(),
command_actions: presentation.command_actions,
aggregated_output: command
.aggregated_output
.filter(|output| !output.is_empty()),
exit_code: command.exit_code,
duration_ms: command
.duration
.and_then(|duration| i64::try_from(duration.as_millis()).ok()),
}
}
CoreTurnItem::DynamicToolCall(call) => ThreadItem::DynamicToolCall {
id: call.id,
namespace: call.namespace,
@@ -2927,10 +2927,19 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
plugin_id: Some("sample@openai-curated".to_string()),
script_path: Some("scripts/run.py".to_string()),
process_id: Some("pid-1".to_string()),
command: vec!["echo".to_string(), "done".to_string()],
command: vec![
"git".to_string(),
"-c".to_string(),
"http.extraHeader=Authorization: Bearer example_synthetic_bearer_token_123456"
.to_string(),
"-c".to_string(),
"http.extraHeader=X-Trace:example".to_string(),
"push".to_string(),
],
cwd: PathUri::from_abs_path(&test_path_buf("/tmp").abs()),
parsed_cmd: vec![codex_protocol::parse_command::ParsedCommand::Unknown {
cmd: "echo done".to_string(),
cmd: "git -c 'http.extraHeader=Authorization: Bearer example_synthetic_bearer_token_123456' -c http.extraHeader=X-Trace:example push"
.to_string(),
}],
source: CoreExecCommandSource::Agent,
interaction_input: None,
@@ -2949,13 +2958,15 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
id: "exec-1".to_string(),
plugin_id: Some("sample@openai-curated".to_string()),
script_path: Some("scripts/run.py".to_string()),
command: "echo done".to_string(),
command: "git -c 'http.extraHeader=Authorization: Bearer [REDACTED_SECRET]' -c 'http.extraHeader=X-Trace:example' push"
.to_string(),
cwd: LegacyAppPathString::from_abs_path(&test_path_buf("/tmp").abs()),
process_id: Some("pid-1".to_string()),
source: CommandExecutionSource::Agent,
status: CommandExecutionStatus::Completed,
command_actions: vec![CommandAction::Unknown {
command: "echo done".to_string(),
command: "git -c 'http.extraHeader=Authorization: Bearer [REDACTED_SECRET]' -c http.extraHeader=X-Trace:example push"
.to_string(),
}],
aggregated_output: Some("done\n".to_string()),
exit_code: Some(0),
+1 -1
View File
@@ -1516,7 +1516,7 @@ The app-server streams JSON-RPC notifications while a turn is running. Each turn
- `agentMessage` — `{id, text}` containing the accumulated agent reply.
- `plan` — `{id, text}` emitted for plan-mode turns; plan text can stream via `item/plan/delta` (experimental).
- `reasoning` — `{id, summary, content}` where `summary` holds streamed reasoning summaries (applicable for most OpenAI models) and `content` holds raw reasoning blocks (applicable for e.g. open source models).
- `commandExecution` — `{id, pluginId?, scriptPath?, command, cwd, status, commandActions, aggregatedOutput?, exitCode?, durationMs?}` for sandboxed commands; `pluginId` is present only for commands attributed to a trusted first-party plugin, newly attributed items also include `scriptPath` as a safe `/`-separated path relative to the trusted plugin root, older history may omit `scriptPath`, and `status` is `inProgress`, `completed`, `failed`, or `declined`.
- `commandExecution` — `{id, pluginId?, scriptPath?, command, cwd, status, commandActions, aggregatedOutput?, exitCode?, durationMs?}` for sandboxed commands; `pluginId` is present only for commands attributed to a trusted first-party plugin, newly attributed items also include `scriptPath` as a safe `/`-separated path relative to the trusted plugin root, older history may omit `scriptPath`, and `status` is `inProgress`, `completed`, `failed`, or `declined`. Ordinary execution items and their replay expose `command` and `commandActions` as redacted display values, not executable commands.
`cwd` and read `commandActions[].path` use the executor's native path convention, even when the app-server runs on a different operating system. For example, an app-server running on Linux can return `C:\repo\src\main.rs` for a Windows executor; clients must not interpret that path as local to the app-server.
- `fileChange` — `{id, changes, status}` describing proposed edits; `changes` list `{path, kind, diff}` and `status` is `inProgress`, `completed`, `failed`, or `declined`.
- `mcpToolCall` — `{id, server, tool, status, arguments, appContext, mcpAppResourceUri?, pluginId, readOnlyHint, result?, error?}` describing MCP calls; `appContext` is `{connectorId, linkId, resourceUri, appName, actionName}` for calls through a trusted MCP app, where `connectorId` identifies the connector that owns the tool, `linkId` identifies the app link, `resourceUri` points to the widget template, `appName` is the connector's display name, and `actionName` is the stable connector `Action.name`. `readOnlyHint` is `true` for read-only tools, `false` for write-capable tools, and `null` when the annotation is unavailable, including older rollout entries. The hint describes tool capability, not whether an invocation succeeded or performed a write; use `status`, `result`, and `error` to determine the execution outcome. `appName` and `actionName` may be null for older rollout entries. The top-level `mcpAppResourceUri` is deprecated and temporarily duplicated for client migration. `tool` identifies the raw MCP tool. `status` is `inProgress`, `completed`, or `failed`.
@@ -16,6 +16,7 @@ use codex_app_server_protocol::AdditionalPermissionProfile as V2AdditionalPermis
use codex_app_server_protocol::CodexErrorInfo as V2CodexErrorInfo;
use codex_app_server_protocol::CommandAction as V2ParsedCommand;
use codex_app_server_protocol::CommandExecutionApprovalDecision;
use codex_app_server_protocol::CommandExecutionPresentation;
use codex_app_server_protocol::CommandExecutionRequestApprovalParams;
use codex_app_server_protocol::CommandExecutionRequestApprovalResponse;
use codex_app_server_protocol::CommandExecutionSource;
@@ -620,13 +621,17 @@ pub(crate) async fn apply_bespoke_event_handling(
{
CommandExecutionApprovalPresentation::Network(network_approval_context)
} else {
let command_string = shlex_join(&command);
let command_presentation = CommandExecutionPresentation::from_raw(
&command,
&parsed_cmd,
&cwd.clone().into(),
);
let completion_item = CommandExecutionCompletionItem {
plugin_id,
script_path,
command: command_string,
command: command_presentation.command,
cwd: cwd.clone().into(),
command_actions: command_actions.clone(),
command_actions: command_presentation.command_actions,
};
CommandExecutionApprovalPresentation::Command(completion_item)
};
@@ -637,9 +642,9 @@ pub(crate) async fn apply_bespoke_event_handling(
}
CommandExecutionApprovalPresentation::Command(completion_item) => (
None,
Some(completion_item.command.clone()),
Some(shlex_join(&command)),
Some(completion_item.cwd.clone()),
Some(completion_item.command_actions.clone()),
Some(command_actions),
Some(completion_item),
),
};
@@ -2044,16 +2044,24 @@ async fn turn_start_exec_approval_toggle_v2() -> Result<()> {
let tmp = TempDir::new()?;
let codex_home = tmp.path().to_path_buf();
let bearer_token = "example_bearer_token_1234567890";
let first_shell_command = vec![
"python3".to_string(),
"-c".to_string(),
"import sys; print(sys.argv[1].endswith('7890'))".to_string(),
format!("Authorization: Bearer {bearer_token}"),
];
let expected_approval_command = format_with_current_shell_display(&shlex::try_join(
first_shell_command.iter().map(String::as_str),
)?);
let expected_display_command =
expected_approval_command.replace(bearer_token, "[REDACTED_SECRET]");
// Mock server: first turn requests a shell call (elicitation), then completes.
// Second turn same, but we'll set approval_policy=never to avoid elicitation.
let responses = vec![
create_shell_command_sse_response(
vec![
"python3".to_string(),
"-c".to_string(),
"print(42)".to_string(),
],
first_shell_command,
/*workdir*/ None,
Some(5000),
"call1",
@@ -2124,6 +2132,10 @@ async fn turn_start_exec_approval_toggle_v2() -> Result<()> {
params.environment_id.as_deref(),
Some(expected_environment_id.as_str())
);
assert_eq!(
params.command.as_deref(),
Some(expected_approval_command.as_str())
);
let resolved_request_id = request_id.clone();
// Approve and wait for task completion
@@ -2135,12 +2147,32 @@ async fn turn_start_exec_approval_toggle_v2() -> Result<()> {
)
.await?;
let mut saw_resolved = false;
let mut saw_completed_command = false;
loop {
let message = timeout(DEFAULT_READ_TIMEOUT, mcp.read_next_message()).await??;
let JSONRPCMessage::Notification(notification) = message else {
continue;
};
match notification.method.as_str() {
"item/completed" => {
let completed: ItemCompletedNotification =
serde_json::from_value(notification.params.expect("item/completed params"))?;
match completed.item {
ThreadItem::CommandExecution {
id,
command,
exit_code,
aggregated_output,
..
} if id == "call1" => {
assert_eq!(command, expected_display_command);
assert_eq!(exit_code, Some(0));
assert!(aggregated_output.is_some_and(|output| output.contains("True")));
saw_completed_command = true;
}
_ => {}
}
}
"serverRequest/resolved" => {
let resolved: ServerRequestResolvedNotification = serde_json::from_value(
notification
@@ -2154,6 +2186,7 @@ async fn turn_start_exec_approval_toggle_v2() -> Result<()> {
}
"turn/completed" => {
assert!(saw_resolved, "serverRequest/resolved should arrive first");
assert!(saw_completed_command, "expected completed command item");
break;
}
_ => {}
@@ -2227,14 +2260,22 @@ async fn run_turn_start_exec_approval_rejection_v2(
let tmp = TempDir::new()?;
let codex_home = tmp.path().to_path_buf();
let bearer_token = "example_bearer_token_1234567890";
let shell_command = vec![
"python3".to_string(),
"-c".to_string(),
"print(42)".to_string(),
format!("Authorization: Bearer {bearer_token}"),
];
let expected_approval_command = format_with_current_shell_display(&shlex::try_join(
shell_command.iter().map(String::as_str),
)?);
let expected_display_command =
expected_approval_command.replace(bearer_token, "[REDACTED_SECRET]");
let responses = vec![
create_shell_command_sse_response(
vec![
"python3".to_string(),
"-c".to_string(),
"print(42)".to_string(),
],
shell_command,
/*workdir*/ None,
Some(5000),
"call-decline",
@@ -2282,11 +2323,22 @@ async fn run_turn_start_exec_approval_rejection_v2(
}
})
.await??;
let ThreadItem::CommandExecution { id, status, .. } = started_command_execution else {
let ThreadItem::CommandExecution {
id,
status,
command,
command_actions,
..
} = started_command_execution
else {
unreachable!("loop ensures we break on command execution items");
};
assert_eq!(id, "call-decline");
assert_eq!(status, CommandExecutionStatus::InProgress);
assert_eq!(command, expected_display_command);
let displayed_actions = serde_json::to_string(&command_actions)?;
assert!(displayed_actions.contains("[REDACTED_SECRET]"));
assert!(!displayed_actions.contains(bearer_token));
let server_req = timeout(
DEFAULT_READ_TIMEOUT,
@@ -2299,6 +2351,12 @@ async fn run_turn_start_exec_approval_rejection_v2(
assert_eq!(params.item_id, "call-decline");
assert_eq!(params.thread_id, thread.id);
assert_eq!(params.turn_id, turn.id);
assert_eq!(
params.command.as_deref(),
Some(expected_approval_command.as_str())
);
let approval_actions = serde_json::to_string(&params.command_actions)?;
assert!(approval_actions.contains(bearer_token));
mcp.send_response(request_id, approval_response).await?;
@@ -2315,6 +2373,8 @@ async fn run_turn_start_exec_approval_rejection_v2(
let ThreadItem::CommandExecution {
id,
status,
command,
command_actions,
exit_code,
aggregated_output,
..
@@ -2324,6 +2384,10 @@ async fn run_turn_start_exec_approval_rejection_v2(
};
assert_eq!(id, "call-decline");
assert_eq!(status, expected_status);
assert_eq!(command, expected_display_command);
let displayed_actions = serde_json::to_string(&command_actions)?;
assert!(displayed_actions.contains("[REDACTED_SECRET]"));
assert!(!displayed_actions.contains(bearer_token));
assert!(exit_code.is_none());
assert!(aggregated_output.is_none());