Split DeveloperInstructions into individual fragments. (#18813)

Split DeveloperInstructions into individual fragments.
This commit is contained in:
pakrym-oai
2026-04-21 10:22:36 -07:00
committed by GitHub
Unverified
parent 5fe767e8e1
commit 2a226096f6
55 changed files with 1410 additions and 1058 deletions
-822
View File
@@ -4,20 +4,15 @@ use std::io;
use std::num::NonZeroUsize;
use std::path::Path;
use std::path::PathBuf;
use std::sync::LazyLock;
use codex_utils_image::PromptImageMode;
use codex_utils_image::load_for_prompt_bytes;
use codex_utils_template::Template;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::ser::Serializer;
use ts_rs::TS;
use crate::config_types::ApprovalsReviewer;
use crate::config_types::CollaborationMode;
use crate::config_types::SandboxMode;
use crate::permissions::FileSystemAccessMode;
use crate::permissions::FileSystemPath;
use crate::permissions::FileSystemSandboxEntry;
@@ -25,36 +20,14 @@ use crate::permissions::FileSystemSandboxKind;
use crate::permissions::FileSystemSandboxPolicy;
use crate::permissions::FileSystemSpecialPath;
use crate::permissions::NetworkSandboxPolicy;
use crate::protocol::AskForApproval;
use crate::protocol::COLLABORATION_MODE_CLOSE_TAG;
use crate::protocol::COLLABORATION_MODE_OPEN_TAG;
use crate::protocol::GranularApprovalConfig;
use crate::protocol::NetworkAccess;
use crate::protocol::REALTIME_CONVERSATION_CLOSE_TAG;
use crate::protocol::REALTIME_CONVERSATION_OPEN_TAG;
use crate::protocol::SandboxPolicy;
use crate::protocol::WritableRoot;
use crate::user_input::UserInput;
use codex_execpolicy::Policy;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_image::ImageProcessingError;
use schemars::JsonSchema;
use crate::mcp::CallToolResult;
static SANDBOX_MODE_DANGER_FULL_ACCESS_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
Template::parse(SANDBOX_MODE_DANGER_FULL_ACCESS.trim_end())
.unwrap_or_else(|err| panic!("danger-full-access sandbox template must parse: {err}"))
});
static SANDBOX_MODE_WORKSPACE_WRITE_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
Template::parse(SANDBOX_MODE_WORKSPACE_WRITE.trim_end())
.unwrap_or_else(|err| panic!("workspace-write sandbox template must parse: {err}"))
});
static SANDBOX_MODE_READ_ONLY_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
Template::parse(SANDBOX_MODE_READ_ONLY.trim_end())
.unwrap_or_else(|err| panic!("read-only sandbox template must parse: {err}"))
});
type CommitID = String;
/// Details of a ghost commit created from a repository state.
@@ -636,345 +609,6 @@ impl Default for BaseInstructions {
}
}
/// Developer-provided guidance that is injected into a turn as a developer role
/// message.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
#[serde(rename = "developer_instructions", rename_all = "snake_case")]
pub struct DeveloperInstructions {
text: String,
}
const APPROVAL_POLICY_NEVER: &str = include_str!("prompts/permissions/approval_policy/never.md");
const APPROVAL_POLICY_UNLESS_TRUSTED: &str =
include_str!("prompts/permissions/approval_policy/unless_trusted.md");
const APPROVAL_POLICY_ON_FAILURE: &str =
include_str!("prompts/permissions/approval_policy/on_failure.md");
const APPROVAL_POLICY_ON_REQUEST_RULE: &str =
include_str!("prompts/permissions/approval_policy/on_request.md");
const APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION: &str =
include_str!("prompts/permissions/approval_policy/on_request_rule_request_permission.md");
const AUTO_REVIEW_APPROVAL_SUFFIX: &str = "`approvals_reviewer` is `auto_review`: Sandbox escalations with require_escalated will be reviewed for compliance with the policy. If a rejection happens, you should proceed only with a materially safer alternative, or inform the user of the risk and send a final message to ask for approval.";
const SANDBOX_MODE_DANGER_FULL_ACCESS: &str =
include_str!("prompts/permissions/sandbox_mode/danger_full_access.md");
const SANDBOX_MODE_WORKSPACE_WRITE: &str =
include_str!("prompts/permissions/sandbox_mode/workspace_write.md");
const SANDBOX_MODE_READ_ONLY: &str = include_str!("prompts/permissions/sandbox_mode/read_only.md");
const REALTIME_START_INSTRUCTIONS: &str = include_str!("prompts/realtime/realtime_start.md");
const REALTIME_END_INSTRUCTIONS: &str = include_str!("prompts/realtime/realtime_end.md");
struct PermissionsPromptConfig<'a> {
approval_policy: AskForApproval,
approvals_reviewer: ApprovalsReviewer,
exec_policy: &'a Policy,
exec_permission_approvals_enabled: bool,
request_permissions_tool_enabled: bool,
}
impl DeveloperInstructions {
pub fn new<T: Into<String>>(text: T) -> Self {
Self { text: text.into() }
}
pub fn from(
approval_policy: AskForApproval,
approvals_reviewer: ApprovalsReviewer,
exec_policy: &Policy,
exec_permission_approvals_enabled: bool,
request_permissions_tool_enabled: bool,
) -> DeveloperInstructions {
let with_request_permissions_tool = |text: &str| {
if request_permissions_tool_enabled {
format!("{text}\n\n{}", request_permissions_tool_prompt_section())
} else {
text.to_string()
}
};
let on_request_instructions = || {
let on_request_rule = if exec_permission_approvals_enabled {
APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION.to_string()
} else {
APPROVAL_POLICY_ON_REQUEST_RULE.to_string()
};
let mut sections = vec![on_request_rule];
if request_permissions_tool_enabled {
sections.push(request_permissions_tool_prompt_section().to_string());
}
if let Some(prefixes) = approved_command_prefixes_text(exec_policy) {
sections.push(format!(
"## Approved command prefixes\nThe following prefix rules have already been approved: {prefixes}"
));
}
sections.join("\n\n")
};
let text = match approval_policy {
AskForApproval::Never => APPROVAL_POLICY_NEVER.to_string(),
AskForApproval::UnlessTrusted => {
with_request_permissions_tool(APPROVAL_POLICY_UNLESS_TRUSTED)
}
AskForApproval::OnFailure => with_request_permissions_tool(APPROVAL_POLICY_ON_FAILURE),
AskForApproval::OnRequest => on_request_instructions(),
AskForApproval::Granular(granular_config) => granular_instructions(
granular_config,
exec_policy,
exec_permission_approvals_enabled,
request_permissions_tool_enabled,
),
};
let text = if approvals_reviewer == ApprovalsReviewer::GuardianSubagent
&& approval_policy != AskForApproval::Never
{
format!("{text}\n\n{AUTO_REVIEW_APPROVAL_SUFFIX}")
} else {
text
};
DeveloperInstructions::new(text)
}
pub fn into_text(self) -> String {
self.text
}
pub fn concat(self, other: impl Into<DeveloperInstructions>) -> Self {
let mut text = self.text;
if !text.ends_with('\n') {
text.push('\n');
}
text.push_str(&other.into().text);
Self { text }
}
pub fn model_switch_message(model_instructions: String) -> Self {
DeveloperInstructions::new(format!(
"<model_switch>\nThe user was previously using a different model. Please continue the conversation according to the following instructions:\n\n{model_instructions}\n</model_switch>"
))
}
pub fn realtime_start_message() -> Self {
Self::realtime_start_message_with_instructions(REALTIME_START_INSTRUCTIONS.trim())
}
pub fn realtime_start_message_with_instructions(instructions: &str) -> Self {
DeveloperInstructions::new(format!(
"{REALTIME_CONVERSATION_OPEN_TAG}\n{instructions}\n{REALTIME_CONVERSATION_CLOSE_TAG}"
))
}
pub fn realtime_end_message(reason: &str) -> Self {
DeveloperInstructions::new(format!(
"{REALTIME_CONVERSATION_OPEN_TAG}\n{}\n\nReason: {reason}\n{REALTIME_CONVERSATION_CLOSE_TAG}",
REALTIME_END_INSTRUCTIONS.trim()
))
}
pub fn personality_spec_message(spec: String) -> Self {
let message = format!(
"<personality_spec> The user has requested a new communication style. Future messages should adhere to the following personality: \n{spec} </personality_spec>"
);
DeveloperInstructions::new(message)
}
pub fn from_policy(
sandbox_policy: &SandboxPolicy,
approval_policy: AskForApproval,
approvals_reviewer: ApprovalsReviewer,
exec_policy: &Policy,
cwd: &Path,
exec_permission_approvals_enabled: bool,
request_permissions_tool_enabled: bool,
) -> Self {
let network_access = if sandbox_policy.has_full_network_access() {
NetworkAccess::Enabled
} else {
NetworkAccess::Restricted
};
let (sandbox_mode, writable_roots) = match sandbox_policy {
SandboxPolicy::DangerFullAccess => (SandboxMode::DangerFullAccess, None),
SandboxPolicy::ReadOnly { .. } => (SandboxMode::ReadOnly, None),
SandboxPolicy::ExternalSandbox { .. } => (SandboxMode::DangerFullAccess, None),
SandboxPolicy::WorkspaceWrite { .. } => {
let roots = sandbox_policy.get_writable_roots_with_cwd(cwd);
(SandboxMode::WorkspaceWrite, Some(roots))
}
};
DeveloperInstructions::from_permissions_with_network(
sandbox_mode,
network_access,
PermissionsPromptConfig {
approval_policy,
approvals_reviewer,
exec_policy,
exec_permission_approvals_enabled,
request_permissions_tool_enabled,
},
writable_roots,
)
}
/// Returns developer instructions from a collaboration mode if they exist and are non-empty.
pub fn from_collaboration_mode(collaboration_mode: &CollaborationMode) -> Option<Self> {
collaboration_mode
.settings
.developer_instructions
.as_ref()
.filter(|instructions| !instructions.is_empty())
.map(|instructions| {
DeveloperInstructions::new(format!(
"{COLLABORATION_MODE_OPEN_TAG}{instructions}{COLLABORATION_MODE_CLOSE_TAG}"
))
})
}
fn from_permissions_with_network(
sandbox_mode: SandboxMode,
network_access: NetworkAccess,
config: PermissionsPromptConfig<'_>,
writable_roots: Option<Vec<WritableRoot>>,
) -> Self {
let start_tag = DeveloperInstructions::new("<permissions instructions>");
let end_tag = DeveloperInstructions::new("</permissions instructions>");
start_tag
.concat(DeveloperInstructions::sandbox_text(
sandbox_mode,
network_access,
))
.concat(DeveloperInstructions::from(
config.approval_policy,
config.approvals_reviewer,
config.exec_policy,
config.exec_permission_approvals_enabled,
config.request_permissions_tool_enabled,
))
.concat(DeveloperInstructions::from_writable_roots(writable_roots))
.concat(end_tag)
}
fn from_writable_roots(writable_roots: Option<Vec<WritableRoot>>) -> Self {
let Some(roots) = writable_roots else {
return DeveloperInstructions::new("");
};
if roots.is_empty() {
return DeveloperInstructions::new("");
}
let roots_list: Vec<String> = roots
.iter()
.map(|r| format!("`{}`", r.root.to_string_lossy()))
.collect();
let text = if roots_list.len() == 1 {
format!(" The writable root is {}.", roots_list[0])
} else {
format!(" The writable roots are {}.", roots_list.join(", "))
};
DeveloperInstructions::new(text)
}
fn sandbox_text(mode: SandboxMode, network_access: NetworkAccess) -> DeveloperInstructions {
let template = match mode {
SandboxMode::DangerFullAccess => &*SANDBOX_MODE_DANGER_FULL_ACCESS_TEMPLATE,
SandboxMode::WorkspaceWrite => &*SANDBOX_MODE_WORKSPACE_WRITE_TEMPLATE,
SandboxMode::ReadOnly => &*SANDBOX_MODE_READ_ONLY_TEMPLATE,
};
let network_access = network_access.to_string();
let text = template
.render([("network_access", network_access.as_str())])
.unwrap_or_else(|err| panic!("sandbox template must render: {err}"));
DeveloperInstructions::new(text)
}
}
fn approved_command_prefixes_text(exec_policy: &Policy) -> Option<String> {
format_allow_prefixes(exec_policy.get_allowed_prefixes())
.filter(|prefixes| !prefixes.is_empty())
}
fn granular_prompt_intro_text() -> &'static str {
"# Approval Requests\n\nApproval policy is `granular`. Categories set to `false` are automatically rejected instead of prompting the user."
}
fn request_permissions_tool_prompt_section() -> &'static str {
"# request_permissions Tool\n\nThe built-in `request_permissions` tool is available in this session. Invoke it when you need to request additional `network` or `file_system` permissions before later shell-like commands need them. Request only the specific permissions required for the task."
}
fn granular_instructions(
granular_config: GranularApprovalConfig,
exec_policy: &Policy,
exec_permission_approvals_enabled: bool,
request_permissions_tool_enabled: bool,
) -> String {
let sandbox_approval_prompts_allowed = granular_config.allows_sandbox_approval();
let shell_permission_requests_available =
exec_permission_approvals_enabled && sandbox_approval_prompts_allowed;
let request_permissions_tool_prompts_allowed =
request_permissions_tool_enabled && granular_config.allows_request_permissions();
let categories = [
Some((
granular_config.allows_sandbox_approval(),
"`sandbox_approval`",
)),
Some((granular_config.allows_rules_approval(), "`rules`")),
Some((granular_config.allows_skill_approval(), "`skill_approval`")),
request_permissions_tool_enabled.then_some((
granular_config.allows_request_permissions(),
"`request_permissions`",
)),
Some((
granular_config.allows_mcp_elicitations(),
"`mcp_elicitations`",
)),
];
let prompted_categories = categories
.iter()
.flatten()
.filter(|&&(is_allowed, _)| is_allowed)
.map(|&(_, category)| format!("- {category}"))
.collect::<Vec<_>>();
let rejected_categories = categories
.iter()
.flatten()
.filter(|&&(is_allowed, _)| !is_allowed)
.map(|&(_, category)| format!("- {category}"))
.collect::<Vec<_>>();
let mut sections = vec![granular_prompt_intro_text().to_string()];
if !prompted_categories.is_empty() {
sections.push(format!(
"These approval categories may still prompt the user when needed:\n{}",
prompted_categories.join("\n")
));
}
if !rejected_categories.is_empty() {
sections.push(format!(
"These approval categories are automatically rejected instead of prompting the user:\n{}",
rejected_categories.join("\n")
));
}
if shell_permission_requests_available {
sections.push(APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION.to_string());
}
if request_permissions_tool_prompts_allowed {
sections.push(request_permissions_tool_prompt_section().to_string());
}
if let Some(prefixes) = approved_command_prefixes_text(exec_policy) {
sections.push(format!(
"## Approved command prefixes\nThe following prefix rules have already been approved: {prefixes}"
));
}
sections.join("\n\n")
}
const MAX_RENDERED_PREFIXES: usize = 100;
const MAX_ALLOW_PREFIX_TEXT_BYTES: usize = 5000;
const TRUNCATED_MARKER: &str = "...\n[Some commands were truncated]";
@@ -1031,31 +665,6 @@ fn render_command_prefix(prefix: &[String]) -> String {
format!("[{tokens}]")
}
impl From<DeveloperInstructions> for ResponseItem {
fn from(di: DeveloperInstructions) -> Self {
ResponseItem::Message {
id: None,
role: "developer".to_string(),
content: vec![ContentItem::InputText {
text: di.into_text(),
}],
end_turn: None,
phase: None,
}
}
}
impl From<SandboxMode> for DeveloperInstructions {
fn from(mode: SandboxMode) -> Self {
let network_access = match mode {
SandboxMode::DangerFullAccess => NetworkAccess::Enabled,
SandboxMode::WorkspaceWrite | SandboxMode::ReadOnly => NetworkAccess::Restricted,
};
DeveloperInstructions::sandbox_text(mode, network_access)
}
}
fn should_serialize_reasoning_content(content: &Option<Vec<ReasoningItemContent>>) -> bool {
match content {
Some(content) => !content
@@ -1730,13 +1339,9 @@ impl std::fmt::Display for FunctionCallOutputPayload {
#[cfg(test)]
mod tests {
use super::*;
use crate::config_types::SandboxMode;
use crate::protocol::AskForApproval;
use crate::protocol::GranularApprovalConfig;
use anyhow::Result;
use codex_execpolicy::Policy;
use pretty_assertions::assert_eq;
use std::path::PathBuf;
use tempfile::tempdir;
#[test]
@@ -2021,433 +1626,6 @@ mod tests {
);
}
#[test]
fn converts_sandbox_mode_into_developer_instructions() {
let workspace_write: DeveloperInstructions = SandboxMode::WorkspaceWrite.into();
assert_eq!(
workspace_write,
DeveloperInstructions::new(
"Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `workspace-write`: The sandbox permits reading files, and editing files in `cwd` and `writable_roots`. Editing files in other directories requires approval. Network access is restricted."
)
);
let read_only: DeveloperInstructions = SandboxMode::ReadOnly.into();
assert_eq!(
read_only,
DeveloperInstructions::new(
"Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `read-only`: The sandbox only permits reading files. Network access is restricted."
)
);
let danger_full_access: DeveloperInstructions = SandboxMode::DangerFullAccess.into();
assert_eq!(
danger_full_access,
DeveloperInstructions::new(
"Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`: No filesystem sandboxing - all commands are permitted. Network access is enabled."
)
);
}
#[test]
fn builds_permissions_with_network_access_override() {
let instructions = DeveloperInstructions::from_permissions_with_network(
SandboxMode::WorkspaceWrite,
NetworkAccess::Enabled,
PermissionsPromptConfig {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: false,
request_permissions_tool_enabled: false,
},
/*writable_roots*/ None,
);
let text = instructions.into_text();
assert!(
text.contains("Network access is enabled."),
"expected network access to be enabled in message"
);
assert!(
text.contains("How to request escalation"),
"expected approval guidance to be included"
);
}
#[test]
fn builds_permissions_from_policy() {
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
read_only_access: Default::default(),
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};
let instructions = DeveloperInstructions::from_policy(
&policy,
AskForApproval::UnlessTrusted,
ApprovalsReviewer::User,
&Policy::empty(),
&PathBuf::from("/tmp"),
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
);
let text = instructions.into_text();
assert!(text.contains("Network access is enabled."));
assert!(text.contains("`approval_policy` is `unless-trusted`"));
}
#[test]
fn includes_request_rule_instructions_for_on_request() {
let mut exec_policy = Policy::empty();
exec_policy
.add_prefix_rule(
&["git".to_string(), "pull".to_string()],
codex_execpolicy::Decision::Allow,
)
.expect("add rule");
let instructions = DeveloperInstructions::from_permissions_with_network(
SandboxMode::WorkspaceWrite,
NetworkAccess::Enabled,
PermissionsPromptConfig {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
exec_policy: &exec_policy,
exec_permission_approvals_enabled: false,
request_permissions_tool_enabled: false,
},
/*writable_roots*/ None,
);
let text = instructions.into_text();
assert!(text.contains("prefix_rule"));
assert!(text.contains("Approved command prefixes"));
assert!(text.contains(r#"["git", "pull"]"#));
}
#[test]
fn includes_request_permissions_tool_instructions_for_unless_trusted_when_enabled() {
let instructions = DeveloperInstructions::from_permissions_with_network(
SandboxMode::WorkspaceWrite,
NetworkAccess::Enabled,
PermissionsPromptConfig {
approval_policy: AskForApproval::UnlessTrusted,
approvals_reviewer: ApprovalsReviewer::User,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: false,
request_permissions_tool_enabled: true,
},
/*writable_roots*/ None,
);
let text = instructions.into_text();
assert!(text.contains("`approval_policy` is `unless-trusted`"));
assert!(text.contains("# request_permissions Tool"));
}
#[test]
fn includes_request_permissions_tool_instructions_for_on_failure_when_enabled() {
let instructions = DeveloperInstructions::from_permissions_with_network(
SandboxMode::WorkspaceWrite,
NetworkAccess::Enabled,
PermissionsPromptConfig {
approval_policy: AskForApproval::OnFailure,
approvals_reviewer: ApprovalsReviewer::User,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: false,
request_permissions_tool_enabled: true,
},
/*writable_roots*/ None,
);
let text = instructions.into_text();
assert!(text.contains("`approval_policy` is `on-failure`"));
assert!(text.contains("# request_permissions Tool"));
}
#[test]
fn includes_request_permission_rule_instructions_for_on_request_when_enabled() {
let instructions = DeveloperInstructions::from_permissions_with_network(
SandboxMode::WorkspaceWrite,
NetworkAccess::Enabled,
PermissionsPromptConfig {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: true,
request_permissions_tool_enabled: false,
},
/*writable_roots*/ None,
);
let text = instructions.into_text();
assert!(text.contains("with_additional_permissions"));
assert!(text.contains("additional_permissions"));
}
#[test]
fn includes_request_permissions_tool_instructions_for_on_request_when_tool_is_enabled() {
let instructions = DeveloperInstructions::from_permissions_with_network(
SandboxMode::WorkspaceWrite,
NetworkAccess::Enabled,
PermissionsPromptConfig {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: false,
request_permissions_tool_enabled: true,
},
/*writable_roots*/ None,
);
let text = instructions.into_text();
assert!(text.contains("# request_permissions Tool"));
assert!(
text.contains("The built-in `request_permissions` tool is available in this session.")
);
}
#[test]
fn on_request_includes_tool_guidance_alongside_inline_permission_guidance_when_both_exist() {
let instructions = DeveloperInstructions::from_permissions_with_network(
SandboxMode::WorkspaceWrite,
NetworkAccess::Enabled,
PermissionsPromptConfig {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: true,
request_permissions_tool_enabled: true,
},
/*writable_roots*/ None,
);
let text = instructions.into_text();
assert!(text.contains("with_additional_permissions"));
assert!(text.contains("# request_permissions Tool"));
}
#[test]
fn guardian_subagent_approvals_append_guardian_specific_guidance() {
let text = DeveloperInstructions::from(
AskForApproval::OnRequest,
ApprovalsReviewer::GuardianSubagent,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
)
.into_text();
assert!(text.contains("`approvals_reviewer` is `auto_review`"));
assert!(!text.contains("`approvals_reviewer` is `guardian_subagent`"));
assert!(text.contains("materially safer alternative"));
}
#[test]
fn guardian_subagent_approvals_omit_guardian_specific_guidance_when_approval_is_never() {
let text = DeveloperInstructions::from(
AskForApproval::Never,
ApprovalsReviewer::GuardianSubagent,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
)
.into_text();
assert!(!text.contains("`approvals_reviewer` is `auto_review`"));
assert!(!text.contains("`approvals_reviewer` is `guardian_subagent`"));
}
fn granular_categories_section(title: &str, categories: &[&str]) -> String {
format!("{title}\n{}", categories.join("\n"))
}
fn granular_prompt_expected(
prompted_categories: &[&str],
rejected_categories: &[&str],
include_shell_permission_request_instructions: bool,
include_request_permissions_tool_section: bool,
) -> String {
let mut sections = vec![granular_prompt_intro_text().to_string()];
if !prompted_categories.is_empty() {
sections.push(granular_categories_section(
"These approval categories may still prompt the user when needed:",
prompted_categories,
));
}
if !rejected_categories.is_empty() {
sections.push(granular_categories_section(
"These approval categories are automatically rejected instead of prompting the user:",
rejected_categories,
));
}
if include_shell_permission_request_instructions {
sections.push(APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION.to_string());
}
if include_request_permissions_tool_section {
sections.push(request_permissions_tool_prompt_section().to_string());
}
sections.join("\n\n")
}
#[test]
fn granular_policy_lists_prompted_and_rejected_categories_separately() {
let text = DeveloperInstructions::from(
AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: false,
rules: true,
skill_approval: false,
request_permissions: true,
mcp_elicitations: false,
}),
ApprovalsReviewer::User,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ true,
/*request_permissions_tool_enabled*/ false,
)
.into_text();
assert_eq!(
text,
[
granular_prompt_intro_text().to_string(),
granular_categories_section(
"These approval categories may still prompt the user when needed:",
&["- `rules`"],
),
granular_categories_section(
"These approval categories are automatically rejected instead of prompting the user:",
&["- `sandbox_approval`", "- `skill_approval`", "- `mcp_elicitations`",],
),
]
.join("\n\n")
);
}
#[test]
fn granular_policy_includes_command_permission_instructions_when_sandbox_approval_can_prompt() {
let text = DeveloperInstructions::from(
AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: true,
rules: true,
skill_approval: true,
request_permissions: true,
mcp_elicitations: true,
}),
ApprovalsReviewer::User,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ true,
/*request_permissions_tool_enabled*/ false,
)
.into_text();
assert_eq!(
text,
granular_prompt_expected(
&[
"- `sandbox_approval`",
"- `rules`",
"- `skill_approval`",
"- `mcp_elicitations`",
],
&[],
/*include_shell_permission_request_instructions*/ true,
/*include_request_permissions_tool_section*/ false,
)
);
}
#[test]
fn granular_policy_omits_shell_permission_instructions_when_inline_requests_are_disabled() {
let text = DeveloperInstructions::from(
AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: true,
rules: true,
skill_approval: true,
request_permissions: true,
mcp_elicitations: true,
}),
ApprovalsReviewer::User,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
)
.into_text();
assert_eq!(
text,
granular_prompt_expected(
&[
"- `sandbox_approval`",
"- `rules`",
"- `skill_approval`",
"- `mcp_elicitations`",
],
&[],
/*include_shell_permission_request_instructions*/ false,
/*include_request_permissions_tool_section*/ false,
)
);
}
#[test]
fn granular_policy_includes_request_permissions_tool_only_when_that_prompt_can_still_fire() {
let allowed = DeveloperInstructions::from(
AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: true,
rules: true,
skill_approval: true,
request_permissions: true,
mcp_elicitations: true,
}),
ApprovalsReviewer::User,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ true,
/*request_permissions_tool_enabled*/ true,
)
.into_text();
assert!(allowed.contains("# request_permissions Tool"));
let rejected = DeveloperInstructions::from(
AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: true,
rules: true,
skill_approval: true,
request_permissions: false,
mcp_elicitations: true,
}),
ApprovalsReviewer::User,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ true,
/*request_permissions_tool_enabled*/ true,
)
.into_text();
assert!(!rejected.contains("# request_permissions Tool"));
}
#[test]
fn granular_policy_lists_request_permissions_category_without_tool_section_when_tool_is_unavailable()
{
let text = DeveloperInstructions::from(
AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: false,
rules: false,
skill_approval: false,
request_permissions: true,
mcp_elicitations: false,
}),
ApprovalsReviewer::User,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ true,
/*request_permissions_tool_enabled*/ false,
)
.into_text();
assert!(!text.contains("- `request_permissions`"));
assert!(!text.contains("# request_permissions Tool"));
}
#[test]
fn render_command_prefix_list_sorts_by_len_then_total_len_then_alphabetical() {
let prefixes = vec![
@@ -1 +0,0 @@
Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected.
@@ -1 +0,0 @@
Approvals are your mechanism to get user consent to run shell commands without the sandbox. `approval_policy` is `on-failure`: The harness will allow all commands to run in the sandbox (if enabled), and failures will be escalated to the user for approval to run again without the sandbox.
@@ -1,57 +0,0 @@
# Escalation Requests
Commands are run outside the sandbox if they are approved by the user, or match an existing rule that allows it to run unrestricted. The command string is split into independent command segments at shell control operators, including but not limited to:
- Pipes: |
- Logical operators: &&, ||
- Command separators: ;
- Subshell boundaries: (...), $(...)
Each resulting segment is evaluated independently for sandbox restrictions and approval requirements.
Example:
git pull | tee output.txt
This is treated as two command segments:
["git", "pull"]
["tee", "output.txt"]
Commands that use more advanced shell features like redirection (>, >>, <), substitutions ($(...), ...), environment variables (FOO=bar), or wildcard patterns (*, ?) will not be evaluated against rules, to limit the scope of what an approved rule allows.
## How to request escalation
IMPORTANT: To request approval to execute a command that will require escalated privileges:
- Provide the `sandbox_permissions` parameter with the value `"require_escalated"`
- Include a short question asking the user if they want to allow the action in `justification` parameter. e.g. "Do you want to download and install dependencies for this project?"
- Optionally suggest a `prefix_rule` - this will be shown to the user with an option to persist the rule approval for future sessions.
If you run a command that is important to solving the user's query, but it fails because of sandboxing or with a likely sandbox-related network error (for example DNS/host resolution, registry/index access, or dependency download failure), rerun the command with "require_escalated". ALWAYS proceed to use the `justification` parameter - do not message the user before requesting approval for the command.
## When to request escalation
While commands are running inside the sandbox, here are some scenarios that will require escalation outside the sandbox:
- You need to run a command that writes to a directory that requires it (e.g. running tests that write to /var)
- You need to run a GUI app (e.g., open/xdg-open/osascript) to open browsers or files.
- If you run a command that is important to solving the user's query, but it fails because of sandboxing or with a likely sandbox-related network error (for example DNS/host resolution, registry/index access, or dependency download failure), rerun the command with `require_escalated`. ALWAYS proceed to use the `sandbox_permissions` and `justification` parameters. do not message the user before requesting approval for the command.
- You are about to take a potentially destructive action such as an `rm` or `git reset` that the user did not explicitly ask for.
- Be judicious with escalating, but if completing the user's request requires it, you should do so - don't try and circumvent approvals by using other tools.
## prefix_rule guidance
When choosing a `prefix_rule`, request one that will allow you to fulfill similar requests from the user in the future without re-requesting escalation. It should be categorical and reasonably scoped to similar capabilities. You should rarely pass the entire command into `prefix_rule`.
### Banned prefix_rules
Avoid requesting overly broad prefixes that the user would be ill-advised to approve. For example, do not request ["python3"], ["python", "-"], or other similar prefixes that would allow arbitrary scripting.
NEVER provide a prefix_rule argument for destructive commands like rm.
NEVER provide a prefix_rule if your command uses a heredoc or herestring.
### Examples
Good examples of prefixes:
- ["npm", "run", "dev"]
- ["gh", "pr", "check"]
- ["cargo", "test"]
@@ -1,33 +0,0 @@
# Permission Requests
Commands may require user approval before execution. Prefer requesting sandboxed additional permissions instead of asking to run fully outside the sandbox.
## Preferred request mode
When you need extra sandboxed permissions for one command, use:
- `sandbox_permissions: "with_additional_permissions"`
- `additional_permissions` with one or more of:
- `network.enabled`: set to `true` to enable network access
- `file_system.read`: list of paths that need read access
- `file_system.write`: list of paths that need write access
When using the `request_permissions` tool directly, only request `network` and `file_system` permissions.
This keeps execution inside the current sandbox policy, while adding only the requested permissions for that command, unless an exec-policy allow rule applies and authorizes running the command outside the sandbox.
If the command already matches an exec-policy allow rule, the command can be auto-approved without an extra prompt. In that case, exec-policy allow behavior (including any sandbox bypass) takes precedence.
## Escalation Requests
Use full escalation only when sandboxed additional permissions cannot satisfy the task.
- `sandbox_permissions: "require_escalated"`
- Include `justification` as a short question asking for approval.
- Optionally include `prefix_rule` to suggest a reusable allow rule.
## Command segmentation reminder
The command string is split into independent command segments at shell control operators, including pipes (`|`), logical operators (`&&`, `||`), command separators (`;`), and subshell boundaries (`(...)`, `$()`).
Each segment is evaluated independently for sandbox restrictions and approval requirements.
@@ -1 +0,0 @@
Approvals are your mechanism to get user consent to run shell commands without the sandbox. `approval_policy` is `unless-trusted`: The harness will escalate most commands for user approval, apart from a limited allowlist of safe "read" commands.
@@ -1 +0,0 @@
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`: No filesystem sandboxing - all commands are permitted. Network access is {{network_access}}.
@@ -1 +0,0 @@
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `read-only`: The sandbox only permits reading files. Network access is {{network_access}}.
@@ -1 +0,0 @@
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `workspace-write`: The sandbox permits reading files, and editing files in `cwd` and `writable_roots`. Editing files in other directories requires approval. Network access is {{network_access}}.
@@ -1,3 +0,0 @@
Realtime conversation ended.
Subsequent user input will return to typed text rather than transcript-style text. Do not assume recognition errors or missing punctuation once realtime has ended. Resume normal chat behavior.
@@ -1,9 +0,0 @@
Realtime conversation started.
You are operating as a backend executor behind an intermediary. The user does not talk to you directly. Any response you produce will be consumed by the intermediary and may be summarized before the user sees it.
When invoked, you receive the latest conversation transcript and any relevant mode or metadata. The intermediary may invoke you even when backend help is not actually needed. Use the transcript to decide whether you should do work. If backend help is unnecessary, avoid verbose responses that add user-visible latency.
When user text is routed from realtime, treat it as a transcript. It may be unpunctuated or contain recognition errors.
- Keep responses concise and action-oriented. Your updates should help the intermediary respond to the user.