Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4e199e6
Enterprise session sharing via object storage (/share)
charley-oai Jan 22, 2026
f4c955a
Lint
charley-oai Jan 22, 2026
16cfee1
Fork remotely stored session
charley-oai Jan 22, 2026
45ce36d
Lint and fix
charley-oai Jan 22, 2026
275a6b7
Remove AWS support for now to make CI happy
charley-oai Jan 22, 2026
cd265a3
Fix cargo deny
charley-oai Jan 22, 2026
0f3c89d
Fix /share user input handling after rebase
charley-oai Jan 25, 2026
0c2c47a
Preserve query params when building HTTP object URLs
charley-oai Jan 25, 2026
99f190b
Prefer current cwd if remote downloaded
charley-oai Jan 25, 2026
23855a0
Check metadata even when the rollout blob is missing, and enforce own…
charley-oai Jan 25, 2026
8d542f8
lint
charley-oai Jan 29, 2026
ffa7644
lint
charley-oai Jan 29, 2026
05cae8a
Fix remote fork cwd + enforce shared ownership
charley-oai Jan 29, 2026
ced5d77
Skip cwd prompt for shared forks
charley-oai Jan 29, 2026
9c6cc81
Fix share prompt fields after rebase
charley-oai Feb 15, 2026
726369e
Add command hook for session object store URL
charley-oai Feb 16, 2026
68730de
Reject overwrite when share metadata missing
charley-oai Feb 16, 2026
e2e83dc
Block in-app resume for shared sessions
charley-oai Feb 16, 2026
37a68ee
Use io::Error::other for command failures
charley-oai Feb 16, 2026
4db264b
Stream session share rollouts
charley-oai Feb 17, 2026
21db430
Require rollout file for sharing
charley-oai Feb 17, 2026
27b5e7c
Re-resolve session storage URL commands
charley-oai Feb 17, 2026
12f7ffd
Make share metadata creation atomic
charley-oai Feb 17, 2026
9df7169
Resolve session storage URL async with timeout
charley-oai Feb 17, 2026
4525c78
Remove downloaded rollout on metadata failure
charley-oai Feb 17, 2026
de87a5a
Fix async storage URL resolution return
charley-oai Feb 17, 2026
f022b4d
Fallback to UTC for rollout download timestamps
charley-oai Feb 17, 2026
06862a3
Treat unreadable share metadata as unshared
charley-oai Feb 17, 2026
18a35e1
Simplify storage URL timeout handling
charley-oai Feb 17, 2026
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
176 changes: 105 additions & 71 deletions MODULE.bazel.lock

Large diffs are not rendered by default.

1,037 changes: 722 additions & 315 deletions codex-rs/Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions codex-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ async-channel = "2.3.1"
async-stream = "0.3.6"
async-trait = "0.1.89"
axum = { version = "0.8", default-features = false }
azure_core = "0.21"
azure_identity = "0.21"
base64 = "0.22.1"
bm25 = "2.3.2"
bytes = "1.10.1"
Expand Down
4 changes: 3 additions & 1 deletion codex-rs/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ arc-swap = "1.8.0"
async-channel = { workspace = true }
async-trait = { workspace = true }
askama = { workspace = true }
azure_core = { workspace = true }
azure_identity = { workspace = true }
base64 = { workspace = true }
bm25 = { workspace = true }
chardetng = { workspace = true }
Expand Down Expand Up @@ -102,7 +104,7 @@ tokio = { workspace = true, features = [
"rt-multi-thread",
"signal",
] }
tokio-util = { workspace = true, features = ["rt"] }
tokio-util = { workspace = true, features = ["io", "rt"] }
tokio-tungstenite = { workspace = true }
toml = { workspace = true }
toml_edit = { workspace = true }
Expand Down
11 changes: 11 additions & 0 deletions codex-rs/core/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1685,6 +1685,17 @@
],
"description": "Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`."
},
"session_object_storage_url": {
"description": "Base URL for the object store used by /share (enterprise/self-hosted).",
"type": "string"
},
"session_object_storage_url_cmd": {
"description": "Optional command to produce the base URL for the object store used by /share. The command runs as argv and must print a non-empty URL to stdout.",
"items": {
"type": "string"
},
"type": "array"
},
"shell_environment_policy": {
"allOf": [
{
Expand Down
115 changes: 115 additions & 0 deletions codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ use std::collections::HashMap;
use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;
#[cfg(test)]
use tempfile::tempdir;
#[cfg(not(target_os = "macos"))]
Expand Down Expand Up @@ -109,6 +111,7 @@ pub use codex_git::GhostSnapshotConfig;
/// the context window.
pub(crate) const PROJECT_DOC_MAX_BYTES: usize = 32 * 1024; // 32 KiB
pub(crate) const DEFAULT_AGENT_MAX_THREADS: Option<usize> = Some(6);
const SESSION_OBJECT_STORAGE_URL_CMD_TIMEOUT: Duration = Duration::from_secs(5);

pub const CONFIG_TOML_FILE: &str = "config.toml";

Expand Down Expand Up @@ -346,6 +349,11 @@ pub struct Config {
/// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
pub chatgpt_base_url: String,

/// Optional base URL for storing shared session rollouts in an object store.
pub session_object_storage_url: Option<String>,
Comment thread
charley-oai marked this conversation as resolved.
/// Optional command to produce the base URL for storing shared session rollouts.
pub session_object_storage_url_cmd: Option<Vec<String>>,

/// When set, restricts ChatGPT login to a specific workspace identifier.
pub forced_chatgpt_workspace_id: Option<String>,

Expand Down Expand Up @@ -947,6 +955,12 @@ pub struct ConfigToml {
/// When unset, Codex will bind to an ephemeral port chosen by the OS.
pub mcp_oauth_callback_port: Option<u16>,

/// Base URL for the object store used by /share (enterprise/self-hosted).
pub session_object_storage_url: Option<String>,
/// Optional command to produce the base URL for the object store used by /share.
/// The command runs as argv and must print a non-empty URL to stdout.
pub session_object_storage_url_cmd: Option<Vec<String>>,

/// User-defined provider entries that extend/override the built-in list.
#[serde(default)]
pub model_providers: HashMap<String, ModelProviderInfo>,
Expand Down Expand Up @@ -1508,6 +1522,15 @@ impl Config {
Some(WindowsSandboxModeToml::Unelevated) => WindowsSandboxLevel::RestrictedToken,
None => WindowsSandboxLevel::from_features(&features),
};
let session_object_storage_url =
cfg.session_object_storage_url.as_ref().and_then(|value| {
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
});
let mut sandbox_policy = cfg.derive_sandbox_policy(
sandbox_mode,
config_profile.sandbox_mode,
Expand Down Expand Up @@ -1815,6 +1838,8 @@ impl Config {
.chatgpt_base_url
.or(cfg.chatgpt_base_url)
.unwrap_or("https://chatgpt.com/backend-api/".to_string()),
session_object_storage_url,
session_object_storage_url_cmd: cfg.session_object_storage_url_cmd,
forced_chatgpt_workspace_id,
forced_login_method,
include_apply_patch_tool: include_apply_patch_tool_flag,
Expand Down Expand Up @@ -1881,6 +1906,35 @@ impl Config {
Ok(config)
}

pub async fn resolve_session_object_storage_url(&self) -> std::io::Result<Option<String>> {
if let Some(url) = self.session_object_storage_url.as_ref() {
return Ok(Some(url.clone()));
}

let Some(command) = self.session_object_storage_url_cmd.clone() else {
return Ok(None);
};

let mut handle = tokio::task::spawn_blocking(move || {
Self::try_run_command_for_value(Some(&command), "session_object_storage_url_cmd")
});
match tokio::time::timeout(SESSION_OBJECT_STORAGE_URL_CMD_TIMEOUT, &mut handle).await {
Ok(result) => result.map_err(|err| {
std::io::Error::other(format!("session_object_storage_url_cmd panicked: {err}"))
})?,
Err(_) => {
handle.abort();
Err(std::io::Error::new(
Comment on lines +1921 to +1927
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Terminate timed-out storage URL commands

resolve_session_object_storage_url times out a spawn_blocking task and calls handle.abort(), but Command::output() keeps running in that blocking thread. A hung session_object_storage_url_cmd process is not actually killed, so repeated calls can leak child processes/threads and eventually starve the blocking pool.

Useful? React with 👍 / 👎.

ErrorKind::TimedOut,
format!(
"session_object_storage_url_cmd timed out after {}s",
SESSION_OBJECT_STORAGE_URL_CMD_TIMEOUT.as_secs()
),
))
}
}
}

fn load_instructions(codex_dir: Option<&Path>) -> Option<String> {
let base = codex_dir?;
for candidate in [LOCAL_PROJECT_DOC_FILENAME, DEFAULT_PROJECT_DOC_FILENAME] {
Expand Down Expand Up @@ -1925,6 +1979,59 @@ impl Config {
}
}

fn try_run_command_for_value(
command: Option<&[String]>,
context: &str,
) -> std::io::Result<Option<String>> {
let Some(command) = command else {
return Ok(None);
};

if command.is_empty() {
return Err(std::io::Error::new(
ErrorKind::InvalidInput,
format!("{context} is empty"),
));
}

let mut cmd = Command::new(&command[0]);
if let Some(args) = command.get(1..) {
cmd.args(args);
}

let output = cmd.output().map_err(|err| {
std::io::Error::new(err.kind(), format!("{context} failed to start: {err}"))
})?;
Comment thread
charley-oai marked this conversation as resolved.

if !output.status.success() {
let status = output.status;
let stderr = String::from_utf8_lossy(&output.stderr);
let stderr = stderr.trim();
let message = if stderr.is_empty() {
format!("{context} failed with status {status}")
} else {
format!("{context} failed with status {status}: {stderr}")
};
return Err(std::io::Error::other(message));
}

let stdout = String::from_utf8(output.stdout).map_err(|err| {
std::io::Error::new(
ErrorKind::InvalidData,
format!("{context} output was not UTF-8: {err}"),
)
})?;
let trimmed = stdout.trim();
if trimmed.is_empty() {
Err(std::io::Error::new(
ErrorKind::InvalidData,
format!("{context} returned empty output"),
))
} else {
Ok(Some(trimmed.to_string()))
}
}

pub fn set_windows_sandbox_enabled(&mut self, value: bool) {
self.permissions.windows_sandbox_mode = if value {
Some(WindowsSandboxModeToml::Unelevated)
Expand Down Expand Up @@ -4133,6 +4240,8 @@ model_verbosity = "high"
model_verbosity: None,
personality: Some(Personality::Pragmatic),
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
session_object_storage_url: None,
session_object_storage_url_cmd: None,
base_instructions: None,
developer_instructions: None,
compact_prompt: None,
Expand Down Expand Up @@ -4244,6 +4353,8 @@ model_verbosity = "high"
model_verbosity: None,
personality: Some(Personality::Pragmatic),
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
session_object_storage_url: None,
session_object_storage_url_cmd: None,
base_instructions: None,
developer_instructions: None,
compact_prompt: None,
Expand Down Expand Up @@ -4353,6 +4464,8 @@ model_verbosity = "high"
model_verbosity: None,
personality: Some(Personality::Pragmatic),
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
session_object_storage_url: None,
session_object_storage_url_cmd: None,
base_instructions: None,
developer_instructions: None,
compact_prompt: None,
Expand Down Expand Up @@ -4448,6 +4561,8 @@ model_verbosity = "high"
model_verbosity: Some(Verbosity::High),
personality: Some(Personality::Pragmatic),
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
session_object_storage_url: None,
session_object_storage_url_cmd: None,
base_instructions: None,
developer_instructions: None,
compact_prompt: None,
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ mod proposed_plan_parser;
mod sandbox_tags;
pub mod sandboxing;
mod session_prefix;
pub mod session_share;
mod shell_detect;
mod stream_events_utils;
mod tagged_block_parser;
Expand Down
Loading
Loading