mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
Prompt for project trust in remote TUI workspaces (#39082)
## What changed - Query the remote app server for project config layers before starting a thread and show the trust prompt when the project has no existing decision. - Resolve relative remote working directories and repository-root trust targets, then persist accepted trust through `config/batchWrite` on the remote server. - Preserve existing trusted and untrusted decisions, including an untrusted repository that contains the requested working directory. - Exit when the remote trust prompt is declined and ignore repeated key events in the trust selector. ## Testing - Add coverage for remote trust detection, persistence, thread startup, existing decisions, nested untrusted projects, and rendering a remote Git subdirectory. GitOrigin-RevId: e5fba2ea23bad1fb28f01df522cadbe05fcbb942
This commit is contained in:
@@ -11,22 +11,33 @@ use codex_app_server_protocol::ConfigEdit;
|
||||
use codex_app_server_protocol::ConfigReadParams;
|
||||
use codex_app_server_protocol::ConfigReadResponse;
|
||||
use codex_app_server_protocol::ConfigWriteResponse;
|
||||
use codex_app_server_protocol::EnvironmentInfoParams;
|
||||
use codex_app_server_protocol::EnvironmentInfoResponse;
|
||||
use codex_app_server_protocol::MergeStrategy;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::SkillsConfigWriteParams;
|
||||
use codex_app_server_protocol::SkillsConfigWriteResponse;
|
||||
use codex_config::loader::project_trust_key;
|
||||
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
|
||||
use codex_features::FEATURES;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::LegacyAppPathString;
|
||||
use color_eyre::eyre::Result;
|
||||
use color_eyre::eyre::WrapErr;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::fmt::Display;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct RemoteProjectTrust {
|
||||
pub cwd: PathBuf,
|
||||
pub trust_target: PathBuf,
|
||||
}
|
||||
|
||||
pub(crate) fn replace_config_value(key_path: impl Into<String>, value: JsonValue) -> ConfigEdit {
|
||||
ConfigEdit {
|
||||
key_path: key_path.into(),
|
||||
@@ -187,6 +198,116 @@ pub(crate) async fn read_effective_config(
|
||||
.wrap_err("config/read failed in TUI")
|
||||
}
|
||||
|
||||
pub(crate) async fn read_remote_project_trust(
|
||||
request_handle: AppServerRequestHandle,
|
||||
cwd: &Path,
|
||||
) -> Result<Option<RemoteProjectTrust>> {
|
||||
let cwd_string = cwd.to_string_lossy().into_owned();
|
||||
let cwd = match LegacyAppPathString::from_string(cwd_string.clone()).to_inferred_path_uri() {
|
||||
Some(cwd) => cwd,
|
||||
None => {
|
||||
let request_id = RequestId::String(format!("tui-environment-info-{}", Uuid::new_v4()));
|
||||
let environment: EnvironmentInfoResponse = request_handle
|
||||
.request_typed(ClientRequest::EnvironmentInfo {
|
||||
request_id,
|
||||
params: EnvironmentInfoParams {
|
||||
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
},
|
||||
})
|
||||
.await
|
||||
.wrap_err("environment/info failed while resolving remote project trust")?;
|
||||
environment
|
||||
.cwd
|
||||
.ok_or_else(|| color_eyre::eyre::eyre!("remote app server did not provide a cwd"))?
|
||||
.join(&cwd_string)
|
||||
.wrap_err("failed to resolve the remote project directory")?
|
||||
}
|
||||
};
|
||||
let cwd_uri = cwd;
|
||||
let cwd = cwd_uri.inferred_native_path_string();
|
||||
let request_id = RequestId::String(format!("tui-project-trust-read-{}", Uuid::new_v4()));
|
||||
let response: JsonValue = request_handle
|
||||
.request_typed(ClientRequest::ConfigRead {
|
||||
request_id,
|
||||
params: ConfigReadParams {
|
||||
include_layers: true,
|
||||
cwd: Some(cwd.clone()),
|
||||
},
|
||||
})
|
||||
.await
|
||||
.wrap_err("config/read failed while checking remote project trust")?;
|
||||
let project_layers = response
|
||||
.get("layers")
|
||||
.and_then(JsonValue::as_array)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|layer| layer["name"]["type"] == "project")
|
||||
.collect::<Vec<_>>();
|
||||
let disabled_project = project_layers.iter().rev().find(|layer| {
|
||||
layer
|
||||
.get("disabledReason")
|
||||
.and_then(JsonValue::as_str)
|
||||
.is_some()
|
||||
});
|
||||
let disabled_reason = disabled_project
|
||||
.and_then(|layer| layer.get("disabledReason"))
|
||||
.and_then(JsonValue::as_str);
|
||||
let trust_target = disabled_reason
|
||||
.and_then(|reason| reason.split_once(", add "))
|
||||
.and_then(|(_, reason)| reason.rsplit_once(" as a trusted project in "))
|
||||
.map(|(trust_target, _)| trust_target)
|
||||
.or_else(|| {
|
||||
disabled_project
|
||||
.and_then(|layer| layer["name"]["dotCodexFolder"].as_str())
|
||||
.and_then(|path| {
|
||||
path.strip_suffix("/.codex")
|
||||
.or_else(|| path.strip_suffix("\\.codex"))
|
||||
})
|
||||
})
|
||||
.unwrap_or(&cwd);
|
||||
let projects = response["config"]["projects"].as_object();
|
||||
let has_trust_decision = projects
|
||||
.and_then(|projects| projects.get(trust_target))
|
||||
.and_then(|project| project.get("trust_level"))
|
||||
.and_then(JsonValue::as_str)
|
||||
.is_some_and(|level| matches!(level, "trusted" | "untrusted"));
|
||||
let explicitly_untrusted = disabled_reason.is_some_and(|reason| {
|
||||
projects.into_iter().flatten().any(|(path, project)| {
|
||||
project.get("trust_level").and_then(JsonValue::as_str) == Some("untrusted")
|
||||
&& reason
|
||||
.strip_prefix(path)
|
||||
.is_some_and(|suffix| suffix.starts_with(" is marked as untrusted"))
|
||||
})
|
||||
});
|
||||
if has_trust_decision
|
||||
|| explicitly_untrusted
|
||||
|| (disabled_project.is_none()
|
||||
&& project_layers
|
||||
.iter()
|
||||
.any(|layer| layer.get("disabledReason").is_none()))
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if project_layers.is_empty()
|
||||
&& projects.into_iter().flatten().any(|(path, project)| {
|
||||
project.get("trust_level").and_then(JsonValue::as_str) == Some("untrusted")
|
||||
&& LegacyAppPathString::from_string(path.clone())
|
||||
.to_inferred_path_uri()
|
||||
.is_some_and(|project_uri| cwd_uri.starts_with(&project_uri))
|
||||
})
|
||||
{
|
||||
return Err(color_eyre::eyre::eyre!(
|
||||
"remote project directory is inside an explicitly untrusted project; pass the repository root explicitly with --cd"
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Some(RemoteProjectTrust {
|
||||
cwd: PathBuf::from(&cwd),
|
||||
trust_target: PathBuf::from(trust_target),
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) async fn write_skill_enabled(
|
||||
request_handle: AppServerRequestHandle,
|
||||
path: AbsolutePathBuf,
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
use super::*;
|
||||
use crate::legacy_core::config::ConfigBuilder;
|
||||
use crate::legacy_core::config::ConfigOverrides;
|
||||
use codex_app_server_client::AppServerClient;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use color_eyre::eyre::WrapErr;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
@@ -23,6 +28,98 @@ fn trusted_project_edit_targets_project_trust_level() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_project_trust_guards_thread_start_and_preserves_repository_decisions() -> Result<()>
|
||||
{
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let codex_home = temp_dir.path().join("codex-home");
|
||||
let project_root = temp_dir
|
||||
.path()
|
||||
.join("project as a trusted project in sibling");
|
||||
let project_cwd = project_root.join("nested");
|
||||
std::fs::create_dir_all(&codex_home)?;
|
||||
std::fs::create_dir_all(&project_cwd)?;
|
||||
std::fs::create_dir(project_root.join(".git"))?;
|
||||
std::fs::create_dir(project_cwd.join(".codex"))?;
|
||||
let undecided_config = format!(
|
||||
"[{}]\n",
|
||||
trusted_project_edit(&project_root)
|
||||
.key_path
|
||||
.trim_end_matches(".trust_level")
|
||||
);
|
||||
std::fs::write(codex_home.join("config.toml"), &undecided_config)?;
|
||||
std::fs::write(
|
||||
project_cwd.join(".codex/config.toml"),
|
||||
"model_reasoning_effort = \"high\"\n",
|
||||
)?;
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.clone())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
cwd: Some(codex_home.clone()),
|
||||
..ConfigOverrides::default()
|
||||
})
|
||||
.build()
|
||||
.await?;
|
||||
let app_server =
|
||||
AppServerClient::InProcess(crate::tests::start_test_embedded_app_server(config).await?);
|
||||
let relative_cwd = pathdiff::diff_paths(&project_cwd, std::env::current_dir()?)
|
||||
.ok_or_else(|| color_eyre::eyre::eyre!("failed to calculate relative project path"))?;
|
||||
|
||||
assert_eq!(
|
||||
read_remote_project_trust(app_server.request_handle(), &relative_cwd).await?,
|
||||
Some(RemoteProjectTrust {
|
||||
cwd: project_cwd.clone(),
|
||||
trust_target: PathBuf::from(project_trust_key(&project_root)),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(codex_home.join("config.toml"))?,
|
||||
undecided_config
|
||||
);
|
||||
|
||||
write_trusted_project(app_server.request_handle(), &project_root).await?;
|
||||
let persisted_config: toml::Value =
|
||||
toml::from_str(&std::fs::read_to_string(codex_home.join("config.toml"))?)?;
|
||||
assert_eq!(
|
||||
persisted_config["projects"][project_trust_key(&project_root)]["trust_level"].as_str(),
|
||||
Some("trusted")
|
||||
);
|
||||
|
||||
let _: ThreadStartResponse = app_server
|
||||
.request_typed(ClientRequest::ThreadStart {
|
||||
request_id: RequestId::Integer(1),
|
||||
params: ThreadStartParams {
|
||||
cwd: Some(project_cwd.to_string_lossy().into_owned()),
|
||||
ephemeral: Some(true),
|
||||
..ThreadStartParams::default()
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(
|
||||
read_remote_project_trust(app_server.request_handle(), &project_cwd).await?,
|
||||
None
|
||||
);
|
||||
|
||||
let mut untrusted_project = trusted_project_edit(&project_root);
|
||||
untrusted_project.value = serde_json::json!("untrusted");
|
||||
write_config_batch(app_server.request_handle(), vec![untrusted_project]).await?;
|
||||
assert_eq!(
|
||||
read_remote_project_trust(app_server.request_handle(), &project_cwd).await?,
|
||||
None
|
||||
);
|
||||
|
||||
std::fs::remove_file(project_cwd.join(".codex/config.toml"))?;
|
||||
std::fs::remove_dir(project_cwd.join(".codex"))?;
|
||||
let canonical_project_cwd = PathBuf::from(project_trust_key(&project_root)).join("nested");
|
||||
let error = read_remote_project_trust(app_server.request_handle(), &canonical_project_cwd)
|
||||
.await
|
||||
.expect_err("an untrusted repository must not be overridden by its subdirectory");
|
||||
assert!(error.to_string().contains("explicitly untrusted project"));
|
||||
|
||||
app_server.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_config_error_preserves_server_validation_message() {
|
||||
let err = Err::<(), _>(color_eyre::eyre::eyre!(
|
||||
|
||||
@@ -1072,10 +1072,36 @@ async fn run_ratatui_app(
|
||||
}
|
||||
}
|
||||
}
|
||||
let remote_project_trust =
|
||||
if uses_remote_workspace && let Some(remote_cwd) = remote_cwd_override.as_deref() {
|
||||
match startup_draft
|
||||
.run_until(
|
||||
&mut tui,
|
||||
config_update::read_remote_project_trust(
|
||||
app_server_session.request_handle(),
|
||||
remote_cwd,
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(remote_project_trust)) => remote_project_trust,
|
||||
Ok(Err(err)) => {
|
||||
shutdown_startup_session(Some(app_server_session), &mut terminal_restore_guard)
|
||||
.await;
|
||||
return Err(err);
|
||||
}
|
||||
Err(err) => {
|
||||
shutdown_startup_session(Some(app_server_session), &mut terminal_restore_guard)
|
||||
.await;
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut app_server = Some(app_server_session);
|
||||
|
||||
let should_show_trust_screen_flag =
|
||||
!uses_remote_workspace && should_show_trust_screen(&initial_config);
|
||||
let should_show_trust_screen_flag = remote_project_trust.is_some()
|
||||
|| (!uses_remote_workspace && should_show_trust_screen(&initial_config));
|
||||
#[cfg(target_os = "windows")]
|
||||
let mut trust_decision_was_made = false;
|
||||
let startup_model_provider = initial_config.model_provider_id.clone();
|
||||
@@ -1117,6 +1143,7 @@ async fn run_ratatui_app(
|
||||
OnboardingScreenArgs {
|
||||
show_login_screen,
|
||||
show_trust_screen: should_show_trust_screen_flag,
|
||||
remote_project_trust,
|
||||
login_status,
|
||||
app_server_request_handle: app_server
|
||||
.as_ref()
|
||||
@@ -1151,7 +1178,8 @@ async fn run_ratatui_app(
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
trust_decision_was_made = onboarding_result.directory_trust_persisted;
|
||||
trust_decision_was_made =
|
||||
!uses_remote_workspace && onboarding_result.directory_trust_persisted;
|
||||
}
|
||||
let reloaded_config = startup_draft
|
||||
.run_until(&mut tui, async {
|
||||
@@ -1168,8 +1196,8 @@ async fn run_ratatui_app(
|
||||
|
||||
// Reload config when persisted trust or auth changes alter the current process.
|
||||
Ok::<_, std::io::Error>(
|
||||
if onboarding_result.directory_trust_persisted
|
||||
|| (show_login_screen && !uses_remote_workspace)
|
||||
if !uses_remote_workspace
|
||||
&& (onboarding_result.directory_trust_persisted || show_login_screen)
|
||||
{
|
||||
load_config_or_exit(
|
||||
cli_kv_overrides.clone(),
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
|
||||
use codex_app_server_client::AppServerEvent;
|
||||
use codex_app_server_client::AppServerRequestHandle;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::ConfigBatchWriteParams;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
||||
@@ -32,7 +35,9 @@ use codex_protocol::config_types::ForcedLoginMethod;
|
||||
|
||||
use crate::LoginStatus;
|
||||
use crate::app_server_session::AppServerSession;
|
||||
use crate::config_update::RemoteProjectTrust;
|
||||
use crate::config_update::format_config_error;
|
||||
use crate::config_update::replace_config_value;
|
||||
use crate::config_update::write_trusted_project;
|
||||
use crate::key_hint::KeyBindingListExt;
|
||||
use crate::legacy_core::config::Config;
|
||||
@@ -47,8 +52,10 @@ use crate::tui::FrameRequester;
|
||||
use crate::tui::Tui;
|
||||
use crate::tui::TuiEvent;
|
||||
use color_eyre::eyre::Result;
|
||||
use color_eyre::eyre::WrapErr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum Step {
|
||||
@@ -76,12 +83,14 @@ pub(crate) trait StepStateProvider {
|
||||
pub(crate) struct OnboardingScreen {
|
||||
request_frame: FrameRequester,
|
||||
steps: Vec<Step>,
|
||||
remote_trust_key: Option<String>,
|
||||
is_done: bool,
|
||||
should_exit: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct OnboardingScreenArgs {
|
||||
pub show_trust_screen: bool,
|
||||
pub remote_project_trust: Option<RemoteProjectTrust>,
|
||||
pub show_login_screen: bool,
|
||||
pub login_status: LoginStatus,
|
||||
pub app_server_request_handle: Option<AppServerRequestHandle>,
|
||||
@@ -105,12 +114,16 @@ impl OnboardingScreen {
|
||||
pub(crate) async fn new(tui: &mut Tui, args: OnboardingScreenArgs) -> Self {
|
||||
let OnboardingScreenArgs {
|
||||
show_trust_screen,
|
||||
remote_project_trust,
|
||||
show_login_screen,
|
||||
login_status,
|
||||
app_server_request_handle,
|
||||
config,
|
||||
} = args;
|
||||
let cwd = config.cwd.to_path_buf();
|
||||
let remote_trust_key = remote_project_trust
|
||||
.as_ref()
|
||||
.map(|project| project.trust_target.to_string_lossy().into_owned());
|
||||
let auth_config = config.auth_config();
|
||||
let mut steps: Vec<Step> = Vec::new();
|
||||
steps.push(Step::Welcome(WelcomeWidget::new(
|
||||
@@ -142,16 +155,23 @@ impl OnboardingScreen {
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
let show_windows_create_sandbox_hint =
|
||||
crate::windows_sandbox::level_from_config(&config) == WindowsSandboxLevel::Disabled;
|
||||
let show_windows_create_sandbox_hint = remote_project_trust.is_none()
|
||||
&& crate::windows_sandbox::level_from_config(&config) == WindowsSandboxLevel::Disabled;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let show_windows_create_sandbox_hint = false;
|
||||
let highlighted = TrustDirectorySelection::Trust;
|
||||
if show_trust_screen {
|
||||
let trust_target = resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &config.cwd)
|
||||
.await
|
||||
.map(Into::into)
|
||||
.unwrap_or_else(|| cwd.clone());
|
||||
let (cwd, trust_target) = match remote_project_trust {
|
||||
Some(RemoteProjectTrust { cwd, trust_target }) => (cwd, trust_target),
|
||||
None => {
|
||||
let trust_target =
|
||||
resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &config.cwd)
|
||||
.await
|
||||
.map(Into::into)
|
||||
.unwrap_or_else(|| cwd.clone());
|
||||
(cwd, trust_target)
|
||||
}
|
||||
};
|
||||
steps.push(Step::TrustDirectory(TrustDirectoryWidget {
|
||||
cwd,
|
||||
trust_target,
|
||||
@@ -165,6 +185,7 @@ impl OnboardingScreen {
|
||||
Self {
|
||||
request_frame: tui.frame_requester(),
|
||||
steps,
|
||||
remote_trust_key,
|
||||
is_done: false,
|
||||
should_exit: false,
|
||||
}
|
||||
@@ -305,6 +326,8 @@ impl KeyboardHandler for OnboardingScreen {
|
||||
// If the user cancels the auth menu, exit the app rather than
|
||||
// leave the user at a prompt in an unauthed state.
|
||||
self.should_exit = true;
|
||||
} else if self.is_trust_step_active() {
|
||||
self.should_exit = true;
|
||||
}
|
||||
self.is_done = true;
|
||||
} else {
|
||||
@@ -635,11 +658,36 @@ async fn persist_selected_trust(
|
||||
return false;
|
||||
};
|
||||
|
||||
let result = match request_handle {
|
||||
Some(request_handle) => write_trusted_project(request_handle, &trust_target)
|
||||
let result = match (
|
||||
request_handle,
|
||||
onboarding_screen.remote_trust_key.as_deref(),
|
||||
) {
|
||||
(Some(request_handle), Some(project_key)) => {
|
||||
let project_key = project_key.replace('\\', "\\\\").replace('"', "\\\"");
|
||||
request_handle
|
||||
.request_typed::<serde_json::Value>(ClientRequest::ConfigBatchWrite {
|
||||
request_id: RequestId::String(format!(
|
||||
"tui-project-trust-write-{}",
|
||||
Uuid::new_v4()
|
||||
)),
|
||||
params: ConfigBatchWriteParams {
|
||||
edits: vec![replace_config_value(
|
||||
format!("projects.\"{project_key}\".trust_level"),
|
||||
serde_json::json!("trusted"),
|
||||
)],
|
||||
file_path: None,
|
||||
expected_version: None,
|
||||
reload_user_config: true,
|
||||
},
|
||||
})
|
||||
.await
|
||||
.map(|_| ())
|
||||
.wrap_err("config/batchWrite failed while persisting remote project trust")
|
||||
}
|
||||
(Some(request_handle), None) => write_trusted_project(request_handle, &trust_target)
|
||||
.await
|
||||
.map(|_| ()),
|
||||
None => Err(color_eyre::eyre::eyre!("app server unavailable")),
|
||||
(None, _) => Err(color_eyre::eyre::eyre!("app server unavailable")),
|
||||
};
|
||||
|
||||
match result {
|
||||
@@ -742,6 +790,7 @@ mod tests {
|
||||
highlighted: TrustDirectorySelection::Trust,
|
||||
error: None,
|
||||
})],
|
||||
remote_trust_key: None,
|
||||
is_done: false,
|
||||
should_exit: false,
|
||||
};
|
||||
@@ -787,6 +836,8 @@ mod tests {
|
||||
panic!("trust step should remain present");
|
||||
};
|
||||
assert_eq!(widget.highlighted, TrustDirectorySelection::Quit);
|
||||
onboarding_screen.handle_key_event(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE));
|
||||
assert!(onboarding_screen.should_exit());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -802,6 +853,7 @@ mod tests {
|
||||
highlighted: TrustDirectorySelection::Trust,
|
||||
error: None,
|
||||
})],
|
||||
remote_trust_key: None,
|
||||
is_done: false,
|
||||
should_exit: false,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: tui/src/onboarding/trust_directory.rs
|
||||
expression: "terminal.backend().to_string().lines().map(str::trim_end).collect::<Vec<_>>().join(\"\\n\")"
|
||||
---
|
||||
> You are in /srv/remote/project/nested
|
||||
|
||||
Note: You’re in a subdirectory of a Git project. Trusting will apply
|
||||
to the repository root: /srv/remote/project
|
||||
|
||||
Do you trust the contents of this directory? Working with untrusted
|
||||
contents comes with higher risk of prompt injection. Trusting the
|
||||
directory allows project-local config, hooks, and exec policies to
|
||||
load.
|
||||
|
||||
› 1. Yes, continue
|
||||
2. No, quit
|
||||
|
||||
Press enter to continue
|
||||
@@ -127,7 +127,7 @@ impl WidgetRef for &TrustDirectoryWidget {
|
||||
|
||||
impl KeyboardHandler for TrustDirectoryWidget {
|
||||
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||||
if key_event.kind == KeyEventKind::Release {
|
||||
if key_event.kind != KeyEventKind::Press {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -222,6 +222,11 @@ mod tests {
|
||||
widget.handle_key_event(release);
|
||||
assert_eq!(widget.selection, None);
|
||||
|
||||
let repeat =
|
||||
KeyEvent::new_with_kind(KeyCode::Enter, KeyModifiers::NONE, KeyEventKind::Repeat);
|
||||
widget.handle_key_event(repeat);
|
||||
assert_eq!(widget.selection, None);
|
||||
|
||||
let press = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
|
||||
widget.handle_key_event(press);
|
||||
assert!(widget.should_quit);
|
||||
@@ -240,6 +245,31 @@ mod tests {
|
||||
insta::assert_snapshot!(terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_snapshot_for_remote_git_subdirectory() {
|
||||
let widget = TrustDirectoryWidget {
|
||||
cwd: PathBuf::from("/srv/remote/project/nested"),
|
||||
trust_target: PathBuf::from("/srv/remote/project"),
|
||||
..widget(/*error*/ None)
|
||||
};
|
||||
|
||||
let mut terminal =
|
||||
Terminal::new(VT100Backend::new(/*width*/ 70, /*height*/ 18)).expect("terminal");
|
||||
terminal
|
||||
.draw(|f| (&widget).render_ref(f.area(), f.buffer_mut()))
|
||||
.expect("draw");
|
||||
|
||||
insta::assert_snapshot!(
|
||||
terminal
|
||||
.backend()
|
||||
.to_string()
|
||||
.lines()
|
||||
.map(str::trim_end)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_snapshot_for_trust_error() {
|
||||
let widget = widget(Some(
|
||||
|
||||
Reference in New Issue
Block a user