Update guardian output schema (#17061)

## Summary
- Update guardian output schema to separate risk, authorization,
outcome, and rationale.
- Feed guardian rationale into rejection messages.
- Split the guardian policy into template and tenant-config sections.

## Validation
- `cargo test -p codex-core mcp_tool_call`
- `env -u CODEX_SANDBOX_NETWORK_DISABLED INSTA_UPDATE=always cargo test
-p codex-core guardian::`

---------

Co-authored-by: Owen Lin <owen@openai.com>
This commit is contained in:
maja-openai
2026-04-08 15:47:29 -07:00
committed by GitHub
parent 49677ec71f
commit dcbc91fd39
45 changed files with 673 additions and 312 deletions

View File

@@ -1,6 +1,6 @@
use crate::codex::Session;
use crate::guardian::GUARDIAN_REJECTION_MESSAGE;
use crate::guardian::GuardianApprovalRequest;
use crate::guardian::guardian_rejection_message;
use crate::guardian::review_approval_request;
use crate::guardian::routes_approval_to_guardian;
use crate::network_policy_decision::denied_network_policy_message;
@@ -368,6 +368,7 @@ impl NetworkApprovalService {
protocol,
};
let owner_call = self.resolve_single_active_call().await;
let guardian_approval_id = Self::approval_id_for_key(&key);
let approval_decision = if routes_approval_to_guardian(&turn_context) {
// TODO(ccunningham): Attach guardian network reviews to the reviewed tool item
// lifecycle instead of this temporary standalone network approval id.
@@ -375,7 +376,7 @@ impl NetworkApprovalService {
&session,
&turn_context,
GuardianApprovalRequest::NetworkAccess {
id: Self::approval_id_for_key(&key),
id: guardian_approval_id.clone(),
turn_id: owner_call
.as_ref()
.map_or_else(|| turn_context.sub_id.clone(), |call| call.turn_id.clone()),
@@ -488,11 +489,12 @@ impl NetworkApprovalService {
ReviewDecision::Denied | ReviewDecision::Abort => {
if routes_approval_to_guardian(&turn_context) {
if let Some(owner_call) = owner_call.as_ref() {
let message =
guardian_rejection_message(session.as_ref(), &guardian_approval_id)
.await;
self.record_call_outcome(
&owner_call.registration_id,
NetworkApprovalOutcome::DeniedByPolicy(
GUARDIAN_REJECTION_MESSAGE.to_string(),
),
NetworkApprovalOutcome::DeniedByPolicy(message),
)
.await;
}

View File

@@ -6,7 +6,7 @@ simple sequence for any ToolRuntime: approval → select sandbox → attempt →
retry with an escalated sandbox strategy on denial (no reapproval thanks to
caching).
*/
use crate::guardian::GUARDIAN_REJECTION_MESSAGE;
use crate::guardian::guardian_rejection_message;
use crate::guardian::routes_approval_to_guardian;
use crate::network_policy_decision::network_approval_context_from_payload;
use crate::tools::network_approval::DeferredNetworkApproval;
@@ -149,7 +149,8 @@ impl ToolOrchestrator {
match decision {
ReviewDecision::Denied | ReviewDecision::Abort => {
let reason = if routes_approval_to_guardian(turn_ctx) {
GUARDIAN_REJECTION_MESSAGE.to_string()
guardian_rejection_message(tool_ctx.session.as_ref(), &tool_ctx.call_id)
.await
} else {
"rejected by user".to_string()
};
@@ -302,7 +303,11 @@ impl ToolOrchestrator {
match decision {
ReviewDecision::Denied | ReviewDecision::Abort => {
let reason = if routes_approval_to_guardian(turn_ctx) {
GUARDIAN_REJECTION_MESSAGE.to_string()
guardian_rejection_message(
tool_ctx.session.as_ref(),
&tool_ctx.call_id,
)
.await
} else {
"rejected by user".to_string()
};

View File

@@ -3,6 +3,7 @@ use crate::exec::ExecCapturePolicy;
use crate::exec::ExecExpiration;
use crate::exec::is_likely_sandbox_denied;
use crate::guardian::GuardianApprovalRequest;
use crate::guardian::guardian_rejection_message;
use crate::guardian::review_approval_request;
use crate::guardian::routes_approval_to_guardian;
use crate::sandboxing::ExecOptions;
@@ -469,7 +470,13 @@ impl CoreShellActionProvider {
}
},
ReviewDecision::Denied => {
EscalationDecision::deny(Some("User denied execution".to_string()))
let message = if routes_approval_to_guardian(&self.turn) {
guardian_rejection_message(self.session.as_ref(), &self.call_id)
.await
} else {
"User denied execution".to_string()
};
EscalationDecision::deny(Some(message))
}
ReviewDecision::Abort => {
EscalationDecision::deny(Some("User cancelled execution".to_string()))