Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/codex-runtime/tests/runtime_binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ async fn hello_on_live_socket() {
match parsed.result {
Some(RunnerOpResult::Hello { protocol }) => {
assert_eq!(protocol, WIRE_PROTOCOL);
assert_eq!(protocol, 5);
assert_eq!(protocol, 6);
}
other => panic!("unexpected hello {other:?}"),
}
Expand Down
32 changes: 32 additions & 0 deletions crates/domain/src/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};

use crate::approval::ApprovalsMode;
use crate::error::ErrorCode;
use crate::files::{DEFAULT_FIND_LIMIT, DEFAULT_READ_LIMIT};

/// Advertised PTY size. Must match the isolated PTY adapter default.
pub const PTY_INITIAL_ROWS: u16 = 24;
Expand Down Expand Up @@ -112,13 +113,24 @@ pub struct FileOperationInfo {
pub available: bool,
}

/// Advertised file-window contract. Always present; availability is
/// still `files.read.available` / `files.find.available`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct FileCapabilityInfo {
pub read_range: bool,
pub find_pagination: bool,
pub read_max_bytes: u32,
pub find_max_paths: u32,
}

/// Effective file-tool eligibility. Nested so later capability fields
/// can be additive without renaming `available`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct FileExecutionInfo {
pub read: FileOperationInfo,
pub find: FileOperationInfo,
pub patch: FileOperationInfo,
pub capabilities: FileCapabilityInfo,
}

/// Static eligibility of a managed process in this workspace.
Expand Down Expand Up @@ -212,6 +224,12 @@ impl WorkspaceExecutionInfo {
patch: FileOperationInfo {
available: permissions.write && environment.file_write_supported,
},
capabilities: FileCapabilityInfo {
read_range: true,
find_pagination: true,
read_max_bytes: DEFAULT_READ_LIMIT,
find_max_paths: DEFAULT_FIND_LIMIT,
},
};
let process_available = permissions.exec && environment.exec_supported;
let process = ProcessExecutionInfo {
Expand Down Expand Up @@ -318,10 +336,24 @@ mod tests {
assert_eq!(exec.files.read.available, read);
assert_eq!(exec.files.find.available, find);
assert_eq!(exec.files.patch.available, patch);
assert!(exec.files.capabilities.read_range);
assert!(exec.files.capabilities.find_pagination);
assert_eq!(exec.files.capabilities.read_max_bytes, DEFAULT_READ_LIMIT);
assert_eq!(exec.files.capabilities.find_max_paths, DEFAULT_FIND_LIMIT);
let json = serde_json::to_value(exec).unwrap();
assert_eq!(json["files"]["read"]["available"], read);
assert_eq!(json["files"]["find"]["available"], find);
assert_eq!(json["files"]["patch"]["available"], patch);
assert_eq!(json["files"]["capabilities"]["read_range"], true);
assert_eq!(json["files"]["capabilities"]["find_pagination"], true);
assert_eq!(
json["files"]["capabilities"]["read_max_bytes"].as_u64(),
Some(u64::from(DEFAULT_READ_LIMIT))
);
assert_eq!(
json["files"]["capabilities"]["find_max_paths"].as_u64(),
Some(u64::from(DEFAULT_FIND_LIMIT))
);
}

#[test]
Expand Down
121 changes: 121 additions & 0 deletions crates/domain/src/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,25 @@ use serde::{Deserialize, Serialize};
use crate::ids::{WorkId, WorkspaceId};
use crate::work::CoordinationHint;

/// Per-call byte cap for `read`. Omitted `limit` uses this value.
pub const DEFAULT_READ_LIMIT: u32 = 1024 * 1024;
/// Per-call path cap for `find`. Omitted `limit` uses this value.
pub const DEFAULT_FIND_LIMIT: u32 = 10_000;

fn is_zero_u64(value: &u64) -> bool {
*value == 0
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ReadParams {
pub workspace_id: WorkspaceId,
pub path: String,
/// Byte offset into the file. Omitted is 0.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub offset: Option<u64>,
/// Byte window size. Omitted is [`DEFAULT_READ_LIMIT`]. Max is the same.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub work_id: Option<WorkId>,
}
Expand All @@ -18,6 +33,17 @@ pub struct ReadResult {
pub content: String,
pub version: String,
pub truncated: bool,
/// Start of this window in file bytes. Omitted from JSON when 0.
#[serde(default, skip_serializing_if = "is_zero_u64")]
pub offset: u64,
/// File bytes included in this window. Not `content.len()` after UTF-8 lossy.
pub byte_count: u64,
/// True when this window is not valid UTF-8 and `content` used replacement decoding.
#[serde(default)]
pub content_lossy: bool,
/// Present only when more observed file bytes remain after this window.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_offset: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub coordination: Option<CoordinationHint>,
}
Expand All @@ -27,6 +53,12 @@ pub struct FindParams {
pub workspace_id: WorkspaceId,
#[serde(default)]
pub glob: Option<String>,
/// Index into the sorted matching path list. Omitted is 0.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub offset: Option<u64>,
/// Path window size. Omitted is [`DEFAULT_FIND_LIMIT`]. Max is the same.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub work_id: Option<WorkId>,
}
Expand All @@ -35,6 +67,95 @@ pub struct FindParams {
pub struct FindResult {
pub paths: Vec<String>,
pub truncated: bool,
/// Start of this window in the sorted path list. Omitted from JSON when 0.
#[serde(default, skip_serializing_if = "is_zero_u64")]
pub offset: u64,
/// Walk hit an internal bound; the matching set is not known to be complete.
#[serde(default)]
pub incomplete: bool,
/// Identity of this observation's sorted matching set, not the returned page.
#[serde(default)]
pub listing_version: String,
/// Present only when more observed matching paths remain after this page.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_offset: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub coordination: Option<CoordinationHint>,
}

#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;

#[test]
fn omitted_read_params_default_to_first_window() {
let params: ReadParams = serde_json::from_value(json!({
"workspace_id": "demo",
"path": "a.txt"
}))
.unwrap();
assert!(params.offset.is_none());
assert!(params.limit.is_none());
}

#[test]
fn zero_offset_is_omitted_from_read_json() {
let result = ReadResult {
path: "a.txt".into(),
content: "hi".into(),
version: "sha256:x".into(),
truncated: false,
offset: 0,
byte_count: 2,
content_lossy: false,
next_offset: None,
coordination: None,
};
let json = serde_json::to_value(&result).unwrap();
assert!(json.get("offset").is_none());
assert!(json.get("next_offset").is_none());
assert_eq!(json["byte_count"], 2);
assert_eq!(json["content_lossy"], false);
}

#[test]
fn omitted_result_fields_default_on_deserialize() {
let read: ReadResult = serde_json::from_value(json!({
"path": "a.txt",
"content": "hi",
"version": "sha256:x",
"truncated": false,
"byte_count": 2
}))
.unwrap();
assert!(!read.content_lossy);
assert!(read.next_offset.is_none());
let find: FindResult = serde_json::from_value(json!({
"paths": ["a.txt"],
"truncated": false
}))
.unwrap();
assert!(!find.incomplete);
assert!(find.listing_version.is_empty());
assert!(find.next_offset.is_none());
}

#[test]
fn zero_offset_is_omitted_from_find_json() {
let result = FindResult {
paths: vec!["a.txt".into()],
truncated: false,
offset: 0,
incomplete: false,
listing_version: "sha256:x".into(),
next_offset: None,
coordination: None,
};
let json = serde_json::to_value(&result).unwrap();
assert!(json.get("offset").is_none());
assert!(json.get("next_offset").is_none());
assert_eq!(json["incomplete"], false);
assert_eq!(json["listing_version"], "sha256:x");
}
}
13 changes: 8 additions & 5 deletions crates/domain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ pub use error::{
};
pub use execution::{
ClientEnvironmentKind, CommandSandboxState, EffectivePermissionInfo, EnvironmentExecutionInfo,
FileExecutionInfo, FileOperationInfo, IsolationInfo, NetworkEnforcementState, NetworkInfo,
NetworkPolicyState, ProcessCapabilityInfo, ProcessDisconnectAction, ProcessExecutionInfo,
ProcessLifetimeInfo, ProcessLifetimeOwner, ProcessRestartRecovery, PtyCapabilityInfo,
WorkspaceExecutionInfo, WorkspaceSerializationInfo, PTY_INITIAL_COLS, PTY_INITIAL_ROWS,
FileCapabilityInfo, FileExecutionInfo, FileOperationInfo, IsolationInfo,
NetworkEnforcementState, NetworkInfo, NetworkPolicyState, ProcessCapabilityInfo,
ProcessDisconnectAction, ProcessExecutionInfo, ProcessLifetimeInfo, ProcessLifetimeOwner,
ProcessRestartRecovery, PtyCapabilityInfo, WorkspaceExecutionInfo, WorkspaceSerializationInfo,
PTY_INITIAL_COLS, PTY_INITIAL_ROWS,
};
pub use files::{
FindParams, FindResult, ReadParams, ReadResult, DEFAULT_FIND_LIMIT, DEFAULT_READ_LIMIT,
};
pub use files::{FindParams, FindResult, ReadParams, ReadResult};
pub use ids::{ApprovalId, IntentId, OperationId, OperationKey, ProcessId, WorkId, WorkspaceId};
pub use info::{workspace_info, WorkspaceInfo, WorkspaceInfoParams};
pub use intent::{DeliveryPolicy, IntentKind, IntentState, UserIntent};
Expand Down
18 changes: 15 additions & 3 deletions crates/runner/src/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,32 @@ use crate::process::InProcessRunner;
use crate::PathSandbox;

impl InProcessRunner {
pub async fn read_file(&self, ws: &Workspace, path: &str) -> Result<ReadResult, ErrorBody> {
pub async fn read_file(
&self,
ws: &Workspace,
path: &str,
offset: Option<u64>,
limit: Option<u32>,
) -> Result<ReadResult, ErrorBody> {
ws.require_file_read()?;
self.touch_watch(ws);
PathSandbox::new(ws.clone()).read_file(path).await
PathSandbox::new(ws.clone())
.read_file_window(path, offset, limit)
.await
}

pub async fn find_files(
&self,
ws: &Workspace,
glob: Option<&str>,
offset: Option<u64>,
limit: Option<u32>,
) -> Result<FindResult, ErrorBody> {
ws.require_file_read()?;
self.touch_watch(ws);
PathSandbox::new(ws.clone()).find(glob).await
PathSandbox::new(ws.clone())
.find_window(glob, offset, limit)
.await
}

pub async fn file_version(&self, ws: &Workspace, path: &str) -> Result<String, ErrorBody> {
Expand Down
Loading
Loading