mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
permissions: move workspace roots onto thread state
This commit is contained in:
@@ -335,21 +335,6 @@ pub struct ActivePermissionProfile {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub extends: Option<String>,
|
||||
|
||||
/// Bounded user-requested modifications applied on top of the named
|
||||
/// profile, if any.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub modifications: Vec<ActivePermissionProfileModification>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[ts(tag = "type")]
|
||||
pub enum ActivePermissionProfileModification {
|
||||
/// Additional concrete directory that should be writable.
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
AdditionalWritableRoot { path: AbsolutePathBuf },
|
||||
}
|
||||
|
||||
impl ActivePermissionProfile {
|
||||
@@ -357,17 +342,8 @@ impl ActivePermissionProfile {
|
||||
Self {
|
||||
id: id.into(),
|
||||
extends: None,
|
||||
modifications: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_modifications(
|
||||
mut self,
|
||||
modifications: Vec<ActivePermissionProfileModification>,
|
||||
) -> Self {
|
||||
self.modifications = modifications;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PermissionProfile {
|
||||
@@ -539,6 +515,20 @@ impl PermissionProfile {
|
||||
self.network_sandbox_policy(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn materialize_project_roots_with_workspace_roots(
|
||||
&self,
|
||||
workspace_roots: &[AbsolutePathBuf],
|
||||
) -> Self {
|
||||
let (file_system_sandbox_policy, network_sandbox_policy) = self.to_runtime_permissions();
|
||||
let file_system_sandbox_policy = file_system_sandbox_policy
|
||||
.materialize_project_roots_with_workspace_roots(workspace_roots);
|
||||
Self::from_runtime_permissions_with_enforcement(
|
||||
self.enforcement(),
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
|
||||
@@ -576,27 +576,16 @@ impl FileSystemSandboxPolicy {
|
||||
/// into split filesystem policy.
|
||||
pub fn from_legacy_sandbox_policy_for_cwd(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Self {
|
||||
let mut file_system_policy = Self::from(sandbox_policy);
|
||||
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = sandbox_policy {
|
||||
if let Ok(cwd_root) = AbsolutePathBuf::from_absolute_path(cwd) {
|
||||
for protected_path in default_read_only_subpaths_for_writable_root(
|
||||
&cwd_root, /*protect_missing_dot_codex*/ true,
|
||||
) {
|
||||
append_default_read_only_path_if_no_explicit_rule(
|
||||
&mut file_system_policy.entries,
|
||||
protected_path,
|
||||
);
|
||||
}
|
||||
}
|
||||
for writable_root in writable_roots {
|
||||
for protected_path in default_read_only_subpaths_for_writable_root(
|
||||
writable_root,
|
||||
/*protect_missing_dot_codex*/ false,
|
||||
) {
|
||||
append_default_read_only_path_if_no_explicit_rule(
|
||||
&mut file_system_policy.entries,
|
||||
protected_path,
|
||||
);
|
||||
}
|
||||
if let SandboxPolicy::WorkspaceWrite { .. } = sandbox_policy
|
||||
&& let Ok(cwd_root) = AbsolutePathBuf::from_absolute_path(cwd)
|
||||
{
|
||||
for protected_path in default_read_only_subpaths_for_writable_root(
|
||||
&cwd_root, /*protect_missing_dot_codex*/ true,
|
||||
) {
|
||||
append_default_read_only_path_if_no_explicit_rule(
|
||||
&mut file_system_policy.entries,
|
||||
protected_path,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -717,6 +706,38 @@ impl FileSystemSandboxPolicy {
|
||||
self
|
||||
}
|
||||
|
||||
/// Replaces symbolic `:project_roots` entries with concrete entries for
|
||||
/// each workspace root.
|
||||
pub fn materialize_project_roots_with_workspace_roots(
|
||||
mut self,
|
||||
workspace_roots: &[AbsolutePathBuf],
|
||||
) -> Self {
|
||||
let mut entries = Vec::with_capacity(self.entries.len());
|
||||
for entry in self.entries {
|
||||
let FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::ProjectRoots { subpath },
|
||||
} = &entry.path
|
||||
else {
|
||||
entries.push(entry);
|
||||
continue;
|
||||
};
|
||||
|
||||
entries.extend(workspace_roots.iter().map(|root| FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: match subpath.as_ref() {
|
||||
Some(subpath) => {
|
||||
AbsolutePathBuf::resolve_path_against_base(subpath, root.as_path())
|
||||
}
|
||||
None => root.clone(),
|
||||
},
|
||||
},
|
||||
access: entry.access,
|
||||
}));
|
||||
}
|
||||
self.entries = entries;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_additional_readable_roots(
|
||||
mut self,
|
||||
cwd: &Path,
|
||||
@@ -1032,10 +1053,10 @@ impl FileSystemSandboxPolicy {
|
||||
let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd).ok();
|
||||
let has_full_disk_write_access = self.has_full_disk_write_access();
|
||||
let mut workspace_root_writable = false;
|
||||
let mut writable_roots = Vec::new();
|
||||
let mut tmpdir_writable = false;
|
||||
let mut slash_tmp_writable = false;
|
||||
let mut unbridgeable_root_write = false;
|
||||
let mut unbridgeable_external_write = false;
|
||||
|
||||
for entry in &self.entries {
|
||||
match &entry.path {
|
||||
@@ -1045,7 +1066,7 @@ impl FileSystemSandboxPolicy {
|
||||
if cwd_absolute.as_ref().is_some_and(|cwd| cwd == path) {
|
||||
workspace_root_writable = true;
|
||||
} else {
|
||||
writable_roots.push(path.clone());
|
||||
unbridgeable_external_write = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1061,11 +1082,8 @@ impl FileSystemSandboxPolicy {
|
||||
FileSystemSpecialPath::ProjectRoots { subpath } => {
|
||||
if subpath.is_none() && entry.access.can_write() {
|
||||
workspace_root_writable = true;
|
||||
} else if let Some(path) =
|
||||
resolve_file_system_special_path(value, cwd_absolute.as_ref())
|
||||
&& entry.access.can_write()
|
||||
{
|
||||
writable_roots.push(path);
|
||||
} else if entry.access.can_write() {
|
||||
unbridgeable_external_write = true;
|
||||
}
|
||||
}
|
||||
FileSystemSpecialPath::Tmpdir => {
|
||||
@@ -1093,21 +1111,20 @@ impl FileSystemSandboxPolicy {
|
||||
});
|
||||
}
|
||||
|
||||
if unbridgeable_root_write || unbridgeable_external_write {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"permissions profile requests filesystem writes outside the workspace root, which is not supported until the runtime enforces FileSystemSandboxPolicy directly",
|
||||
));
|
||||
}
|
||||
|
||||
if workspace_root_writable {
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: dedup_absolute_paths(
|
||||
writable_roots,
|
||||
/*normalize_effective_paths*/ false,
|
||||
),
|
||||
network_access: network_policy.is_enabled(),
|
||||
exclude_tmpdir_env_var: !tmpdir_writable,
|
||||
exclude_slash_tmp: !slash_tmp_writable,
|
||||
}
|
||||
} else if unbridgeable_root_write
|
||||
|| !writable_roots.is_empty()
|
||||
|| tmpdir_writable
|
||||
|| slash_tmp_writable
|
||||
{
|
||||
} else if tmpdir_writable || slash_tmp_writable {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"permissions profile requests filesystem writes outside the workspace root, which is not supported until the runtime enforces FileSystemSandboxPolicy directly",
|
||||
@@ -1173,12 +1190,11 @@ impl From<&SandboxPolicy> for FileSystemSandboxPolicy {
|
||||
}])
|
||||
}
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots,
|
||||
exclude_tmpdir_env_var,
|
||||
exclude_slash_tmp,
|
||||
..
|
||||
} => FileSystemSandboxPolicy::workspace_write(
|
||||
writable_roots,
|
||||
&[],
|
||||
*exclude_tmpdir_env_var,
|
||||
*exclude_slash_tmp,
|
||||
),
|
||||
@@ -1485,7 +1501,6 @@ fn legacy_runtime_file_system_policy_for_cwd(
|
||||
cwd: &Path,
|
||||
) -> FileSystemSandboxPolicy {
|
||||
let SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots,
|
||||
exclude_tmpdir_env_var,
|
||||
exclude_slash_tmp,
|
||||
..
|
||||
@@ -1525,16 +1540,6 @@ fn legacy_runtime_file_system_policy_for_cwd(
|
||||
access: FileSystemAccessMode::Write,
|
||||
});
|
||||
}
|
||||
entries.extend(
|
||||
writable_roots
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|path| FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path },
|
||||
access: FileSystemAccessMode::Write,
|
||||
}),
|
||||
);
|
||||
|
||||
if let Ok(cwd_root) = AbsolutePathBuf::from_absolute_path(cwd) {
|
||||
for protected_path in default_read_only_subpaths_for_writable_root(
|
||||
&cwd_root, /*protect_missing_dot_codex*/ true,
|
||||
@@ -1542,14 +1547,6 @@ fn legacy_runtime_file_system_policy_for_cwd(
|
||||
append_default_read_only_path_if_no_explicit_rule(&mut entries, protected_path);
|
||||
}
|
||||
}
|
||||
for writable_root in writable_roots {
|
||||
for protected_path in default_read_only_subpaths_for_writable_root(
|
||||
writable_root,
|
||||
/*protect_missing_dot_codex*/ false,
|
||||
) {
|
||||
append_default_read_only_path_if_no_explicit_rule(&mut entries, protected_path);
|
||||
}
|
||||
}
|
||||
|
||||
FileSystemSandboxPolicy::restricted(entries)
|
||||
}
|
||||
@@ -1808,6 +1805,29 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_bridge_rejects_extra_writable_root_when_cwd_is_writable() {
|
||||
let cwd = TempDir::new().expect("tempdir");
|
||||
let cwd_path = AbsolutePathBuf::from_absolute_path(cwd.path()).expect("absolute cwd");
|
||||
let extra_root = AbsolutePathBuf::resolve_path_against_base("extra-root", cwd.path());
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path: cwd_path },
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path: extra_root },
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
]);
|
||||
|
||||
let err = policy
|
||||
.to_legacy_sandbox_policy(NetworkSandboxPolicy::Restricted, cwd.path())
|
||||
.expect_err("extra writable roots cannot be represented by WorkspaceWrite");
|
||||
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn writable_roots_proactively_protect_missing_dot_codex() {
|
||||
@@ -1838,7 +1858,6 @@ mod tests {
|
||||
#[test]
|
||||
fn legacy_workspace_write_projection_preserves_symbolic_project_root() {
|
||||
let policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: Vec::new(),
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
@@ -1994,7 +2013,6 @@ mod tests {
|
||||
)
|
||||
.expect("absolute root");
|
||||
let policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
@@ -2510,7 +2528,6 @@ mod tests {
|
||||
fn legacy_projection_runtime_enforcement_ignores_entry_order() {
|
||||
let cwd = TempDir::new().expect("tempdir");
|
||||
let legacy_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: Vec::new(),
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
@@ -2537,7 +2554,6 @@ mod tests {
|
||||
fn missing_symbolic_metadata_carveouts_need_direct_runtime_enforcement() {
|
||||
let cwd = TempDir::new().expect("tempdir");
|
||||
let legacy_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: Vec::new(),
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
|
||||
@@ -462,6 +462,10 @@ pub enum Op {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cwd: Option<PathBuf>,
|
||||
|
||||
/// Updated workspace roots for sandbox/tool calls.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
workspace_roots: Option<Vec<AbsolutePathBuf>>,
|
||||
|
||||
/// Updated command approval policy.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
approval_policy: Option<AskForApproval>,
|
||||
@@ -1004,14 +1008,9 @@ pub enum SandboxPolicy {
|
||||
},
|
||||
|
||||
/// Same as `ReadOnly` but additionally grants write access to the current
|
||||
/// working directory ("workspace").
|
||||
/// thread workspace roots.
|
||||
#[serde(rename = "workspace-write")]
|
||||
WorkspaceWrite {
|
||||
/// Additional folders (beyond cwd and possibly TMPDIR) that should be
|
||||
/// writable from within the sandbox.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
writable_roots: Vec<AbsolutePathBuf>,
|
||||
|
||||
/// When set to `true`, outbound network access is allowed. `false` by
|
||||
/// default.
|
||||
#[serde(default)]
|
||||
@@ -1121,7 +1120,6 @@ impl SandboxPolicy {
|
||||
/// not allow network access.
|
||||
pub fn new_workspace_write_policy() -> Self {
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
@@ -1159,13 +1157,11 @@ impl SandboxPolicy {
|
||||
SandboxPolicy::ExternalSandbox { .. } => Vec::new(),
|
||||
SandboxPolicy::ReadOnly { .. } => Vec::new(),
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots,
|
||||
exclude_tmpdir_env_var,
|
||||
exclude_slash_tmp,
|
||||
network_access: _,
|
||||
} => {
|
||||
// Start from explicitly configured writable roots.
|
||||
let mut roots: Vec<AbsolutePathBuf> = writable_roots.clone();
|
||||
let mut roots: Vec<AbsolutePathBuf> = Vec::new();
|
||||
|
||||
// Always include defaults: cwd, /tmp (if present on Unix), and
|
||||
// on macOS, the per-user TMPDIR unless explicitly excluded.
|
||||
@@ -2704,6 +2700,8 @@ pub struct SessionMeta {
|
||||
pub forked_from_id: Option<ThreadId>,
|
||||
pub timestamp: String,
|
||||
pub cwd: PathBuf,
|
||||
#[serde(default)]
|
||||
pub workspace_roots: Vec<AbsolutePathBuf>,
|
||||
pub originator: String,
|
||||
pub cli_version: String,
|
||||
#[serde(default)]
|
||||
@@ -2738,6 +2736,7 @@ impl Default for SessionMeta {
|
||||
forked_from_id: None,
|
||||
timestamp: String::new(),
|
||||
cwd: PathBuf::new(),
|
||||
workspace_roots: Vec::new(),
|
||||
originator: String::new(),
|
||||
cli_version: String::new(),
|
||||
source: SessionSource::default(),
|
||||
@@ -2801,13 +2800,15 @@ pub struct TurnContextNetworkItem {
|
||||
/// context updates, and again after mid-turn compaction when replacement
|
||||
/// history re-establishes full context, so resume/fork replay can recover the
|
||||
/// latest durable baseline.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
|
||||
#[derive(Serialize, Clone, Debug, JsonSchema, TS)]
|
||||
pub struct TurnContextItem {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub turn_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub trace_id: Option<String>,
|
||||
pub cwd: PathBuf,
|
||||
#[serde(default)]
|
||||
pub workspace_roots: Vec<AbsolutePathBuf>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub current_date: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -2816,6 +2817,8 @@ pub struct TurnContextItem {
|
||||
pub sandbox_policy: SandboxPolicy,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub permission_profile: Option<PermissionProfile>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub active_permission_profile: Option<ActivePermissionProfile>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub network: Option<TurnContextNetworkItem>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -2840,6 +2843,181 @@ pub struct TurnContextItem {
|
||||
pub truncation_policy: Option<TruncationPolicy>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct RolloutSandboxPolicy {
|
||||
policy: SandboxPolicy,
|
||||
legacy_writable_roots: Vec<AbsolutePathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
enum RolloutSandboxPolicyWire {
|
||||
DangerFullAccess,
|
||||
ReadOnly {
|
||||
#[serde(default)]
|
||||
network_access: bool,
|
||||
},
|
||||
ExternalSandbox {
|
||||
#[serde(default)]
|
||||
network_access: NetworkAccess,
|
||||
},
|
||||
WorkspaceWrite {
|
||||
#[serde(default)]
|
||||
writable_roots: Vec<AbsolutePathBuf>,
|
||||
#[serde(default)]
|
||||
network_access: bool,
|
||||
#[serde(default)]
|
||||
exclude_tmpdir_env_var: bool,
|
||||
#[serde(default)]
|
||||
exclude_slash_tmp: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<RolloutSandboxPolicyWire> for RolloutSandboxPolicy {
|
||||
fn from(value: RolloutSandboxPolicyWire) -> Self {
|
||||
match value {
|
||||
RolloutSandboxPolicyWire::DangerFullAccess => Self {
|
||||
policy: SandboxPolicy::DangerFullAccess,
|
||||
legacy_writable_roots: Vec::new(),
|
||||
},
|
||||
RolloutSandboxPolicyWire::ReadOnly { network_access } => Self {
|
||||
policy: SandboxPolicy::ReadOnly { network_access },
|
||||
legacy_writable_roots: Vec::new(),
|
||||
},
|
||||
RolloutSandboxPolicyWire::ExternalSandbox { network_access } => Self {
|
||||
policy: SandboxPolicy::ExternalSandbox { network_access },
|
||||
legacy_writable_roots: Vec::new(),
|
||||
},
|
||||
RolloutSandboxPolicyWire::WorkspaceWrite {
|
||||
writable_roots,
|
||||
network_access,
|
||||
exclude_tmpdir_env_var,
|
||||
exclude_slash_tmp,
|
||||
} => Self {
|
||||
policy: SandboxPolicy::WorkspaceWrite {
|
||||
network_access,
|
||||
exclude_tmpdir_env_var,
|
||||
exclude_slash_tmp,
|
||||
},
|
||||
legacy_writable_roots: writable_roots,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for RolloutSandboxPolicy {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
Ok(RolloutSandboxPolicyWire::deserialize(deserializer)?.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl RolloutSandboxPolicy {
|
||||
fn workspace_roots_for_legacy_workspace_write(
|
||||
&self,
|
||||
cwd: &Path,
|
||||
) -> Option<Vec<AbsolutePathBuf>> {
|
||||
if !matches!(self.policy, SandboxPolicy::WorkspaceWrite { .. }) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut roots = Vec::new();
|
||||
if let Ok(cwd) = AbsolutePathBuf::from_absolute_path(cwd) {
|
||||
roots.push(cwd);
|
||||
}
|
||||
for root in &self.legacy_writable_roots {
|
||||
if !roots.iter().any(|existing| existing == root) {
|
||||
roots.push(root.clone());
|
||||
}
|
||||
}
|
||||
Some(roots)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for TurnContextItem {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Wire {
|
||||
#[serde(default)]
|
||||
turn_id: Option<String>,
|
||||
#[serde(default)]
|
||||
trace_id: Option<String>,
|
||||
cwd: PathBuf,
|
||||
#[serde(default)]
|
||||
workspace_roots: Option<Vec<AbsolutePathBuf>>,
|
||||
current_date: Option<String>,
|
||||
timezone: Option<String>,
|
||||
approval_policy: AskForApproval,
|
||||
sandbox_policy: RolloutSandboxPolicy,
|
||||
#[serde(default)]
|
||||
permission_profile: Option<PermissionProfile>,
|
||||
#[serde(default)]
|
||||
active_permission_profile: Option<ActivePermissionProfile>,
|
||||
network: Option<TurnContextNetworkItem>,
|
||||
#[serde(default)]
|
||||
file_system_sandbox_policy: Option<FileSystemSandboxPolicy>,
|
||||
model: String,
|
||||
personality: Option<Personality>,
|
||||
#[serde(default)]
|
||||
collaboration_mode: Option<CollaborationMode>,
|
||||
#[serde(default)]
|
||||
realtime_active: Option<bool>,
|
||||
effort: Option<ReasoningEffortConfig>,
|
||||
summary: ReasoningSummaryConfig,
|
||||
user_instructions: Option<String>,
|
||||
developer_instructions: Option<String>,
|
||||
final_output_json_schema: Option<Value>,
|
||||
#[serde(default)]
|
||||
truncation_policy: Option<TruncationPolicy>,
|
||||
}
|
||||
|
||||
let wire = Wire::deserialize(deserializer)?;
|
||||
let workspace_roots = match wire.workspace_roots {
|
||||
Some(workspace_roots) => workspace_roots,
|
||||
None => wire
|
||||
.sandbox_policy
|
||||
.workspace_roots_for_legacy_workspace_write(&wire.cwd)
|
||||
.unwrap_or_else(|| {
|
||||
vec![
|
||||
AbsolutePathBuf::relative_to_current_dir(&wire.cwd).unwrap_or_else(|_| {
|
||||
AbsolutePathBuf::resolve_path_against_base(&wire.cwd, "/")
|
||||
}),
|
||||
]
|
||||
}),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
turn_id: wire.turn_id,
|
||||
trace_id: wire.trace_id,
|
||||
cwd: wire.cwd,
|
||||
workspace_roots,
|
||||
current_date: wire.current_date,
|
||||
timezone: wire.timezone,
|
||||
approval_policy: wire.approval_policy,
|
||||
sandbox_policy: wire.sandbox_policy.policy,
|
||||
permission_profile: wire.permission_profile,
|
||||
active_permission_profile: wire.active_permission_profile,
|
||||
network: wire.network,
|
||||
file_system_sandbox_policy: wire.file_system_sandbox_policy,
|
||||
model: wire.model,
|
||||
personality: wire.personality,
|
||||
collaboration_mode: wire.collaboration_mode,
|
||||
realtime_active: wire.realtime_active,
|
||||
effort: wire.effort,
|
||||
summary: wire.summary,
|
||||
user_instructions: wire.user_instructions,
|
||||
developer_instructions: wire.developer_instructions,
|
||||
final_output_json_schema: wire.final_output_json_schema,
|
||||
truncation_policy: wire.truncation_policy,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TurnContextItem {
|
||||
pub fn permission_profile(&self) -> PermissionProfile {
|
||||
self.permission_profile.clone().unwrap_or_else(|| {
|
||||
@@ -3448,6 +3626,10 @@ pub struct SessionConfiguredEvent {
|
||||
/// session.
|
||||
pub cwd: AbsolutePathBuf,
|
||||
|
||||
/// Workspace roots used to realize symbolic `:project_roots` entries.
|
||||
#[serde(default)]
|
||||
pub workspace_roots: Vec<AbsolutePathBuf>,
|
||||
|
||||
/// The effort the model is putting into reasoning about the user's request.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reasoning_effort: Option<ReasoningEffortConfig>,
|
||||
@@ -3491,11 +3673,13 @@ impl<'de> Deserialize<'de> for SessionConfiguredEvent {
|
||||
// `SessionConfiguredEvent` is persisted into rollout history. Older
|
||||
// rollouts only have `sandbox_policy`, so accept it on deserialize
|
||||
// and immediately project it into the canonical `permission_profile`.
|
||||
sandbox_policy: Option<SandboxPolicy>,
|
||||
sandbox_policy: Option<RolloutSandboxPolicy>,
|
||||
permission_profile: Option<PermissionProfile>,
|
||||
#[serde(default)]
|
||||
active_permission_profile: Option<ActivePermissionProfile>,
|
||||
cwd: AbsolutePathBuf,
|
||||
#[serde(default)]
|
||||
workspace_roots: Option<Vec<AbsolutePathBuf>>,
|
||||
reasoning_effort: Option<ReasoningEffortConfig>,
|
||||
initial_messages: Option<Vec<EventMsg>>,
|
||||
network_proxy: Option<SessionNetworkProxyRuntime>,
|
||||
@@ -3503,16 +3687,24 @@ impl<'de> Deserialize<'de> for SessionConfiguredEvent {
|
||||
}
|
||||
|
||||
let wire = Wire::deserialize(deserializer)?;
|
||||
let permission_profile = match (wire.permission_profile, wire.sandbox_policy) {
|
||||
let permission_profile = match (wire.permission_profile, wire.sandbox_policy.as_ref()) {
|
||||
(Some(permission_profile), _) => permission_profile,
|
||||
(None, Some(sandbox_policy)) => PermissionProfile::from_legacy_sandbox_policy_for_cwd(
|
||||
&sandbox_policy,
|
||||
&sandbox_policy.policy,
|
||||
wire.cwd.as_path(),
|
||||
),
|
||||
(None, None) => {
|
||||
return Err(serde::de::Error::missing_field("permission_profile"));
|
||||
}
|
||||
};
|
||||
let workspace_roots = wire.workspace_roots.unwrap_or_else(|| {
|
||||
wire.sandbox_policy
|
||||
.as_ref()
|
||||
.and_then(|sandbox_policy| {
|
||||
sandbox_policy.workspace_roots_for_legacy_workspace_write(wire.cwd.as_path())
|
||||
})
|
||||
.unwrap_or_else(|| vec![wire.cwd.clone()])
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
session_id: wire.session_id,
|
||||
@@ -3528,6 +3720,7 @@ impl<'de> Deserialize<'de> for SessionConfiguredEvent {
|
||||
permission_profile,
|
||||
active_permission_profile: wire.active_permission_profile,
|
||||
cwd: wire.cwd,
|
||||
workspace_roots,
|
||||
reasoning_effort: wire.reasoning_effort,
|
||||
initial_messages: wire.initial_messages,
|
||||
network_proxy: wire.network_proxy,
|
||||
@@ -4477,7 +4670,6 @@ mod tests {
|
||||
#[test]
|
||||
fn legacy_sandbox_policy_semantics_survive_split_bridge() {
|
||||
let cwd = TempDir::new().expect("tempdir");
|
||||
let writable_root = AbsolutePathBuf::resolve_path_against_base("writable", cwd.path());
|
||||
let policies = [
|
||||
SandboxPolicy::DangerFullAccess,
|
||||
SandboxPolicy::ExternalSandbox {
|
||||
@@ -4490,13 +4682,11 @@ mod tests {
|
||||
network_access: false,
|
||||
},
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
},
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![writable_root],
|
||||
network_access: true,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: true,
|
||||
@@ -5174,17 +5364,68 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_context_item_migrates_legacy_workspace_write_writable_roots() -> Result<()> {
|
||||
let cwd = test_path_buf("/home/user/project");
|
||||
let extra_root = test_path_buf("/home/user/extra").abs();
|
||||
let item: TurnContextItem = serde_json::from_value(json!({
|
||||
"cwd": cwd,
|
||||
"approval_policy": "never",
|
||||
"sandbox_policy": {
|
||||
"type": "workspace-write",
|
||||
"writable_roots": [extra_root],
|
||||
"exclude_tmpdir_env_var": true,
|
||||
"exclude_slash_tmp": true
|
||||
},
|
||||
"model": "gpt-5",
|
||||
"summary": "auto",
|
||||
}))?;
|
||||
|
||||
assert_eq!(
|
||||
item.sandbox_policy,
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
}
|
||||
);
|
||||
assert_eq!(item.workspace_roots, vec![cwd.abs(), extra_root]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_context_item_migrates_missing_workspace_roots_to_cwd() -> Result<()> {
|
||||
let cwd = test_path_buf("/home/user/project");
|
||||
let item: TurnContextItem = serde_json::from_value(json!({
|
||||
"cwd": cwd,
|
||||
"approval_policy": "never",
|
||||
"sandbox_policy": { "type": "read-only" },
|
||||
"permission_profile": PermissionProfile::workspace_write(),
|
||||
"model": "gpt-5",
|
||||
"summary": "auto",
|
||||
}))?;
|
||||
|
||||
assert_eq!(item.workspace_roots, vec![cwd.abs()]);
|
||||
assert_eq!(
|
||||
item.permission_profile(),
|
||||
PermissionProfile::workspace_write()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_context_item_serializes_network_when_present() -> Result<()> {
|
||||
let item = TurnContextItem {
|
||||
turn_id: None,
|
||||
trace_id: None,
|
||||
cwd: test_path_buf("/tmp"),
|
||||
workspace_roots: Vec::new(),
|
||||
current_date: None,
|
||||
timezone: None,
|
||||
approval_policy: AskForApproval::Never,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
permission_profile: None,
|
||||
active_permission_profile: None,
|
||||
network: Some(TurnContextNetworkItem {
|
||||
allowed_domains: vec!["api.example.com".to_string()],
|
||||
denied_domains: vec!["blocked.example.com".to_string()],
|
||||
@@ -5257,6 +5498,7 @@ mod tests {
|
||||
permission_profile: permission_profile.clone(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
@@ -5276,6 +5518,7 @@ mod tests {
|
||||
"approvals_reviewer": "user",
|
||||
"permission_profile": permission_profile,
|
||||
"cwd": test_path_buf("/home/user/project"),
|
||||
"workspace_roots": [],
|
||||
"reasoning_effort": "medium",
|
||||
"rollout_path": format!("{}", rollout_file.path().display()),
|
||||
}
|
||||
@@ -5301,6 +5544,41 @@ mod tests {
|
||||
|
||||
let event: SessionConfiguredEvent = serde_json::from_value(value)?;
|
||||
assert_eq!(event.permission_profile, PermissionProfile::read_only());
|
||||
assert_eq!(event.workspace_roots, vec![cwd.abs()]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_legacy_session_configured_event_migrates_workspace_write_writable_roots()
|
||||
-> Result<()> {
|
||||
let cwd = test_path_buf("/home/user/project");
|
||||
let extra_root = test_path_buf("/home/user/extra").abs();
|
||||
let value = json!({
|
||||
"session_id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
|
||||
"model": "codex-mini-latest",
|
||||
"model_provider_id": "openai",
|
||||
"approval_policy": "never",
|
||||
"approvals_reviewer": "user",
|
||||
"sandbox_policy": {
|
||||
"type": "workspace-write",
|
||||
"writable_roots": [extra_root],
|
||||
"exclude_tmpdir_env_var": true,
|
||||
"exclude_slash_tmp": true
|
||||
},
|
||||
"cwd": cwd,
|
||||
});
|
||||
|
||||
let event: SessionConfiguredEvent = serde_json::from_value(value)?;
|
||||
let expected_sandbox = SandboxPolicy::WorkspaceWrite {
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
};
|
||||
assert_eq!(
|
||||
event.permission_profile,
|
||||
PermissionProfile::from_legacy_sandbox_policy_for_cwd(&expected_sandbox, cwd.as_path())
|
||||
);
|
||||
assert_eq!(event.workspace_roots, vec![cwd.abs(), extra_root]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user