mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
address feedback
This commit is contained in:
11
codex-rs/Cargo.lock
generated
11
codex-rs/Cargo.lock
generated
@@ -1042,6 +1042,7 @@ dependencies = [
|
||||
"env-flags",
|
||||
"escargot",
|
||||
"eventsource-stream",
|
||||
"fd-lock",
|
||||
"futures",
|
||||
"gethostname",
|
||||
"indexmap 2.10.0",
|
||||
@@ -1062,6 +1063,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serial_test",
|
||||
"sha1",
|
||||
"shellexpand",
|
||||
"shlex",
|
||||
"similar",
|
||||
"strum_macros 0.27.2",
|
||||
@@ -5342,6 +5344,15 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
||||
|
||||
[[package]]
|
||||
name = "shellexpand"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
|
||||
@@ -106,6 +106,7 @@ env_logger = "0.11.5"
|
||||
escargot = "0.5"
|
||||
eventsource-stream = "0.2.3"
|
||||
futures = { version = "0.3", default-features = false }
|
||||
fd-lock = "4.0.4"
|
||||
icu_decimal = "2.0.0"
|
||||
icu_locale_core = "2.0.0"
|
||||
ignore = "0.4.23"
|
||||
@@ -151,6 +152,7 @@ serde_with = "3.14"
|
||||
serial_test = "3.2.0"
|
||||
sha1 = "0.10.6"
|
||||
sha2 = "0.10"
|
||||
shellexpand = "3.1.0"
|
||||
shlex = "1.3.0"
|
||||
similar = "2.7.0"
|
||||
starlark = "0.13.0"
|
||||
|
||||
@@ -31,6 +31,7 @@ dirs = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
env-flags = { workspace = true }
|
||||
eventsource-stream = { workspace = true }
|
||||
fd-lock = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
gethostname = "0.4"
|
||||
indexmap = { workspace = true }
|
||||
@@ -41,6 +42,7 @@ portable-pty = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
regex-lite = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["json", "stream"] }
|
||||
shellexpand = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
sha1 = { workspace = true }
|
||||
|
||||
@@ -3,14 +3,15 @@ use crate::config_types::AdminAuditToml;
|
||||
use crate::config_types::AdminConfigToml;
|
||||
use crate::exec::ExecParams;
|
||||
use crate::exec::SandboxType;
|
||||
use crate::path_utils::expand_tilde;
|
||||
use crate::protocol::AskForApproval;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use fd_lock::FdLock;
|
||||
use gethostname::gethostname;
|
||||
use reqwest::Client;
|
||||
use serde::Serialize;
|
||||
use serde::ser::SerializeMap;
|
||||
use serde::ser::Serializer;
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::fs::OpenOptions;
|
||||
@@ -21,6 +22,9 @@ use std::path::PathBuf;
|
||||
use tokio::runtime::Handle;
|
||||
use tracing::warn;
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct AdminControls {
|
||||
pub danger: DangerControls,
|
||||
@@ -78,30 +82,25 @@ pub enum DangerAuditAction {
|
||||
Denied,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(tag = "audit_kind", rename_all = "snake_case")]
|
||||
pub enum AdminAuditPayload {
|
||||
Danger(DangerAuditDetails),
|
||||
Command(CommandAuditDetails),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DangerAuditDetails {
|
||||
pub action: DangerAuditAction,
|
||||
pub justification: Option<String>,
|
||||
pub requested_by: DangerRequestSource,
|
||||
pub sandbox: String,
|
||||
pub approval_policy: AskForApproval,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CommandAuditDetails {
|
||||
pub command: Vec<String>,
|
||||
pub command_cwd: String,
|
||||
pub cli_cwd: String,
|
||||
pub sandbox: String,
|
||||
pub sandbox_policy: String,
|
||||
pub escalated: bool,
|
||||
pub justification: Option<String>,
|
||||
Danger {
|
||||
action: DangerAuditAction,
|
||||
justification: Option<String>,
|
||||
requested_by: DangerRequestSource,
|
||||
sandbox_policy: SandboxPolicy,
|
||||
approval_policy: AskForApproval,
|
||||
},
|
||||
Command {
|
||||
command: Vec<String>,
|
||||
command_cwd: PathBuf,
|
||||
cli_cwd: PathBuf,
|
||||
sandbox_type: SandboxType,
|
||||
sandbox_policy: SandboxPolicy,
|
||||
escalated: bool,
|
||||
justification: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
@@ -149,22 +148,17 @@ impl AdminControls {
|
||||
}
|
||||
|
||||
pub fn take_pending_danger(&mut self) -> Option<DangerPending> {
|
||||
if let Some(index) = self
|
||||
.pending
|
||||
.iter()
|
||||
.position(|action| matches!(action, PendingAdminAction::Danger(_)))
|
||||
{
|
||||
match self.pending.remove(index) {
|
||||
self.pending
|
||||
.extract_if(|action| matches!(action, PendingAdminAction::Danger(_)))
|
||||
.next()
|
||||
.and_then(|action| match action {
|
||||
PendingAdminAction::Danger(pending) => Some(pending),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn peek_pending_danger(&self) -> Option<&DangerPending> {
|
||||
self.pending.first().map(|action| match action {
|
||||
PendingAdminAction::Danger(pending) => pending,
|
||||
self.pending.iter().find_map(|action| match action {
|
||||
PendingAdminAction::Danger(pending) => Some(pending),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -183,7 +177,7 @@ impl AdminAuditConfig {
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(expand_path(trimmed)?)
|
||||
Some(expand_tilde(trimmed)?)
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
@@ -214,30 +208,12 @@ impl AdminAuditConfig {
|
||||
impl AdminAuditPayload {
|
||||
pub fn kind(&self) -> AdminAuditEventKind {
|
||||
match self {
|
||||
AdminAuditPayload::Danger(_) => AdminAuditEventKind::Danger,
|
||||
AdminAuditPayload::Command(_) => AdminAuditEventKind::Command,
|
||||
AdminAuditPayload::Danger { .. } => AdminAuditEventKind::Danger,
|
||||
AdminAuditPayload::Command { .. } => AdminAuditEventKind::Command,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for AdminAuditPayload {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(Some(1))?;
|
||||
match self {
|
||||
AdminAuditPayload::Danger(details) => {
|
||||
map.serialize_entry("audit_danger", details)?;
|
||||
}
|
||||
AdminAuditPayload::Command(details) => {
|
||||
map.serialize_entry("audit_command", details)?;
|
||||
}
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl AdminAuditRecord {
|
||||
fn new(payload: AdminAuditPayload) -> Self {
|
||||
Self {
|
||||
@@ -257,14 +233,29 @@ pub fn log_admin_event(config: &AdminAuditConfig, payload: AdminAuditPayload) {
|
||||
|
||||
let record = AdminAuditRecord::new(payload);
|
||||
|
||||
if let Some(path) = &config.log_file
|
||||
&& let Err(err) = append_record_to_file(path, &record)
|
||||
{
|
||||
warn!(path = %path.display(), ?err, "failed to write admin audit event");
|
||||
if let Some(path) = &config.log_file {
|
||||
if let Err(err) = append_record_to_file(path, &record) {
|
||||
warn!(
|
||||
"failed to write admin audit event to {}: {err:?}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(endpoint) = &config.log_endpoint {
|
||||
send_record_to_endpoint(endpoint, record);
|
||||
if Handle::try_current().is_ok() {
|
||||
let endpoint = endpoint.clone();
|
||||
let record_for_endpoint = record.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = send_record_to_endpoint(&endpoint, record_for_endpoint).await {
|
||||
warn!("failed to post admin audit event to {endpoint}: {err:?}");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
warn!(
|
||||
"admin audit HTTP logging requested for {endpoint}, but no async runtime is available",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,54 +264,29 @@ fn append_record_to_file(path: &Path, record: &AdminAuditRecord) -> io::Result<(
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let mut file = OpenOptions::new().create(true).append(true).open(path)?;
|
||||
let mut options = OpenOptions::new();
|
||||
options.create(true).append(true).write(true);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
options.mode(0o600);
|
||||
}
|
||||
|
||||
let file = options.open(path)?;
|
||||
let mut lock = FdLock::new(file);
|
||||
let mut guard = lock.lock()?;
|
||||
let line = serde_json::to_string(record).map_err(io::Error::other)?;
|
||||
file.write_all(line.as_bytes())?;
|
||||
file.write_all(b"\n")?;
|
||||
guard.write_all(line.as_bytes())?;
|
||||
guard.write_all(b"\n")?;
|
||||
guard.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_record_to_endpoint(endpoint: &str, record: AdminAuditRecord) {
|
||||
match Handle::try_current() {
|
||||
Ok(handle) => {
|
||||
let client = reqwest::Client::new();
|
||||
let endpoint = endpoint.to_string();
|
||||
handle.spawn(async move {
|
||||
if let Err(err) = client.post(endpoint).json(&record).send().await {
|
||||
warn!(?err, "failed to post admin audit event");
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("admin audit HTTP logging requested but no async runtime is available");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_path(raw: &str) -> io::Result<PathBuf> {
|
||||
if raw == "~" {
|
||||
return dirs::home_dir().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"could not resolve home directory for admin audit log file",
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(rest) = raw.strip_prefix("~/") {
|
||||
let mut home = dirs::home_dir().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"could not resolve home directory for admin audit log file",
|
||||
)
|
||||
})?;
|
||||
if !rest.is_empty() {
|
||||
home.push(rest);
|
||||
}
|
||||
return Ok(home);
|
||||
}
|
||||
|
||||
Ok(PathBuf::from(raw))
|
||||
async fn send_record_to_endpoint(
|
||||
endpoint: &str,
|
||||
record: AdminAuditRecord,
|
||||
) -> Result<(), reqwest::Error> {
|
||||
Client::new().post(endpoint).json(&record).send().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn current_username() -> String {
|
||||
@@ -343,34 +309,18 @@ fn env_var(key: &str) -> Option<String> {
|
||||
std::env::var(key).ok().filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn sandbox_label(policy: &SandboxPolicy) -> &'static str {
|
||||
match policy {
|
||||
SandboxPolicy::DangerFullAccess => "danger-full-access",
|
||||
SandboxPolicy::ReadOnly => "read-only",
|
||||
SandboxPolicy::WorkspaceWrite { .. } => "workspace-write",
|
||||
}
|
||||
}
|
||||
|
||||
fn sandbox_type_label(sandbox: SandboxType) -> &'static str {
|
||||
match sandbox {
|
||||
SandboxType::None => "none",
|
||||
SandboxType::MacosSeatbelt => "macos-seatbelt",
|
||||
SandboxType::LinuxSeccomp => "linux-seccomp",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_danger_audit_payload(
|
||||
pending: &DangerPending,
|
||||
action: DangerAuditAction,
|
||||
justification: Option<String>,
|
||||
) -> AdminAuditPayload {
|
||||
AdminAuditPayload::Danger(DangerAuditDetails {
|
||||
AdminAuditPayload::Danger {
|
||||
action,
|
||||
justification,
|
||||
requested_by: pending.source,
|
||||
sandbox: sandbox_label(&pending.requested_sandbox).to_string(),
|
||||
sandbox_policy: pending.requested_sandbox.clone(),
|
||||
approval_policy: pending.requested_approval,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_command_audit_payload(
|
||||
@@ -379,13 +329,118 @@ pub fn build_command_audit_payload(
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
cli_cwd: &Path,
|
||||
) -> AdminAuditPayload {
|
||||
AdminAuditPayload::Command(CommandAuditDetails {
|
||||
AdminAuditPayload::Command {
|
||||
command: params.command.clone(),
|
||||
command_cwd: params.cwd.display().to_string(),
|
||||
cli_cwd: cli_cwd.display().to_string(),
|
||||
sandbox: sandbox_type_label(sandbox_type).to_string(),
|
||||
sandbox_policy: sandbox_label(sandbox_policy).to_string(),
|
||||
command_cwd: params.cwd.clone(),
|
||||
cli_cwd: cli_cwd.to_path_buf(),
|
||||
sandbox_type,
|
||||
sandbox_policy: sandbox_policy.clone(),
|
||||
escalated: params.with_escalated_permissions.unwrap_or(false),
|
||||
justification: params.justification.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn danger_payload_serializes_expected_fields() {
|
||||
let pending = DangerPending {
|
||||
source: DangerRequestSource::Approvals,
|
||||
requested_sandbox: SandboxPolicy::DangerFullAccess,
|
||||
requested_approval: AskForApproval::Never,
|
||||
};
|
||||
|
||||
let payload = build_danger_audit_payload(
|
||||
&pending,
|
||||
DangerAuditAction::Requested,
|
||||
Some("reason".to_string()),
|
||||
);
|
||||
let record = AdminAuditRecord::new(payload);
|
||||
let value = serde_json::to_value(record).expect("serialize record");
|
||||
|
||||
assert_eq!(
|
||||
value.get("audit_kind"),
|
||||
Some(&Value::String("danger".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
value.get("action"),
|
||||
Some(&Value::String("requested".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
value.get("requested_by"),
|
||||
Some(&Value::String("approvals".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
value.get("approval_policy"),
|
||||
Some(&Value::String("never".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
value.get("sandbox_policy").and_then(|sp| sp.get("mode")),
|
||||
Some(&Value::String("danger-full-access".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
value.get("justification"),
|
||||
Some(&Value::String("reason".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_payload_serializes_expected_fields() {
|
||||
let mut env = HashMap::new();
|
||||
env.insert("PATH".to_string(), "/usr/bin".to_string());
|
||||
let params = ExecParams {
|
||||
command: vec!["echo".to_string(), "hello".to_string()],
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
timeout_ms: Some(1000),
|
||||
env,
|
||||
with_escalated_permissions: Some(true),
|
||||
justification: Some("investigation".to_string()),
|
||||
};
|
||||
|
||||
let sandbox_policy = SandboxPolicy::new_workspace_write_policy();
|
||||
let payload = build_command_audit_payload(
|
||||
¶ms,
|
||||
SandboxType::MacosSeatbelt,
|
||||
&sandbox_policy,
|
||||
Path::new("/workspace"),
|
||||
);
|
||||
let record = AdminAuditRecord::new(payload);
|
||||
let value = serde_json::to_value(record).expect("serialize record");
|
||||
|
||||
assert_eq!(
|
||||
value.get("audit_kind"),
|
||||
Some(&Value::String("command".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
value.get("command"),
|
||||
Some(&serde_json::json!(["echo", "hello"]))
|
||||
);
|
||||
assert_eq!(
|
||||
value.get("command_cwd"),
|
||||
Some(&Value::String("/tmp".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
value.get("cli_cwd"),
|
||||
Some(&Value::String("/workspace".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
value.get("sandbox_type"),
|
||||
Some(&Value::String("macos-seatbelt".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
value.get("sandbox_policy").and_then(|sp| sp.get("mode")),
|
||||
Some(&Value::String("workspace-write".to_string()))
|
||||
);
|
||||
assert_eq!(value.get("escalated"), Some(&Value::Bool(true)));
|
||||
assert_eq!(
|
||||
value.get("justification"),
|
||||
Some(&Value::String("investigation".to_string()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ use crate::protocol::SandboxPolicy;
|
||||
use crate::seatbelt::spawn_command_under_seatbelt;
|
||||
use crate::spawn::StdioPolicy;
|
||||
use crate::spawn::spawn_child_async;
|
||||
use serde::Serialize;
|
||||
|
||||
const DEFAULT_TIMEOUT_MS: u64 = 10_000;
|
||||
|
||||
@@ -61,7 +62,8 @@ impl ExecParams {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum SandboxType {
|
||||
None,
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ mod mcp_tool_call;
|
||||
mod message_history;
|
||||
mod model_provider_info;
|
||||
pub mod parse_command;
|
||||
mod path_utils;
|
||||
mod truncate;
|
||||
mod unified_exec;
|
||||
mod user_instructions;
|
||||
|
||||
19
codex-rs/core/src/path_utils.rs
Normal file
19
codex-rs/core/src/path_utils.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub(crate) fn expand_tilde(raw: &str) -> io::Result<PathBuf> {
|
||||
if raw.starts_with('~') {
|
||||
// `shellexpand::tilde` falls back to returning the input when the home directory
|
||||
// cannot be resolved; mirror the previous error semantics in that case.
|
||||
let expanded = shellexpand::tilde(raw);
|
||||
if expanded.starts_with('~') {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"could not resolve home directory while expanding path",
|
||||
));
|
||||
}
|
||||
return Ok(PathBuf::from(expanded.as_ref()));
|
||||
}
|
||||
|
||||
Ok(PathBuf::from(raw))
|
||||
}
|
||||
@@ -16,19 +16,9 @@ use codex_ansi_escape::ansi_escape_line;
|
||||
use codex_common::approval_presets::ApprovalPreset;
|
||||
use codex_core::AuthManager;
|
||||
use codex_core::ConversationManager;
|
||||
use codex_core::admin_controls::DangerAuditAction;
|
||||
use codex_core::admin_controls::DangerDecision;
|
||||
use codex_core::admin_controls::DangerPending;
|
||||
use codex_core::admin_controls::DangerRequestSource;
|
||||
use codex_core::admin_controls::PendingAdminAction;
|
||||
use codex_core::admin_controls::build_danger_audit_payload;
|
||||
use codex_core::admin_controls::log_admin_event;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::persist_model_selection;
|
||||
use codex_core::model_family::find_family_for_model;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_core::protocol::SessionSource;
|
||||
use codex_core::protocol::TokenUsage;
|
||||
use codex_core::protocol_config_types::ReasoningEffort as ReasoningEffortConfig;
|
||||
@@ -429,172 +419,6 @@ impl App {
|
||||
self.config.model_reasoning_effort = effort;
|
||||
}
|
||||
|
||||
fn handle_apply_approval_preset(&mut self, preset: ApprovalPreset) -> Result<()> {
|
||||
let approval = preset.approval;
|
||||
let sandbox = preset.sandbox;
|
||||
|
||||
match sandbox.clone() {
|
||||
SandboxPolicy::DangerFullAccess => match self.config.admin.decision_for_danger() {
|
||||
DangerDecision::Allowed => {
|
||||
if let Some(audit) = self.config.admin.audit.as_ref() {
|
||||
let pending = DangerPending {
|
||||
source: DangerRequestSource::Approvals,
|
||||
requested_sandbox: SandboxPolicy::DangerFullAccess,
|
||||
requested_approval: approval,
|
||||
};
|
||||
log_admin_event(
|
||||
audit,
|
||||
build_danger_audit_payload(&pending, DangerAuditAction::Approved, None),
|
||||
);
|
||||
}
|
||||
self.apply_sandbox_and_approval(approval, sandbox);
|
||||
}
|
||||
DangerDecision::RequiresJustification => {
|
||||
let pending = DangerPending {
|
||||
source: DangerRequestSource::Approvals,
|
||||
requested_sandbox: SandboxPolicy::DangerFullAccess,
|
||||
requested_approval: approval,
|
||||
};
|
||||
if let Some(audit) = self.config.admin.audit.as_ref() {
|
||||
log_admin_event(
|
||||
audit,
|
||||
build_danger_audit_payload(
|
||||
&pending,
|
||||
DangerAuditAction::Requested,
|
||||
None,
|
||||
),
|
||||
);
|
||||
}
|
||||
let _ = self.config.admin.take_pending_danger();
|
||||
let _ = self.chat_widget.config_mut().admin.take_pending_danger();
|
||||
self.config
|
||||
.admin
|
||||
.pending
|
||||
.push(PendingAdminAction::Danger(pending.clone()));
|
||||
self.chat_widget
|
||||
.config_mut()
|
||||
.admin
|
||||
.pending
|
||||
.push(PendingAdminAction::Danger(pending.clone()));
|
||||
self.chat_widget.prompt_for_danger_justification(pending);
|
||||
}
|
||||
DangerDecision::Denied => {
|
||||
if let Some(audit) = self.config.admin.audit.as_ref() {
|
||||
let pending = DangerPending {
|
||||
source: DangerRequestSource::Approvals,
|
||||
requested_sandbox: SandboxPolicy::DangerFullAccess,
|
||||
requested_approval: approval,
|
||||
};
|
||||
log_admin_event(
|
||||
audit,
|
||||
build_danger_audit_payload(&pending, DangerAuditAction::Denied, None),
|
||||
);
|
||||
}
|
||||
self.chat_widget.add_error_message(
|
||||
"Full access is disabled by your administrator.".to_string(),
|
||||
);
|
||||
}
|
||||
},
|
||||
other => {
|
||||
self.apply_sandbox_and_approval(approval, other);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_danger_justification_submission(&mut self, justification: String) -> Result<()> {
|
||||
let justification = justification.trim();
|
||||
if justification.is_empty() {
|
||||
self.chat_widget.add_error_message(
|
||||
"Please provide a justification before enabling full access.".to_string(),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(pending) = self.chat_widget.take_pending_danger() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let _ = self.config.admin.take_pending_danger();
|
||||
let _ = self.chat_widget.config_mut().admin.take_pending_danger();
|
||||
|
||||
if let Some(audit) = self.config.admin.audit.as_ref() {
|
||||
log_admin_event(
|
||||
audit,
|
||||
build_danger_audit_payload(
|
||||
&pending,
|
||||
DangerAuditAction::Approved,
|
||||
Some(justification.to_string()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
self.apply_sandbox_and_approval(pending.requested_approval, pending.requested_sandbox);
|
||||
self.chat_widget.add_info_message(
|
||||
"Full access enabled.".to_string(),
|
||||
Some("Justification has been logged.".to_string()),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_danger_justification_cancelled(&mut self) -> Result<()> {
|
||||
let pending_config = self.config.admin.take_pending_danger();
|
||||
let pending_widget = self.chat_widget.take_pending_danger();
|
||||
let _ = self.chat_widget.config_mut().admin.take_pending_danger();
|
||||
|
||||
if let Some(pending) = pending_config.or(pending_widget)
|
||||
&& let Some(audit) = self.config.admin.audit.as_ref()
|
||||
{
|
||||
log_admin_event(
|
||||
audit,
|
||||
build_danger_audit_payload(&pending, DangerAuditAction::Cancelled, None),
|
||||
);
|
||||
}
|
||||
|
||||
let approval_label = self.config.approval_policy.to_string();
|
||||
let sandbox_label = Self::sandbox_policy_label(&self.config.sandbox_policy);
|
||||
|
||||
self.chat_widget.add_info_message(
|
||||
format!(
|
||||
"Full access remains disabled. Current approval policy `{approval_label}`, sandbox `{sandbox_label}`."
|
||||
),
|
||||
None,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_sandbox_and_approval(&mut self, approval: AskForApproval, sandbox: SandboxPolicy) {
|
||||
self.chat_widget.submit_op(Op::OverrideTurnContext {
|
||||
cwd: None,
|
||||
approval_policy: Some(approval),
|
||||
sandbox_policy: Some(sandbox.clone()),
|
||||
model: None,
|
||||
effort: None,
|
||||
summary: None,
|
||||
});
|
||||
self.chat_widget.set_approval_policy(approval);
|
||||
self.chat_widget.set_sandbox_policy(sandbox.clone());
|
||||
self.config.approval_policy = approval;
|
||||
self.config.sandbox_policy = sandbox;
|
||||
}
|
||||
|
||||
fn process_pending_admin_controls(&mut self) {
|
||||
while let Some(pending) = self.config.admin.take_pending_danger() {
|
||||
let _ = self.chat_widget.config_mut().admin.take_pending_danger();
|
||||
self.chat_widget.prompt_for_danger_justification(pending);
|
||||
}
|
||||
}
|
||||
|
||||
fn sandbox_policy_label(policy: &SandboxPolicy) -> &'static str {
|
||||
match policy {
|
||||
SandboxPolicy::DangerFullAccess => "danger-full-access",
|
||||
SandboxPolicy::ReadOnly => "read-only",
|
||||
SandboxPolicy::WorkspaceWrite { .. } => "workspace-write",
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_key_event(&mut self, tui: &mut tui::Tui, key_event: KeyEvent) {
|
||||
match key_event {
|
||||
KeyEvent {
|
||||
|
||||
192
codex-rs/tui/src/app_admin.rs
Normal file
192
codex-rs/tui/src/app_admin.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
use crate::app::App;
|
||||
use codex_common::approval_presets::ApprovalPreset;
|
||||
use codex_core::admin_controls::DangerAuditAction;
|
||||
use codex_core::admin_controls::DangerDecision;
|
||||
use codex_core::admin_controls::DangerPending;
|
||||
use codex_core::admin_controls::DangerRequestSource;
|
||||
use codex_core::admin_controls::PendingAdminAction;
|
||||
use codex_core::admin_controls::build_danger_audit_payload;
|
||||
use codex_core::admin_controls::log_admin_event;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use color_eyre::eyre::Result;
|
||||
|
||||
impl App {
|
||||
pub(crate) fn handle_apply_approval_preset(&mut self, preset: ApprovalPreset) -> Result<()> {
|
||||
self.cancel_existing_pending_requests();
|
||||
|
||||
let ApprovalPreset {
|
||||
approval, sandbox, ..
|
||||
} = preset;
|
||||
|
||||
match sandbox {
|
||||
SandboxPolicy::DangerFullAccess => match self.config.admin.decision_for_danger() {
|
||||
DangerDecision::Allowed => {
|
||||
let pending = DangerPending {
|
||||
source: DangerRequestSource::Approvals,
|
||||
requested_sandbox: SandboxPolicy::DangerFullAccess,
|
||||
requested_approval: approval,
|
||||
};
|
||||
self.log_danger_event(&pending, DangerAuditAction::Approved, None);
|
||||
self.apply_sandbox_and_approval(approval, SandboxPolicy::DangerFullAccess);
|
||||
}
|
||||
DangerDecision::RequiresJustification => {
|
||||
let pending = DangerPending {
|
||||
source: DangerRequestSource::Approvals,
|
||||
requested_sandbox: SandboxPolicy::DangerFullAccess,
|
||||
requested_approval: approval,
|
||||
};
|
||||
self.log_danger_event(&pending, DangerAuditAction::Requested, None);
|
||||
self.push_pending_danger(pending.clone());
|
||||
self.chat_widget.prompt_for_danger_justification(pending);
|
||||
}
|
||||
DangerDecision::Denied => {
|
||||
let pending = DangerPending {
|
||||
source: DangerRequestSource::Approvals,
|
||||
requested_sandbox: SandboxPolicy::DangerFullAccess,
|
||||
requested_approval: approval,
|
||||
};
|
||||
self.log_danger_event(&pending, DangerAuditAction::Denied, None);
|
||||
self.chat_widget.add_error_message(
|
||||
"Full access is disabled by your administrator.".to_string(),
|
||||
);
|
||||
}
|
||||
},
|
||||
other => {
|
||||
self.apply_sandbox_and_approval(approval, other);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn handle_danger_justification_submission(
|
||||
&mut self,
|
||||
justification: String,
|
||||
) -> Result<()> {
|
||||
let justification = justification.trim();
|
||||
if justification.is_empty() {
|
||||
self.chat_widget.add_error_message(
|
||||
"Please provide a justification before enabling full access.".to_string(),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(pending) = self.chat_widget.take_pending_danger() else {
|
||||
return Ok(());
|
||||
};
|
||||
if let Some(internal) = self.drop_pending_from_configs() {
|
||||
debug_assert_eq!(internal, pending);
|
||||
}
|
||||
|
||||
self.log_danger_event(
|
||||
&pending,
|
||||
DangerAuditAction::Approved,
|
||||
Some(justification.to_string()),
|
||||
);
|
||||
|
||||
let DangerPending {
|
||||
requested_approval,
|
||||
requested_sandbox,
|
||||
..
|
||||
} = pending;
|
||||
self.apply_sandbox_and_approval(requested_approval, requested_sandbox);
|
||||
self.chat_widget.add_info_message(
|
||||
"Full access enabled.".to_string(),
|
||||
Some("Justification has been logged.".to_string()),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn handle_danger_justification_cancelled(&mut self) -> Result<()> {
|
||||
self.cancel_existing_pending_requests();
|
||||
|
||||
let approval_label = self.config.approval_policy.to_string();
|
||||
let sandbox_label = self.config.sandbox_policy.to_string();
|
||||
|
||||
self.chat_widget.add_info_message(
|
||||
format!(
|
||||
"Full access remains disabled. Current approval policy `{approval_label}`, sandbox `{sandbox_label}`."
|
||||
),
|
||||
None,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn process_pending_admin_controls(&mut self) {
|
||||
while let Some(pending) = self.drop_pending_from_configs() {
|
||||
self.chat_widget.prompt_for_danger_justification(pending);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_sandbox_and_approval(&mut self, approval: AskForApproval, sandbox: SandboxPolicy) {
|
||||
self.chat_widget.submit_op(Op::OverrideTurnContext {
|
||||
cwd: None,
|
||||
approval_policy: Some(approval),
|
||||
sandbox_policy: Some(sandbox.clone()),
|
||||
model: None,
|
||||
effort: None,
|
||||
summary: None,
|
||||
});
|
||||
self.chat_widget.set_approval_policy(approval);
|
||||
self.chat_widget.set_sandbox_policy(sandbox.clone());
|
||||
self.config.approval_policy = approval;
|
||||
self.config.sandbox_policy = sandbox;
|
||||
}
|
||||
|
||||
fn push_pending_danger(&mut self, pending: DangerPending) {
|
||||
self.config
|
||||
.admin
|
||||
.pending
|
||||
.push(PendingAdminAction::Danger(pending.clone()));
|
||||
self.chat_widget
|
||||
.config_mut()
|
||||
.admin
|
||||
.pending
|
||||
.push(PendingAdminAction::Danger(pending));
|
||||
}
|
||||
|
||||
fn cancel_existing_pending_requests(&mut self) {
|
||||
let mut logged = false;
|
||||
|
||||
if let Some(previous) = self.config.admin.take_pending_danger() {
|
||||
self.log_danger_event(&previous, DangerAuditAction::Cancelled, None);
|
||||
logged = true;
|
||||
}
|
||||
|
||||
if let Some(previous) = self.chat_widget.config_mut().admin.take_pending_danger() {
|
||||
if !logged {
|
||||
self.log_danger_event(&previous, DangerAuditAction::Cancelled, None);
|
||||
logged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(previous) = self.chat_widget.take_pending_danger() {
|
||||
if !logged {
|
||||
self.log_danger_event(&previous, DangerAuditAction::Cancelled, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn drop_pending_from_configs(&mut self) -> Option<DangerPending> {
|
||||
let config_pending = self.config.admin.take_pending_danger();
|
||||
let widget_pending = self.chat_widget.config_mut().admin.take_pending_danger();
|
||||
config_pending.or(widget_pending)
|
||||
}
|
||||
|
||||
fn log_danger_event(
|
||||
&self,
|
||||
pending: &DangerPending,
|
||||
action: DangerAuditAction,
|
||||
justification: Option<String>,
|
||||
) {
|
||||
if let Some(audit) = self.config.admin.audit.as_ref() {
|
||||
log_admin_event(
|
||||
audit,
|
||||
build_danger_audit_payload(pending, action, justification),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ use tracing_subscriber::EnvFilter;
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
mod app;
|
||||
mod app_admin;
|
||||
mod app_backtrack;
|
||||
mod app_event;
|
||||
mod app_event_sender;
|
||||
|
||||
Reference in New Issue
Block a user