use roder_api::forks::{
ForkOwnership, ForkPolicy, ForkReason, ForkRequest, ForkStatus, RemoveForkPolicy,
};
use roder_api::thread::ThreadMetadata;
use super::*;
impl Runtime {
pub(super) async fn validate_agent_worktree_parent(
&self,
parent: Option<&ThreadMetadata>,
) -> anyhow::Result<()> {
anyhow::ensure!(
self.thread_store.is_some(),
"worktree isolation requires a durable thread store"
);
anyhow::ensure!(
parent.is_none_or(|metadata| metadata.runner_binding.is_none()
&& metadata.runner_destination.is_none())
&& self.config.read().await.remote_runner_destination.is_none(),
"worktree isolation requires a local workspace; remote runner inheritance cannot be silently changed"
);
self.fork_provider(crate::forks::DEFAULT_FORK_PROVIDER)?;
Ok(())
}
pub(super) async fn isolate_agent_workspace(
&self,
parent_thread_id: &ThreadId,
mut thread: ThreadMetadata,
) -> anyhow::Result<ThreadMetadata> {
let fork = self
.create_workspace_fork(
crate::forks::DEFAULT_FORK_PROVIDER,
ForkRequest {
source_workspace: std::path::PathBuf::from(&thread.workspace),
ownership: Some(ForkOwnership {
creator_thread_id: Some(parent_thread_id.clone()),
owner_thread_id: Some(thread.thread_id.clone()),
}),
name: Some(format!("agent-{}", thread.thread_id)),
reason: ForkReason::SubagentLane,
policy: ForkPolicy::default(),
provider_config: serde_json::json!({}),
},
)
.await?;
thread.workspace = fork.workspace.display().to_string();
thread.workspace_id = None;
thread.root_id = None;
thread.runner_binding = None;
thread.runner_destination = None;
thread.runner_state = None;
let note = format!(
"\n\nThis agent uses an isolated, Roder-owned Git worktree at {}. \
Execute coding tools in this workspace, not the parent's checkout. \
It starts at committed HEAD; uncommitted parent changes were not copied. \
Changes are not automatically merged or deleted when the agent exits.",
thread.workspace
);
thread
.developer_instructions
.get_or_insert_default()
.push_str(¬e);
thread.workspace_fork = Some(fork.clone());
let store = self
.thread_store
.as_ref()
.ok_or_else(|| anyhow::anyhow!("worktree isolation requires a durable thread store"))?;
match store.update_thread_metadata(thread).await {
Ok(thread) => Ok(thread),
Err(error) => {
let _ = self
.remove_workspace_fork(
&fork.provider_id,
&fork.id,
RemoveForkPolicy {
confirm_workspace: fork.workspace.clone(),
},
)
.await;
Err(error)
}
}
}
pub(super) async fn discard_failed_agent_workspace(&self, thread_id: &ThreadId) {
let Ok(Some(mut metadata)) = self.load_thread_metadata(thread_id).await else {
return;
};
let Some(fork) = metadata.workspace_fork.as_mut() else {
return;
};
if fork
.ownership
.as_ref()
.and_then(|owner| owner.owner_thread_id.as_ref())
!= Some(thread_id)
{
return;
}
if self
.remove_workspace_fork(
&fork.provider_id,
&fork.id,
RemoveForkPolicy {
confirm_workspace: fork.workspace.clone(),
},
)
.await
.is_ok()
{
fork.status = ForkStatus::Removed;
if let Some(store) = &self.thread_store {
let _ = store.update_thread_metadata(metadata).await;
}
}
}
}