From 219c2034173becb616d74775c625bd1cc454e057 Mon Sep 17 00:00:00 2001 From: jif Date: Mon, 24 Aug 2026 21:26:17 +0000 Subject: [PATCH] Add a computer-use-only Guardian v2 review scope (#40480) ## What changed - Add `features.guardianv2.review_scope.computer_use_only` to restrict asynchronous classification and fast approvals to browser and computer-use REPL tools. - Keep other tools on the synchronous approval path without letting them age or reuse scores from in-scope calls. - Give this scope precedence over `sandboxed_exec_commands` when both options are enabled. ## Testing - Cover configuration precedence, direct and code-mode tool detection, fast-approval isolation, and end-to-end approval routing for browser, computer-use, and unrelated MCP servers. GitOrigin-RevId: bfd9ab6abdbd90634b265d354e038724b1e81d65 --- .../app-server/tests/suite/v2/guardian_v2.rs | 77 +++++++++- codex-rs/core/config.schema.json | 4 + .../guardian-v2/src/async_scorer/config.rs | 25 +++- .../src/async_scorer/config_tests.rs | 30 +++- .../guardian-v2/src/async_scorer/extension.rs | 38 +++-- .../src/async_scorer/extension_tests.rs | 139 +++++++++++++++++- codex-rs/features/src/feature_configs.rs | 3 + codex-rs/features/src/tests.rs | 2 + 8 files changed, 289 insertions(+), 29 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/guardian_v2.rs b/codex-rs/app-server/tests/suite/v2/guardian_v2.rs index 3edceb4885..3823b057b8 100644 --- a/codex-rs/app-server/tests/suite/v2/guardian_v2.rs +++ b/codex-rs/app-server/tests/suite/v2/guardian_v2.rs @@ -74,6 +74,7 @@ struct MockResponsesState { luna_score: f64, review_outcome: ReviewOutcome, transcript_content: TranscriptContent, + mcp_server_name: Option<&'static str>, root_worker: bool, root_user_restriction: bool, } @@ -106,6 +107,12 @@ enum ModelReviewRequirement { Required, } +#[derive(Clone, Copy)] +enum GuardianToolScope { + AllTools, + ComputerUseOnly { server_name: &'static str }, +} + #[derive(Clone, Copy)] enum ThreadLifecycle { New, @@ -245,7 +252,7 @@ async fn parent_response( responses::ev_response_created(&call_id), responses::ev_function_call_with_namespace( &call_id, - &format!("mcp__{TEST_SERVER_NAME}"), + &format!("mcp__{}", state.mcp_server_name.unwrap_or(TEST_SERVER_NAME)), TEST_TOOL_NAME, &arguments, ), @@ -323,8 +330,34 @@ async fn guardian_v2_routes_tool_approvals( review_outcome: ReviewOutcome, transcript_content: TranscriptContent, ) -> Result<()> { + guardian_v2_routes_scoped_tool_approvals( + risk, + lifecycle, + requirement, + review_outcome, + transcript_content, + GuardianToolScope::AllTools, + ) + .await +} + +async fn guardian_v2_routes_scoped_tool_approvals( + risk: GuardianRisk, + lifecycle: ThreadLifecycle, + requirement: ModelReviewRequirement, + review_outcome: ReviewOutcome, + transcript_content: TranscriptContent, + scope: GuardianToolScope, +) -> Result<()> { + let server_name = match scope { + GuardianToolScope::AllTools => TEST_SERVER_NAME, + GuardianToolScope::ComputerUseOnly { server_name } => server_name, + }; + let classifier_in_scope = !matches!(scope, GuardianToolScope::ComputerUseOnly { .. }) + || codex_protocol::mcp::is_node_repl_backed_server(server_name); let (luna_score, expected_guardian_reviews) = match (requirement, risk) { (ModelReviewRequirement::Required, _) => (0.25, 2), + (ModelReviewRequirement::Optional, _) if !classifier_in_scope => (0.25, 2), (ModelReviewRequirement::Optional, GuardianRisk::Low) => (0.25, 1), (ModelReviewRequirement::Optional, GuardianRisk::Threshold) => (0.5, 2), (ModelReviewRequirement::Optional, GuardianRisk::High) => (0.95, 2), @@ -339,6 +372,7 @@ async fn guardian_v2_routes_tool_approvals( luna_score, review_outcome, transcript_content, + mcp_server_name: Some(server_name), root_worker: matches!( lifecycle, ThreadLifecycle::RootRollback | ThreadLifecycle::RootRestriction @@ -385,16 +419,23 @@ async fn guardian_v2_routes_tool_approvals( ("approvals_reviewer = \"user\"", ApprovalsReviewer::User) } }; + let guardian_scope_config = if matches!(scope, GuardianToolScope::ComputerUseOnly { .. }) { + "\n\n[features.guardianv2]\nenabled = true\n\n[features.guardianv2.review_scope]\ncomputer_use_only = true" + } else { + "" + }; let mut mock_config = MockResponsesConfig::new(&responses_url) .with_model(MODEL) .with_provider_config("supports_websockets = false") .with_approval_policy("on-request") .with_root_config(reviewer_config) .with_extra_config(&format!( - "[mcp_servers.{TEST_SERVER_NAME}]\nurl = \"{mcp_server_url}/mcp\"\ndefault_tools_approval_mode = \"prompt\"\n\n[analytics]\nenabled = true\n\n[otel]\nmetrics_exporter = {{ otlp-http = {{ endpoint = \"{responses_url}/metrics\", protocol = \"json\" }} }}" + "[mcp_servers.{server_name}]\nurl = \"{mcp_server_url}/mcp\"\ndefault_tools_approval_mode = \"prompt\"\n\n[analytics]\nenabled = true\n\n[otel]\nmetrics_exporter = {{ otlp-http = {{ endpoint = \"{responses_url}/metrics\", protocol = \"json\" }} }}{guardian_scope_config}" )) - .enable_feature(Feature::GuardianV2) .enable_feature(Feature::GuardianApproval); + if matches!(scope, GuardianToolScope::AllTools) { + mock_config = mock_config.enable_feature(Feature::GuardianV2); + } if matches!( lifecycle, ThreadLifecycle::RootRollback | ThreadLifecycle::RootRestriction @@ -507,7 +548,7 @@ async fn guardian_v2_routes_tool_approvals( assert_eq!(reviewed_thread_id, thread_id); } - if matches!(requirement, ModelReviewRequirement::Optional) { + if matches!(requirement, ModelReviewRequirement::Optional) && classifier_in_scope { let luna_request = wait_for_luna_request(responses_state.as_ref(), /*index*/ 0).await?; assert_eq!( luna_request["prompt_cache_key"], @@ -612,17 +653,18 @@ async fn guardian_v2_routes_tool_approvals( responses_state.guardian_reviews.load(Ordering::SeqCst), expected_guardian_reviews ); - if matches!(requirement, ModelReviewRequirement::Required) { + if matches!(requirement, ModelReviewRequirement::Required) || !classifier_in_scope { assert!( responses_state .luna_requests .lock() .expect("Luna request lock should not be poisoned") .is_empty(), - "protected models must not receive Guardian v2 risk scoring" + "out-of-scope tools and protected models must not receive Guardian v2 risk scoring" ); } let requires_strict_review = matches!(requirement, ModelReviewRequirement::Optional) + && classifier_in_scope && matches!(risk, GuardianRisk::Threshold | GuardianRisk::High); let strict_review_count = app_server .pending_notification_methods() @@ -651,7 +693,7 @@ async fn guardian_v2_routes_tool_approvals( ); } - if matches!(requirement, ModelReviewRequirement::Optional) { + if matches!(requirement, ModelReviewRequirement::Optional) && classifier_in_scope { let state_db = StateRuntime::init( codex_state::SqliteConfig::new_for_testing(codex_home.path().abs()), "mock_provider".to_owned(), @@ -775,6 +817,27 @@ async fn guardian_v2_low_risk_actions_skip_subsequent_reviews() -> Result<()> { .await } +#[test_case("node_repl", GuardianRisk::Low; "low risk browser skips full review")] +#[test_case("cua_repl", GuardianRisk::Low; "low risk computer use skips full review")] +#[test_case("node_repl", GuardianRisk::High; "high risk browser receives full review")] +#[test_case(TEST_SERVER_NAME, GuardianRisk::Low; "other mcp keeps synchronous review")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn guardian_v2_computer_use_only_scopes_classification_and_fast_reviews( + server_name: &'static str, + risk: GuardianRisk, +) -> Result<()> { + skip_if_no_network!(Ok(())); + guardian_v2_routes_scoped_tool_approvals( + risk, + ThreadLifecycle::New, + ModelReviewRequirement::Optional, + ReviewOutcome::Allow, + TranscriptContent::Normal, + GuardianToolScope::ComputerUseOnly { server_name }, + ) + .await +} + #[test_case(ReviewOutcome::Allow, TranscriptContent::Normal; "approved_evidence")] #[test_case(ReviewOutcome::Deny, TranscriptContent::Normal; "denied_evidence")] #[test_case(ReviewOutcome::Malformed, TranscriptContent::Normal; "failed_review_without_evidence")] diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 139d73aa27..d419c2b13d 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -1447,6 +1447,10 @@ "additionalProperties": false, "description": "Optional tool-call categories available to the Guardian v2 classifier.", "properties": { + "computer_use_only": { + "description": "Restrict asynchronous classification and fast approvals to browser and computer-use tools.", + "type": "boolean" + }, "sandboxed_exec_commands": { "description": "Include sandboxed shell command calls in Guardian v2 classification.", "type": "boolean" diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/config.rs b/codex-rs/ext/guardian-v2/src/async_scorer/config.rs index 155e8dfdd9..66278a7fe6 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/config.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/config.rs @@ -24,6 +24,12 @@ const DEFAULT_MAX_TOOL_CALL_LAG: usize = 3; pub(crate) const DEFAULT_CLASSIFIER_INSTRUCTIONS: &str = include_str!("classifier_instructions.md"); pub(crate) const CLASSIFICATION_OUTPUT_INSTRUCTIONS: &str = "Your first output token is the entire classification: `high` for high risk or `low` for low risk. Output that token immediately and nothing else."; +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum GuardianV2ReviewScope { + Standard { sandboxed_exec_commands: bool }, + ComputerUseOnly, +} + #[derive(Clone, Debug, PartialEq)] pub(crate) struct GuardianV2Config { local_overrides: GuardianV2ConfigToml, @@ -36,7 +42,7 @@ pub(crate) struct GuardianV2Config { pub(crate) max_classifier_instruction_tokens: Option, pub(crate) reuse_parent_compaction: bool, pub(crate) max_parent_compaction_tokens: usize, - pub(crate) sandboxed_exec_commands: bool, + pub(crate) review_scope: GuardianV2ReviewScope, pub(crate) transcript: TranscriptConfig, } @@ -235,11 +241,22 @@ impl GuardianV2Config { max_classifier_instruction_tokens, reuse_parent_compaction: configured.reuse_parent_compaction.unwrap_or(true), max_parent_compaction_tokens, - sandboxed_exec_commands: configured + review_scope: if configured .review_scope .as_ref() - .and_then(|review_scope| review_scope.sandboxed_exec_commands) - .unwrap_or(false), + .and_then(|review_scope| review_scope.computer_use_only) + .unwrap_or(false) + { + GuardianV2ReviewScope::ComputerUseOnly + } else { + GuardianV2ReviewScope::Standard { + sandboxed_exec_commands: configured + .review_scope + .as_ref() + .and_then(|review_scope| review_scope.sandboxed_exec_commands) + .unwrap_or(false), + } + }, transcript: TranscriptConfig { sources: transcript_config .and_then(|transcript| transcript.sources.clone()) diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/config_tests.rs b/codex-rs/ext/guardian-v2/src/async_scorer/config_tests.rs index d65c02b3aa..f3c63c86fe 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/config_tests.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/config_tests.rs @@ -9,26 +9,52 @@ use pretty_assertions::assert_eq; use super::CLASSIFICATION_OUTPUT_INSTRUCTIONS; use super::DEFAULT_CLASSIFIER_INSTRUCTIONS; use super::GuardianV2Config; +use super::GuardianV2ReviewScope; use crate::async_scorer::transcript::truncate_entry; #[test] fn sandboxed_exec_commands_are_excluded_by_default() { let config = GuardianV2Config::from_overrides(GuardianV2ConfigToml::default()).unwrap(); - assert!(!config.sandboxed_exec_commands); + assert_eq!( + config.review_scope, + GuardianV2ReviewScope::Standard { + sandboxed_exec_commands: false, + } + ); } #[test] fn sandboxed_exec_commands_can_be_included() { let config = GuardianV2Config::from_overrides(GuardianV2ConfigToml { review_scope: Some(GuardianV2ReviewScopeConfigToml { + computer_use_only: None, sandboxed_exec_commands: Some(true), }), ..Default::default() }) .unwrap(); - assert!(config.sandboxed_exec_commands); + assert_eq!( + config.review_scope, + GuardianV2ReviewScope::Standard { + sandboxed_exec_commands: true, + } + ); +} + +#[test] +fn computer_use_only_takes_precedence_over_sandboxed_exec_commands() { + let config = GuardianV2Config::from_overrides(GuardianV2ConfigToml { + review_scope: Some(GuardianV2ReviewScopeConfigToml { + computer_use_only: Some(true), + sandboxed_exec_commands: Some(true), + }), + ..Default::default() + }) + .unwrap(); + + assert_eq!(config.review_scope, GuardianV2ReviewScope::ComputerUseOnly); } #[test] diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/extension.rs b/codex-rs/ext/guardian-v2/src/async_scorer/extension.rs index f7ac274988..81804ccd5e 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/extension.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/extension.rs @@ -34,6 +34,8 @@ use codex_login::AgentIdentityAuthPolicy; use codex_login::AuthManager; use codex_model_provider::create_model_provider; use codex_protocol::ThreadId; +use codex_protocol::mcp::is_node_repl_backed_server; +use codex_protocol::mcp::is_node_repl_backed_tool; use codex_protocol::openai_models::ModelInfo; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::TruncationPolicy; @@ -41,6 +43,7 @@ use codex_protocol::security_risk::SecurityRiskScore; use serde_json::json; use super::config::GuardianV2Config; +use super::config::GuardianV2ReviewScope; use super::review_evidence::render_review_evidence; use super::sampler::LunaSampler; use super::sampler::LunaSamplerConfig; @@ -62,8 +65,14 @@ struct RenderedAction { fn should_classify_tool( tool_name: &ToolName, payload: &ToolPayload, - sandboxed_exec_commands: bool, + review_scope: GuardianV2ReviewScope, ) -> bool { + let GuardianV2ReviewScope::Standard { + sandboxed_exec_commands, + } = review_scope + else { + return is_node_repl_backed_tool(&tool_name.name, tool_name.namespace.as_deref()); + }; if sandboxed_exec_commands || !tool_name.is_default_namespace() || tool_name.name != "exec_command" @@ -367,12 +376,23 @@ impl ApprovalReviewContributor for GuardianV2Extension { &'a self, _session_store: &'a ExtensionData, thread_store: &'a ExtensionData, - _prompt: &'a str, + prompt: &'a str, extension_metrics: Option>, ) -> ExtensionFuture<'a, Option> { Box::pin(async move { thread_store.get::()?; let guardian_config = thread_store.get::()?; + if guardian_config.review_scope == GuardianV2ReviewScope::ComputerUseOnly { + let action = serde_json::from_str::(prompt).ok()?; + if action.get("tool").and_then(serde_json::Value::as_str) != Some("mcp_tool_call") + || !action + .get("server") + .and_then(serde_json::Value::as_str) + .is_some_and(is_node_repl_backed_server) + { + return None; + } + } let score_progress = thread_store.get::()?; let latest_scored_tool_call = score_progress .latest_scored_tool_call @@ -450,14 +470,12 @@ impl GuardianV2Extension { let Some(score_progress) = input.thread_store.get::() else { return; }; - if !should_classify_tool( - input.tool_name, - input.payload, - guardian_config.sandboxed_exec_commands, - ) { - score_progress - .latest_tool_call - .fetch_add(/*val*/ 1, Ordering::Relaxed); + if !should_classify_tool(input.tool_name, input.payload, guardian_config.review_scope) { + if guardian_config.review_scope != GuardianV2ReviewScope::ComputerUseOnly { + score_progress + .latest_tool_call + .fetch_add(/*val*/ 1, Ordering::Relaxed); + } return; } let metrics = score_progress.metrics.clone(); diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs b/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs index e60b888f0a..42c8d67a24 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs @@ -64,6 +64,7 @@ use super::should_classify_tool; use crate::async_scorer::config::CLASSIFICATION_OUTPUT_INSTRUCTIONS; use crate::async_scorer::config::DEFAULT_MODEL_CONTEXT_ITEM_TOKENS; use crate::async_scorer::config::DEFAULT_PARENT_COMPACTION_TOKENS; +use crate::async_scorer::config::GuardianV2ReviewScope; use crate::async_scorer::sampler::CLASSIFICATION_TOKEN_USAGE_METRIC; use crate::async_scorer::sampler::INITIAL_WEBSOCKET_CONNECTIONS; use crate::async_scorer::sampler::MODEL; @@ -294,31 +295,40 @@ async fn sandboxed_shell_classification_respects_review_scope() -> Result<()> { }; let tool_name = ToolName::plain("exec_command"); + let standard_scope = GuardianV2ReviewScope::Standard { + sandboxed_exec_commands: false, + }; assert!(!should_classify_tool( - &tool_name, &sandboxed, /*sandboxed_exec_commands*/ false + &tool_name, + &sandboxed, + standard_scope, )); assert!(!should_classify_tool( &tool_name, &additional_permissions, - /*sandboxed_exec_commands*/ false + standard_scope, )); assert!(should_classify_tool( &tool_name, &unsandboxed, - /*sandboxed_exec_commands*/ false + standard_scope, )); assert!(should_classify_tool( - &tool_name, &sandboxed, /*sandboxed_exec_commands*/ true + &tool_name, + &sandboxed, + GuardianV2ReviewScope::Standard { + sandboxed_exec_commands: true, + }, )); assert!(should_classify_tool( &ToolName::plain("read_file"), &sandboxed, - /*sandboxed_exec_commands*/ false + standard_scope, )); assert!(should_classify_tool( &ToolName::namespaced("mcp", "exec_command"), &sandboxed, - /*sandboxed_exec_commands*/ false + standard_scope, )); skip_if_no_network!(Ok(())); @@ -363,6 +373,123 @@ async fn sandboxed_shell_classification_respects_review_scope() -> Result<()> { Ok(()) } +#[test] +fn computer_use_only_classification_recognizes_direct_and_code_mode_tools() { + let payload = ToolPayload::Function { + arguments: r#"{"code":"await browser.goto('https://example.com')"}"#.to_owned(), + }; + for (tool_name, expected) in [ + (ToolName::namespaced("mcp__node_repl__", "js"), true), + (ToolName::namespaced("mcp__cua_repl__", "js"), true), + (ToolName::plain("mcp__node_repl__js"), true), + (ToolName::plain("mcp__cua_repl__js"), true), + (ToolName::namespaced("mcp__ordinary__", "js"), false), + (ToolName::plain("read_file"), false), + (ToolName::plain("exec_command"), false), + ] { + assert_eq!( + should_classify_tool(&tool_name, &payload, GuardianV2ReviewScope::ComputerUseOnly), + expected, + "unexpected classification scope for {tool_name}" + ); + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn computer_use_only_scores_cannot_approve_other_actions() -> Result<()> { + skip_if_no_network!(Ok(())); + + let fixture = GuardianFailureFixture::new().await?; + let thread_store = fixture.test.codex.thread_extension_data(); + let mut config = thread_store + .get::() + .expect("Guardian v2 should have initialized") + .as_ref() + .clone(); + config.review_scope = GuardianV2ReviewScope::ComputerUseOnly; + thread_store.insert(config); + thread_store.insert(SecurityRiskScore { + scores: BTreeMap::from([("action_risk".to_owned(), 0.25)]), + sampled_at: None, + }); + let progress = thread_store + .get::() + .expect("Guardian v2 should track score progress per thread"); + let latest_tool_call = progress.latest_tool_call.load(Ordering::Acquire); + let turn_store = ExtensionData::new("turn-1"); + let ordinary_tool = ToolName::namespaced("mcp__ordinary__", "write_record"); + let payload = ToolPayload::Function { + arguments: r#"{"record":"sensitive"}"#.to_owned(), + }; + fixture.registry.tool_lifecycle_contributors()[0] + .on_tool_start(ToolStartInput { + session_store: &fixture.session_store, + thread_store, + turn_store: &turn_store, + turn_id: "turn-1", + call_id: "ordinary-call", + tool_name: &ordinary_tool, + payload: &payload, + conversation_history: Arc::new(TestConversationHistory(Vec::new())), + source: ToolCallSource::CodeMode { + cell_id: "cell-1".to_owned(), + runtime_tool_call_id: "nested-1".to_owned(), + }, + }) + .await; + assert_eq!( + progress.latest_tool_call.load(Ordering::Acquire), + latest_tool_call, + "unrelated code-mode calls must not age browser/CUA scores" + ); + + for (action, expected) in [ + ( + json!({"tool": "mcp_tool_call", "server": "node_repl", "tool_name": "js"}), + Some(ReviewDecision::Approved), + ), + ( + json!({"tool": "mcp_tool_call", "server": "cua_repl", "tool_name": "js"}), + Some(ReviewDecision::Approved), + ), + ( + json!({"tool": "mcp_tool_call", "server": "ordinary", "tool_name": "js"}), + None, + ), + (json!({"tool": "exec_command", "server": "node_repl"}), None), + ] { + let prompt = action.to_string(); + assert_eq!( + fixture + .registry + .fast_approval_decision( + &fixture.session_store, + thread_store, + &prompt, + /*extension_metrics*/ None, + ) + .await, + expected, + "unexpected fast approval for {action}" + ); + } + assert_eq!( + fixture + .registry + .fast_approval_decision( + &fixture.session_store, + thread_store, + "not valid JSON", + /*extension_metrics*/ None, + ) + .await, + None, + "malformed approval actions must not reuse a browser score" + ); + + Ok(()) +} + #[test] fn encrypted_parent_compaction_preserves_the_latest_valid_item() { let older = ResponseItem::Compaction { diff --git a/codex-rs/features/src/feature_configs.rs b/codex-rs/features/src/feature_configs.rs index b9e6ae4fec..43e6a4b506 100644 --- a/codex-rs/features/src/feature_configs.rs +++ b/codex-rs/features/src/feature_configs.rs @@ -112,6 +112,9 @@ pub struct GuardianV2TranscriptConfigToml { #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct GuardianV2ReviewScopeConfigToml { + /// Restrict asynchronous classification and fast approvals to browser and computer-use tools. + #[serde(skip_serializing_if = "Option::is_none")] + pub computer_use_only: Option, /// Include sandboxed shell command calls in Guardian v2 classification. #[serde(skip_serializing_if = "Option::is_none")] pub sandboxed_exec_commands: Option, diff --git a/codex-rs/features/src/tests.rs b/codex-rs/features/src/tests.rs index cc7e9fabbf..2e10d91b14 100644 --- a/codex-rs/features/src/tests.rs +++ b/codex-rs/features/src/tests.rs @@ -164,6 +164,7 @@ reuse_parent_compaction = false max_parent_compaction_tokens = 384 [guardianv2.review_scope] +computer_use_only = true sandboxed_exec_commands = true [guardianv2.transcript] @@ -191,6 +192,7 @@ max_recent_non_user_entries = 12 reuse_parent_compaction: Some(false), max_parent_compaction_tokens: Some(384), review_scope: Some(crate::GuardianV2ReviewScopeConfigToml { + computer_use_only: Some(true), sandboxed_exec_commands: Some(true), }), transcript: Some(crate::GuardianV2TranscriptConfigToml {