address feedback

This commit is contained in:
Fouad Matin
2025-10-04 09:35:06 -07:00
parent 662bc2c9ab
commit 9f850f8bb6
10 changed files with 418 additions and 309 deletions

11
codex-rs/Cargo.lock generated
View File

@@ -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"

View File

@@ -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"

View File

@@ -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 }

View File

@@ -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(
&params,
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()))
);
}
}

View File

@@ -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,

View File

@@ -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;

View 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))
}

View File

@@ -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 {

View 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),
);
}
}
}

View File

@@ -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;