Fix remote fork cwd + enforce shared ownership

This commit is contained in:
Charles Cunningham
2026-01-29 11:06:20 -08:00
parent 32dbd27fe5
commit 33a313899d
2 changed files with 75 additions and 0 deletions

View File

@@ -1,3 +1,6 @@
use std::ffi::OsStr;
use std::ffi::OsString;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
@@ -201,6 +204,7 @@ pub async fn download_rollout_if_available(
) -> anyhow::Result<Option<PathBuf>> {
let store = SessionObjectStore::new(base_url).await?;
let key = object_key(session_id);
let meta_key = meta_key(session_id);
let Some(data) = store.get_object_bytes(&key).await? else {
return Ok(None);
};
@@ -214,6 +218,21 @@ pub async fn download_rollout_if_available(
tokio::fs::write(&path, data)
.await
.with_context(|| format!("failed to write rollout file {}", path.display()))?;
let meta_path = share_meta_path_for_rollout_path(&path);
match fetch_meta(&store, &meta_key).await? {
Some(meta) => {
let payload =
serde_json::to_vec(&meta).with_context(|| "failed to serialize metadata")?;
tokio::fs::write(&meta_path, payload)
.await
.with_context(|| {
format!("failed to write share metadata {}", meta_path.display())
})?;
}
None => {
let _ = tokio::fs::remove_file(&meta_path).await;
}
}
Ok(Some(path))
}
@@ -225,6 +244,21 @@ fn meta_key(id: ThreadId) -> String {
format!("{SHARE_OBJECT_PREFIX}/{id}{SHARE_META_SUFFIX}")
}
pub fn local_share_owner(rollout_path: &Path) -> anyhow::Result<Option<String>> {
let meta_path = share_meta_path_for_rollout_path(rollout_path);
let bytes = match std::fs::read(&meta_path) {
Ok(bytes) => bytes,
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(err) => {
return Err(err)
.with_context(|| format!("failed to read share metadata {}", meta_path.display()));
}
};
let meta: SessionShareMeta =
serde_json::from_slice(&bytes).with_context(|| "failed to parse session share metadata")?;
Ok(Some(meta.owner))
}
async fn fetch_meta(
store: &SessionObjectStore,
key: &str,
@@ -264,6 +298,13 @@ fn build_rollout_download_path(codex_home: &Path, session_id: ThreadId) -> anyho
Ok(dir.join(filename))
}
fn share_meta_path_for_rollout_path(path: &Path) -> PathBuf {
let file_name = path.file_name().unwrap_or_else(|| OsStr::new("session"));
let mut name = OsString::from(file_name);
name.push(".share-meta.json");
path.with_file_name(name)
}
impl HttpObjectStore {
fn object_url(&self, key: &str) -> anyhow::Result<Url> {
let mut base = self.base_url.clone();

View File

@@ -30,6 +30,7 @@ use codex_core::path_utils;
use codex_core::protocol::AskForApproval;
use codex_core::read_session_meta_line;
use codex_core::session_share::download_rollout_if_available;
use codex_core::session_share::local_share_owner;
use codex_core::terminal::Multiplexer;
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
use codex_protocol::ThreadId;
@@ -621,6 +622,36 @@ async fn run_ratatui_app(
resume_picker::SessionSelection::StartFresh
};
if let resume_picker::SessionSelection::Resume(path) = &session_selection {
match local_share_owner(path) {
Ok(Some(owner)) => {
let current_owner = auth_manager
.auth_cached()
.and_then(|auth| auth.get_account_email());
if current_owner.as_deref() != Some(owner.as_str()) {
let session_id = read_session_meta_line(path)
.await
.ok()
.map(|meta| meta.meta.id.to_string());
let (id_display, fork_hint) = match session_id {
Some(id) => (id.clone(), format!("Use `codex fork {id}` instead.")),
None => (
"this session".to_string(),
"Use `codex fork` to select it instead.".to_string(),
),
};
return fatal_exit(format!(
"Cannot resume shared session {id_display} owned by {owner}. {fork_hint}"
));
}
}
Ok(None) => {}
Err(err) => {
return fatal_exit(format!("Failed to read shared session metadata: {err}"));
}
}
}
let current_cwd = config.cwd.clone();
let allow_prompt = cli.cwd.is_none();
let action_and_path_if_resume_or_fork = match &session_selection {
@@ -755,6 +786,9 @@ pub(crate) async fn resolve_cwd_for_resume_or_fork(
let Some(history_cwd) = read_session_cwd(path).await else {
return Ok(None);
};
if action == CwdPromptAction::Fork && !history_cwd.exists() {
return Ok(Some(current_cwd.to_path_buf()));
}
if allow_prompt && cwds_differ(current_cwd, &history_cwd) {
let selection =
cwd_prompt::run_cwd_selection_prompt(tui, action, current_cwd, &history_cwd).await?;