mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat(sync): backport vital protocol and model parity updates
This commit is contained in:
@@ -3570,6 +3570,7 @@ async fn handle_response_item(
|
||||
timeout_ms: action.timeout_ms,
|
||||
sandbox_permissions: None,
|
||||
prefix_rule: None,
|
||||
additional_permissions: None,
|
||||
justification: None,
|
||||
};
|
||||
let effective_call_id = match (call_id, id) {
|
||||
@@ -6838,7 +6839,8 @@ fn parse_container_exec_arguments(
|
||||
) -> Result<ExecParams, Box<ResponseInputItem>> {
|
||||
// Parse command.
|
||||
//
|
||||
// Newer prompts use `sandbox_permissions` ("use_default" | "require_escalated");
|
||||
// Newer prompts use `sandbox_permissions` ("use_default" |
|
||||
// "with_additional_permissions" | "require_escalated");
|
||||
// older ones used `with_escalated_permissions: bool`. Accept both.
|
||||
let parsed: std::result::Result<serde_json::Value, serde_json::Error> =
|
||||
serde_json::from_str(&arguments);
|
||||
|
||||
@@ -105,9 +105,9 @@ pub use crate::config_constraint::ConstraintResult;
|
||||
pub(crate) use defaults::merge_with_default_agents;
|
||||
pub(crate) use validation::upgrade_legacy_model_slugs;
|
||||
|
||||
pub(crate) const OPENAI_DEFAULT_MODEL: &str = "gpt-5.2-codex";
|
||||
const OPENAI_DEFAULT_REVIEW_MODEL: &str = "gpt-5.2-codex";
|
||||
pub const GPT_5_CODEX_MEDIUM_MODEL: &str = "gpt-5.2-codex";
|
||||
pub(crate) const OPENAI_DEFAULT_MODEL: &str = "gpt-5.3-codex";
|
||||
const OPENAI_DEFAULT_REVIEW_MODEL: &str = "gpt-5.3-codex";
|
||||
pub const GPT_5_CODEX_MEDIUM_MODEL: &str = "gpt-5.3-codex";
|
||||
|
||||
/// Maximum number of bytes of the documentation that will be embedded. Larger
|
||||
/// files are *silently truncated* to this size so we do not take up too much of
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use code_protocol::models::PermissionProfile;
|
||||
use code_protocol::models::SandboxPermissions;
|
||||
|
||||
use crate::exec_command::session_id::SessionId;
|
||||
@@ -27,6 +28,9 @@ pub struct ExecCommandParams {
|
||||
#[serde(default)]
|
||||
pub(crate) sandbox_permissions: Option<SandboxPermissions>,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) additional_permissions: Option<PermissionProfile>,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) justification: Option<String>,
|
||||
}
|
||||
|
||||
@@ -61,10 +61,14 @@ pub fn create_exec_command_tool_for_responses_api() -> ResponsesApiTool {
|
||||
"sandbox_permissions".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(
|
||||
"Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\"."
|
||||
"Sandbox permissions for the command. Use \"with_additional_permissions\" to request additional sandboxed filesystem access (preferred), or \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\"."
|
||||
.to_string(),
|
||||
),
|
||||
allowed_values: None,
|
||||
allowed_values: Some(vec![
|
||||
"use_default".to_string(),
|
||||
"with_additional_permissions".to_string(),
|
||||
"require_escalated".to_string(),
|
||||
]),
|
||||
},
|
||||
);
|
||||
properties.insert(
|
||||
@@ -77,6 +81,48 @@ pub fn create_exec_command_tool_for_responses_api() -> ResponsesApiTool {
|
||||
allowed_values: None,
|
||||
},
|
||||
);
|
||||
properties.insert(
|
||||
"additional_permissions".to_string(),
|
||||
JsonSchema::Object {
|
||||
properties: BTreeMap::from([(
|
||||
"file_system".to_string(),
|
||||
JsonSchema::Object {
|
||||
properties: BTreeMap::from([
|
||||
(
|
||||
"read".to_string(),
|
||||
JsonSchema::Array {
|
||||
items: Box::new(JsonSchema::String {
|
||||
description: None,
|
||||
allowed_values: None,
|
||||
}),
|
||||
description: Some(
|
||||
"Additional filesystem paths to grant read access for this command."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"write".to_string(),
|
||||
JsonSchema::Array {
|
||||
items: Box::new(JsonSchema::String {
|
||||
description: None,
|
||||
allowed_values: None,
|
||||
}),
|
||||
description: Some(
|
||||
"Additional filesystem paths to grant write access for this command."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
]),
|
||||
required: None,
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
)]),
|
||||
required: Some(vec!["file_system".to_string()]),
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
);
|
||||
|
||||
ResponsesApiTool {
|
||||
name: EXEC_COMMAND_TOOL_NAME.to_owned(),
|
||||
|
||||
@@ -433,9 +433,16 @@ async fn create_exec_command_session(
|
||||
shell,
|
||||
login,
|
||||
sandbox_permissions: _,
|
||||
additional_permissions,
|
||||
justification: _,
|
||||
} = params;
|
||||
|
||||
if additional_permissions.is_some() {
|
||||
tracing::debug!(
|
||||
"exec_command additional_permissions is accepted for compatibility but not yet enforced"
|
||||
);
|
||||
}
|
||||
|
||||
// Use the native pty implementation for the system
|
||||
let pty_system = native_pty_system();
|
||||
|
||||
@@ -584,6 +591,7 @@ PY"#
|
||||
shell: "/bin/bash".to_string(),
|
||||
login: false,
|
||||
sandbox_permissions: None,
|
||||
additional_permissions: None,
|
||||
justification: None,
|
||||
};
|
||||
let initial_output = match session_manager
|
||||
|
||||
@@ -568,10 +568,14 @@ fn create_shell_tool_for_sandbox(sandbox_policy: &SandboxPolicy) -> OpenAiTool {
|
||||
"sandbox_permissions".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(
|
||||
"Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\"."
|
||||
"Sandbox permissions for the command. Use \"with_additional_permissions\" to request additional sandboxed filesystem access (preferred), or \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\"."
|
||||
.to_string(),
|
||||
),
|
||||
allowed_values: Some(vec!["use_default".to_string(), "require_escalated".to_string()]),
|
||||
allowed_values: Some(vec![
|
||||
"use_default".to_string(),
|
||||
"with_additional_permissions".to_string(),
|
||||
"require_escalated".to_string(),
|
||||
]),
|
||||
},
|
||||
);
|
||||
properties.insert(
|
||||
@@ -584,6 +588,48 @@ fn create_shell_tool_for_sandbox(sandbox_policy: &SandboxPolicy) -> OpenAiTool {
|
||||
allowed_values: None,
|
||||
},
|
||||
);
|
||||
properties.insert(
|
||||
"additional_permissions".to_string(),
|
||||
JsonSchema::Object {
|
||||
properties: BTreeMap::from([(
|
||||
"file_system".to_string(),
|
||||
JsonSchema::Object {
|
||||
properties: BTreeMap::from([
|
||||
(
|
||||
"read".to_string(),
|
||||
JsonSchema::Array {
|
||||
items: Box::new(JsonSchema::String {
|
||||
description: None,
|
||||
allowed_values: None,
|
||||
}),
|
||||
description: Some(
|
||||
"Additional filesystem paths to grant read access for this command."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"write".to_string(),
|
||||
JsonSchema::Array {
|
||||
items: Box::new(JsonSchema::String {
|
||||
description: None,
|
||||
allowed_values: None,
|
||||
}),
|
||||
description: Some(
|
||||
"Additional filesystem paths to grant write access for this command."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
]),
|
||||
required: None,
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
)]),
|
||||
required: Some(vec!["file_system".to_string()]),
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let description = match sandbox_policy {
|
||||
@@ -616,6 +662,9 @@ The shell tool is used to execute shell commands.
|
||||
- When invoking a command that will require escalated privileges:
|
||||
- Provide the sandbox_permissions parameter with the value \"require_escalated\"
|
||||
- Include a short, 1 sentence explanation for why we need escalated permissions in the justification parameter.
|
||||
- When additional sandboxed filesystem access is enough:
|
||||
- Provide the sandbox_permissions parameter with the value \"with_additional_permissions\"
|
||||
- Provide additional_permissions.file_system.read and/or additional_permissions.file_system.write with the minimal paths needed.
|
||||
|
||||
Long-running commands may be backgrounded after an initial window. Use `wait` to await background tasks. Optional `timeout` can set a hard kill if needed."#,
|
||||
roots_str,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use code_utils_image::load_and_resize_to_fit;
|
||||
use serde::Deserialize;
|
||||
@@ -35,12 +36,84 @@ pub enum SandboxPermissions {
|
||||
UseDefault,
|
||||
/// Request to run outside the sandbox
|
||||
RequireEscalated,
|
||||
/// Request additional sandbox permissions for this command.
|
||||
WithAdditionalPermissions,
|
||||
}
|
||||
|
||||
impl SandboxPermissions {
|
||||
pub fn requires_escalated_permissions(self) -> bool {
|
||||
matches!(self, SandboxPermissions::RequireEscalated)
|
||||
}
|
||||
|
||||
pub fn requires_additional_permissions(self) -> bool {
|
||||
!matches!(self, SandboxPermissions::UseDefault)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
pub struct FileSystemPermissions {
|
||||
pub read: Option<Vec<PathBuf>>,
|
||||
pub write: Option<Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
impl FileSystemPermissions {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.read.is_none() && self.write.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
pub struct MacOsPermissions {
|
||||
pub preferences: Option<MacOsPreferencesValue>,
|
||||
pub automations: Option<MacOsAutomationValue>,
|
||||
pub accessibility: Option<bool>,
|
||||
pub calendar: Option<bool>,
|
||||
}
|
||||
|
||||
impl MacOsPermissions {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.preferences.is_none()
|
||||
&& self.automations.is_none()
|
||||
&& self.accessibility.is_none()
|
||||
&& self.calendar.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[serde(untagged)]
|
||||
pub enum MacOsPreferencesValue {
|
||||
Bool(bool),
|
||||
Mode(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[serde(untagged)]
|
||||
pub enum MacOsAutomationValue {
|
||||
Bool(bool),
|
||||
BundleIds(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
pub struct PermissionProfile {
|
||||
pub network: Option<bool>,
|
||||
pub file_system: Option<FileSystemPermissions>,
|
||||
pub macos: Option<MacOsPermissions>,
|
||||
}
|
||||
|
||||
impl PermissionProfile {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.network.is_none()
|
||||
&& self
|
||||
.file_system
|
||||
.as_ref()
|
||||
.map(FileSystemPermissions::is_empty)
|
||||
.unwrap_or(true)
|
||||
&& self
|
||||
.macos
|
||||
.as_ref()
|
||||
.map(MacOsPermissions::is_empty)
|
||||
.unwrap_or(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
||||
@@ -227,6 +300,8 @@ 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_rule.md");
|
||||
const APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION: &str =
|
||||
include_str!("prompts/permissions/approval_policy/on_request_rule_request_permission.md");
|
||||
|
||||
const SANDBOX_MODE_DANGER_FULL_ACCESS: &str =
|
||||
include_str!("prompts/permissions/sandbox_mode/danger_full_access.md");
|
||||
@@ -239,16 +314,25 @@ impl DeveloperInstructions {
|
||||
Self { text: text.into() }
|
||||
}
|
||||
|
||||
pub fn from(approval_policy: AskForApproval, exec_policy: &Policy) -> DeveloperInstructions {
|
||||
pub fn from(
|
||||
approval_policy: AskForApproval,
|
||||
exec_policy: &Policy,
|
||||
request_permission_enabled: bool,
|
||||
) -> DeveloperInstructions {
|
||||
let on_request_instructions = || {
|
||||
let on_request_rule = if request_permission_enabled {
|
||||
APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION
|
||||
} else {
|
||||
APPROVAL_POLICY_ON_REQUEST_RULE
|
||||
};
|
||||
let command_prefixes = format_allow_prefixes(exec_policy.get_allowed_prefixes());
|
||||
match command_prefixes {
|
||||
Some(prefixes) => {
|
||||
format!(
|
||||
"{APPROVAL_POLICY_ON_REQUEST_RULE}\n## Approved command prefixes\nThe following prefix rules have already been approved: {prefixes}"
|
||||
"{on_request_rule}\n## Approved command prefixes\nThe following prefix rules have already been approved: {prefixes}"
|
||||
)
|
||||
}
|
||||
None => APPROVAL_POLICY_ON_REQUEST_RULE.to_string(),
|
||||
None => on_request_rule.to_string(),
|
||||
}
|
||||
};
|
||||
let text = match approval_policy {
|
||||
@@ -306,6 +390,7 @@ impl DeveloperInstructions {
|
||||
approval_policy: AskForApproval,
|
||||
exec_policy: &Policy,
|
||||
cwd: &Path,
|
||||
request_permission_enabled: bool,
|
||||
) -> Self {
|
||||
let network_access = if sandbox_policy.has_full_network_access() {
|
||||
NetworkAccess::Enabled
|
||||
@@ -329,6 +414,7 @@ impl DeveloperInstructions {
|
||||
approval_policy,
|
||||
exec_policy,
|
||||
writable_roots,
|
||||
request_permission_enabled,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -352,6 +438,7 @@ impl DeveloperInstructions {
|
||||
approval_policy: AskForApproval,
|
||||
exec_policy: &Policy,
|
||||
writable_roots: Option<Vec<WritableRoot>>,
|
||||
request_permission_enabled: bool,
|
||||
) -> Self {
|
||||
let start_tag = DeveloperInstructions::new("<permissions instructions>");
|
||||
let end_tag = DeveloperInstructions::new("</permissions instructions>");
|
||||
@@ -360,7 +447,11 @@ impl DeveloperInstructions {
|
||||
sandbox_mode,
|
||||
network_access,
|
||||
))
|
||||
.concat(DeveloperInstructions::from(approval_policy, exec_policy))
|
||||
.concat(DeveloperInstructions::from(
|
||||
approval_policy,
|
||||
exec_policy,
|
||||
request_permission_enabled,
|
||||
))
|
||||
.concat(DeveloperInstructions::from_writable_roots(writable_roots))
|
||||
.concat(end_tag)
|
||||
}
|
||||
@@ -754,6 +845,9 @@ pub struct ShellToolCallParams {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub prefix_rule: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub additional_permissions: Option<PermissionProfile>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub justification: Option<String>,
|
||||
}
|
||||
@@ -777,6 +871,9 @@ pub struct ShellCommandToolCallParams {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub prefix_rule: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub additional_permissions: Option<PermissionProfile>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub justification: Option<String>,
|
||||
}
|
||||
@@ -1220,6 +1317,7 @@ mod tests {
|
||||
AskForApproval::OnRequest,
|
||||
&empty_exec_policy(),
|
||||
None,
|
||||
false,
|
||||
);
|
||||
|
||||
let text = instructions.into_text();
|
||||
@@ -1248,6 +1346,7 @@ mod tests {
|
||||
AskForApproval::UnlessTrusted,
|
||||
&empty_exec_policy(),
|
||||
&PathBuf::from("/tmp"),
|
||||
false,
|
||||
);
|
||||
let text = instructions.into_text();
|
||||
assert!(text.contains("Network access is enabled."));
|
||||
@@ -1263,6 +1362,7 @@ mod tests {
|
||||
AskForApproval::OnRequest,
|
||||
&exec_policy,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
|
||||
let text = instructions.into_text();
|
||||
@@ -1574,6 +1674,7 @@ mod tests {
|
||||
timeout_ms: Some(1000),
|
||||
sandbox_permissions: None,
|
||||
prefix_rule: None,
|
||||
additional_permissions: None,
|
||||
justification: None,
|
||||
},
|
||||
params
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
//! are used to preserve compatibility when older payloads omit newly introduced attributes.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
@@ -420,32 +419,18 @@ impl ModelPreset {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Merge remote presets with existing presets, preferring remote when slugs match.
|
||||
/// Recompute the single default preset using picker visibility.
|
||||
///
|
||||
/// Remote presets take precedence. Existing presets not in remote are appended with `is_default` set to false.
|
||||
pub fn merge(
|
||||
remote_presets: Vec<ModelPreset>,
|
||||
existing_presets: Vec<ModelPreset>,
|
||||
) -> Vec<ModelPreset> {
|
||||
if remote_presets.is_empty() {
|
||||
return existing_presets;
|
||||
}
|
||||
|
||||
let remote_slugs: HashSet<&str> = remote_presets
|
||||
.iter()
|
||||
.map(|preset| preset.model.as_str())
|
||||
.collect();
|
||||
|
||||
let mut merged_presets = remote_presets.clone();
|
||||
for mut preset in existing_presets {
|
||||
if remote_slugs.contains(preset.model.as_str()) {
|
||||
continue;
|
||||
}
|
||||
/// The first picker-visible model wins; if none are picker-visible, the first model wins.
|
||||
pub fn mark_default_by_picker_visibility(models: &mut [ModelPreset]) {
|
||||
for preset in models.iter_mut() {
|
||||
preset.is_default = false;
|
||||
merged_presets.push(preset);
|
||||
}
|
||||
|
||||
merged_presets
|
||||
if let Some(default) = models.iter_mut().find(|preset| preset.show_in_picker) {
|
||||
default.is_default = true;
|
||||
} else if let Some(default) = models.first_mut() {
|
||||
default.is_default = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
# 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 filesystem access for one command, use:
|
||||
|
||||
- `sandbox_permissions: "with_additional_permissions"`
|
||||
- `additional_permissions` with one or both fields:
|
||||
- `file_system.read`: list of paths that need read access
|
||||
- `file_system.write`: list of paths that need write access
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user