fix: separate workspace root from cwd

This commit is contained in:
Keisuke Hirata 2026-06-12 01:03:33 +09:00
parent 23a5b53807
commit 7eff9301b9
No known key found for this signature in database
14 changed files with 314 additions and 254 deletions

View File

@ -35,10 +35,14 @@ pub struct SpawnConfig {
/// Process-local Ticket role marker supplied only by Ticket role launches.
/// This does not alter prompts, manifests, or Ticket claim records.
pub ticket_role: Option<String>,
/// Explicit runtime workspace root. The child uses it as process cwd and
/// receives it via `--workspace` so startup does not infer workspace
/// identity from the parent process cwd.
/// Explicit runtime workspace root. The child receives it via
/// `--workspace` so startup does not infer workspace identity from the
/// parent process cwd.
pub workspace_root: PathBuf,
/// Optional child process cwd. This is not runtime workspace identity and
/// is not passed as a CLI argument; the child observes it as its ordinary
/// process current directory.
pub cwd: Option<PathBuf>,
/// `Some(id)` のとき `--session <id>` を付与し、当該セッションから
/// resume させる。
pub resume_from: Option<Uuid>,
@ -149,7 +153,7 @@ where
let mut command = Command::new(config.runtime_command.program());
command
.args(config.runtime_command.prefix_args())
.current_dir(&config.workspace_root)
.current_dir(config.cwd.as_ref().unwrap_or(&config.workspace_root))
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::from(stderr_file))
@ -335,6 +339,7 @@ mod tests {
profile: Some("project:companion".to_string()),
ticket_role: None,
workspace_root: PathBuf::from("/work/other-project"),
cwd: None,
resume_from: None,
}
}
@ -372,9 +377,10 @@ mod tests {
}
#[test]
fn runtime_args_pass_ticket_role_marker_when_present() {
fn runtime_args_do_not_include_child_cwd() {
let mut config = base_config();
config.ticket_role = Some("intake".to_string());
config.ticket_role = Some("orchestrator".to_string());
config.cwd = Some(PathBuf::from("/work/main/.worktree/orchestration/yoi"));
assert_eq!(
runtime_args(&config),
@ -386,7 +392,7 @@ mod tests {
"--profile",
"project:companion",
"--ticket-role",
"intake",
"orchestrator",
]
);
}

View File

@ -78,6 +78,7 @@ impl TicketIntakeHandoff {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TicketRoleLaunchContext {
pub workspace_root: PathBuf,
pub cwd: Option<PathBuf>,
pub original_workspace_root: Option<PathBuf>,
pub target_workspace_root: Option<PathBuf>,
pub role: TicketRole,
@ -97,6 +98,7 @@ impl TicketRoleLaunchContext {
pub fn new(workspace_root: impl Into<PathBuf>, role: TicketRole) -> Self {
Self {
workspace_root: workspace_root.into(),
cwd: None,
original_workspace_root: None,
target_workspace_root: None,
role,
@ -113,6 +115,11 @@ impl TicketRoleLaunchContext {
}
}
pub fn with_cwd(mut self, root: impl Into<PathBuf>) -> Self {
self.cwd = Some(root.into());
self
}
pub fn with_original_workspace_root(mut self, root: impl Into<PathBuf>) -> Self {
self.original_workspace_root = Some(root.into());
self
@ -144,6 +151,7 @@ impl TicketRoleLaunchContext {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TicketRoleLaunchPlan {
pub workspace_root: PathBuf,
pub cwd: Option<PathBuf>,
pub original_workspace_root: PathBuf,
pub target_workspace_root: PathBuf,
pub implementation_worktree_root: PathBuf,
@ -175,6 +183,7 @@ impl TicketRoleLaunchPlan {
profile: Some(self.profile.clone()),
ticket_role: Some(self.role.as_str().to_string()),
workspace_root: self.workspace_root.clone(),
cwd: self.cwd.clone(),
resume_from: None,
})
}
@ -285,6 +294,7 @@ pub fn plan_ticket_role_launch_with_config(
Ok(TicketRoleLaunchPlan {
workspace_root: context.workspace_root,
cwd: context.cwd,
original_workspace_root,
target_workspace_root,
implementation_worktree_root,
@ -678,6 +688,9 @@ fn append_workspace_routing_context(out: &mut String, context: &TicketRoleLaunch
"role_workspace_root",
&context.workspace_root.display().to_string(),
);
if let Some(cwd) = &context.cwd {
push_bounded_bullet(out, "role_cwd", &cwd.display().to_string());
}
push_bounded_bullet(
out,
"original_workspace_root",
@ -875,6 +888,7 @@ mod tests {
fn test_launch_plan(workspace: &std::path::Path) -> TicketRoleLaunchPlan {
TicketRoleLaunchPlan {
workspace_root: workspace.to_path_buf(),
cwd: None,
original_workspace_root: workspace.to_path_buf(),
target_workspace_root: workspace.to_path_buf(),
implementation_worktree_root: workspace.join(".worktree"),
@ -1330,6 +1344,7 @@ workflow = "ticket-review-workflow"
.spawn_config(PodRuntimeCommand::for_executable("/bin/yoi"))
.unwrap();
assert_eq!(spawn_config.workspace_root, temp.path());
assert_eq!(spawn_config.cwd, None);
assert!(text.contains("Workspace routing context:"));
assert!(text.contains("role_workspace_root"));

View File

@ -508,7 +508,7 @@ where
// Pod-immutable snapshots taken before the mutable worker borrow
// below so the worker borrow doesn't conflict with reads on `pod`.
let scope_handle = pod.scope().clone();
let pwd = pod.pwd().to_path_buf();
let cwd = pod.cwd().to_path_buf();
let workspace_root = pod.workspace_root().to_path_buf();
let task_feature = pod.task_feature();
let session_id_for_usage = pod.segment_id().to_string();
@ -526,7 +526,7 @@ where
// ScopedFs (builtin tools, fs_view, compact worker) reads from it,
// and any future scope mutation (SpawnPod-style revoke, future
// GrantScope) propagates through it.
let fs = tools::ScopedFs::with_shared_scope(scope_handle.clone(), pwd.clone());
let fs = tools::ScopedFs::with_shared_scope(scope_handle.clone(), cwd.clone());
let tracker = tools::Tracker::new();
// Same ScopedFs also powers the IPC `ListCompletions` query — keep
// a clone for the FS view we attach below, since the tools consume
@ -607,7 +607,7 @@ where
spawner_socket,
runtime_base.clone(),
workspace_root.clone(),
pwd.clone(),
cwd.clone(),
spawned_registry.clone(),
self_parent_socket,
spawner_manifest,
@ -618,7 +618,7 @@ where
worker.register_tool(read_pod_output_tool(spawned_registry.clone()));
worker.register_tool(stop_pod_tool(spawned_registry.clone()));
let discovery =
PodDiscovery::new(pod_store, spawner_name, runtime_base, pwd, spawned_registry);
PodDiscovery::new(pod_store, spawner_name, runtime_base, cwd, spawned_registry);
worker.register_tool(list_pods_tool(discovery.clone()));
worker.register_tool(restore_pod_tool(discovery.clone()));
worker.register_tool(send_to_peer_pod_tool(discovery));
@ -664,7 +664,7 @@ async fn controller_loop<C, St>(
pod.store().clone(),
spawner_name.clone(),
discovery_runtime_base,
pod.pwd().to_path_buf(),
pod.cwd().to_path_buf(),
spawned_registry.clone(),
);
let mut pending: Option<PendingRun> = None;
@ -1292,7 +1292,7 @@ where
.collect();
protocol::Greeting {
pod_name: manifest.pod.name.clone(),
cwd: pod.pwd().display().to_string(),
cwd: pod.cwd().display().to_string(),
provider: provider_name,
model: model_id,
scope_summary: pod.scope_snapshot().summary(),

View File

@ -29,12 +29,6 @@ struct Cli {
#[arg(long, value_name = "PATH")]
workspace: Option<PathBuf>,
/// Internal spawned child process/tool working directory. This is separate
/// from `--workspace`; adopted Pods use `--workspace` for runtime context
/// and this path for tool defaults.
#[arg(long, value_name = "PATH", requires = "adopt", hide = true)]
tool_cwd: Option<PathBuf>,
/// Manifest TOML to use directly as a one-file compatibility/debug input.
/// This bypasses profile discovery but still applies builtin defaults and
/// the same required-field validation boundary.
@ -107,17 +101,6 @@ fn runtime_workspace_root(cli: &Cli) -> Result<PathBuf, String> {
}
}
fn runtime_tool_cwd(cli: &Cli, workspace_root: &Path) -> Result<PathBuf, String> {
let raw = cli.tool_cwd.as_deref().unwrap_or(workspace_root);
let path = if raw.is_absolute() {
raw.to_path_buf()
} else {
workspace_root.join(raw)
};
std::fs::canonicalize(&path)
.map_err(|e| format!("failed to resolve tool cwd {}: {e}", path.display()))
}
fn runtime_pod_name(cli: &Cli, workspace_root: &Path) -> String {
cli.pod
.as_deref()
@ -300,6 +283,13 @@ fn exit_code_from_i32(code: i32) -> ExitCode {
}
async fn run_cli_inner(cli: Cli) -> ExitCode {
let cwd = match std::env::current_dir() {
Ok(path) => path,
Err(e) => {
eprintln!("error: failed to resolve current directory: {e}");
return ExitCode::FAILURE;
}
};
let workspace_root = match runtime_workspace_root(&cli) {
Ok(root) => root,
Err(e) => {
@ -314,15 +304,6 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
return ExitCode::FAILURE;
}
};
if let Err(e) = std::env::set_current_dir(&workspace_root) {
eprintln!(
"error: failed to enter runtime workspace {}: {e}",
workspace_root.display()
);
return ExitCode::FAILURE;
}
// Initialize persistent store. `paths::sessions_dir()` only
// returns None when none of YOI_HOME / YOI_DATA_DIR /
// HOME is set — surface that as a hard error to match the
@ -372,33 +353,17 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
return ExitCode::FAILURE;
}
};
let tool_cwd = match runtime_tool_cwd(&cli, &workspace_root) {
Ok(path) => path,
Err(e) => {
eprintln!("error: {e}");
return ExitCode::FAILURE;
}
};
match Pod::from_manifest_spawned_with_context(
manifest,
store,
loader,
callback,
workspace_root.clone(),
tool_cwd.clone(),
cwd.clone(),
)
.await
{
Ok(p) => {
if let Err(e) = std::env::set_current_dir(&tool_cwd) {
eprintln!(
"error: failed to enter tool cwd {}: {e}",
tool_cwd.display()
);
return ExitCode::FAILURE;
}
p
}
Ok(p) => p,
Err(e) => {
eprintln!("error: failed to create spawned pod: {e}");
return ExitCode::FAILURE;
@ -418,12 +383,14 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
return ExitCode::FAILURE;
}
};
match Pod::restore_from_manifest(
match Pod::restore_from_manifest_with_context(
source_session_id,
source_segment_id,
manifest,
store,
loader,
workspace_root.clone(),
cwd.clone(),
)
.await
{
@ -437,7 +404,16 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
manifest.pod.name = pod_name.to_string();
match store.read_by_name(pod_name) {
Ok(Some(_)) => {
match Pod::restore_from_pod_metadata(pod_name, manifest, store, loader).await {
match Pod::restore_from_pod_metadata_with_context(
pod_name,
manifest,
store,
loader,
workspace_root.clone(),
cwd.clone(),
)
.await
{
Ok(p) => p,
Err(e) => {
eprintln!("error: failed to restore pod {pod_name}: {e}");
@ -449,20 +425,38 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
eprintln!("error: pod state missing for {pod_name}");
return ExitCode::FAILURE;
}
Ok(None) => match Pod::from_manifest(manifest, store, loader).await {
Ok(p) => p,
Err(e) => {
eprintln!("error: failed to create pod {pod_name}: {e}");
return ExitCode::FAILURE;
Ok(None) => {
match Pod::from_manifest_with_context(
manifest,
store,
loader,
workspace_root.clone(),
cwd.clone(),
)
.await
{
Ok(p) => p,
Err(e) => {
eprintln!("error: failed to create pod {pod_name}: {e}");
return ExitCode::FAILURE;
}
}
},
}
Err(e) => {
eprintln!("error: failed to read pod state for {pod_name}: {e}");
return ExitCode::FAILURE;
}
}
} else {
match Pod::from_manifest(manifest, store, loader).await {
match Pod::from_manifest_with_context(
manifest,
store,
loader,
workspace_root.clone(),
cwd.clone(),
)
.await
{
Ok(p) => p,
Err(e) => {
eprintln!("error: failed to create pod: {e}");
@ -478,7 +472,6 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
pod.set_runtime_ticket_role(Some(role));
}
let pod_name = pod.manifest().pod.name.clone();
// Spawn the controller (starts socket server)
let runtime_base = match paths::runtime_dir() {
Some(d) => d,

View File

@ -45,7 +45,7 @@ pub struct PodFsView {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileCandidate {
/// 入力 prefix と整合する形のパスprefix が absolute なら absolute、
/// relative なら pwd 相対)。
/// relative なら cwd 相対)。
pub path: String,
pub is_dir: bool,
}
@ -114,7 +114,7 @@ impl PodFsView {
/// `path` を ScopedFs 経由で解決し、submit 時の `Segment::FileRef`
/// attachment 用 system message を返す。
///
/// - `path` は relative なら pwd 相対、absolute なら absolute として解釈
/// - `path` は relative なら cwd 相対、absolute なら absolute として解釈
/// - 通常ディレクトリは浅い entry listing として `[Dir: <path>]\n<body>` に展開する
/// - ディレクトリ listing は hidden / gitignore を特別扱いせず、scope 上 readable な
/// 直下 entry だけを最大 `DIR_FILE_REF_ENTRY_LIMIT` 件返す
@ -126,7 +126,7 @@ impl PodFsView {
let abs = if p.is_absolute() {
p.to_path_buf()
} else {
self.fs.pwd().join(p)
self.fs.cwd().join(p)
};
// 通常ディレクトリだけを FileRef listing として扱う。symlink を含むパスは
@ -163,16 +163,16 @@ impl PodFsView {
/// `prefix` にマッチするファイル / ディレクトリを scope 内で浅く列挙する。
///
/// - `prefix` が空 or `pwd` 相対のときは pwd 直下を見る
/// - `prefix` が空 or `cwd` 相対のときは cwd 直下を見る
/// - `prefix` が末尾 `/` のときはそのディレクトリ直下を全列挙
/// - 末尾が名前部分のときは、その名前を starts_with でフィルタ
/// - scope 上 readable なエントリのみ返す
/// - ディレクトリ → ファイル の順、各グループ内は名前昇順
/// - 上限 `COMPLETION_LIMIT` 件で打ち切り(深い列挙はしない)
pub fn list_file_completions(&self, prefix: &str) -> Vec<FileCandidate> {
let pwd = self.fs.pwd();
let cwd = self.fs.cwd();
let scope = self.fs.scope();
let (dir, name_prefix, is_absolute) = split_prefix(prefix, pwd);
let (dir, name_prefix, is_absolute) = split_prefix(prefix, cwd);
let read_dir = match std::fs::read_dir(&dir) {
Ok(rd) => rd,
@ -194,7 +194,7 @@ impl PodFsView {
let display = if is_absolute {
path.display().to_string()
} else {
path.strip_prefix(pwd)
path.strip_prefix(cwd)
.map(|p| p.display().to_string())
.unwrap_or_else(|_| path.display().to_string())
};
@ -343,7 +343,7 @@ fn format_range(offset: Option<usize>, limit: Option<usize>) -> String {
}
}
fn split_prefix(prefix: &str, pwd: &Path) -> (PathBuf, String, bool) {
fn split_prefix(prefix: &str, cwd: &Path) -> (PathBuf, String, bool) {
let is_absolute = Path::new(prefix).is_absolute();
let p = Path::new(prefix);
let (parent, name) = if prefix.is_empty() || prefix.ends_with('/') {
@ -359,9 +359,9 @@ fn split_prefix(prefix: &str, pwd: &Path) -> (PathBuf, String, bool) {
let dir = if is_absolute {
parent
} else if parent.as_os_str().is_empty() {
pwd.to_path_buf()
cwd.to_path_buf()
} else {
pwd.join(parent)
cwd.join(parent)
};
(dir, name, is_absolute)
}

View File

@ -232,7 +232,7 @@ pub struct Pod<C: LlmClient, St: Store> {
/// wrapper over `segment_state.segment_id()`.
segment_state: Arc<SegmentState>,
/// Absolute tool/process working directory of the Pod.
pwd: PathBuf,
cwd: PathBuf,
/// Absolute runtime workspace root used for project records, workflow,
/// memory, Ticket config, Profile context, and spawned-child inheritance.
workspace_root: PathBuf,
@ -423,7 +423,7 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Pod<C, St> {
store: self.store.clone(),
pod_metadata_writer: None,
segment_state: self.segment_state.clone(),
pwd: self.pwd.clone(),
cwd: self.cwd.clone(),
workspace_root: self.workspace_root.clone(),
scope: self.scope.clone(),
delegation_scope: self.delegation_scope.clone(),
@ -577,7 +577,7 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Pod<C, St> {
impl<C: LlmClient, St: Store> Pod<C, St> {
/// Create a new Pod from a pre-built Worker and store.
///
/// Callers must pre-resolve `pwd` (absolute) and build a [`Scope`]
/// Callers must pre-resolve `cwd` (absolute) and build a [`Scope`]
/// — typically via [`Scope::from_config`] when coming from a
/// manifest, or [`Scope::writable`] in tests.
///
@ -589,7 +589,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
manifest: PodManifest,
worker: Worker<C>,
store: St,
pwd: PathBuf,
cwd: PathBuf,
scope: Scope,
) -> Result<Self, PodError> {
// Segment creation is deferred to `ensure_segment_head` at first
@ -606,8 +606,8 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
store,
pod_metadata_writer: None,
segment_state: SegmentState::new(session_id, segment_id, 0),
workspace_root: pwd.clone(),
pwd,
workspace_root: cwd.clone(),
cwd,
scope: SharedScope::new(scope),
delegation_scope,
hook_builder: HookRegistryBuilder::new(),
@ -718,11 +718,11 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
}
/// The Pod's tool/process working directory.
pub fn pwd(&self) -> &Path {
&self.pwd
pub fn cwd(&self) -> &Path {
&self.cwd
}
/// The Pod's runtime workspace root. This stays separate from `pwd` for
/// The Pod's runtime workspace root. This stays separate from `cwd` for
/// spawned children whose SpawnPod `cwd` only changes tool defaults.
pub fn workspace_root(&self) -> &Path {
&self.workspace_root
@ -1325,7 +1325,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
let scope_snapshot = self.scope.snapshot();
let ctx = SystemPromptContext {
now: chrono::Utc::now(),
cwd: &self.pwd,
cwd: &self.cwd,
language: worker_language,
scope: &scope_snapshot,
tool_names,
@ -1562,7 +1562,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
fn resolve_file_refs(&self, segments: &[Segment]) -> Vec<SystemItem> {
let view = crate::fs_view::PodFsView::new(tools::ScopedFs::with_shared_scope(
self.scope.clone(),
self.pwd.clone(),
self.cwd.clone(),
));
let mut out = Vec::new();
for seg in segments {
@ -2461,11 +2461,11 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
auto_read_budget,
)));
// Build an independent compact worker. Scope and pwd are shared
// Build an independent compact worker. Scope and cwd are shared
// with the main Pod (reads go through the same policy) but the
// Tracker is fresh — compact-time reads must not pollute the
// main session's recency list, which feeds `default_refs` above.
let scoped_fs = tools::ScopedFs::with_shared_scope(self.scope.clone(), self.pwd.clone());
let scoped_fs = tools::ScopedFs::with_shared_scope(self.scope.clone(), self.cwd.clone());
let summary_tracker = tools::Tracker::new();
let summary_client: Box<dyn LlmClient> = self.build_compactor_client()?;
let summary_system_prompt = self
@ -3708,7 +3708,7 @@ where
/// process's `std::env::current_dir()` — callers that want a
/// different cwd must `cd` before constructing the Pod (e.g. the
/// `SpawnPod` tool sets `Command::current_dir` on the child). The
/// captured pwd is canonicalised and validated against
/// captured cwd is canonicalised and validated against
/// `manifest.scope`.
///
/// `loader` is installed into the system-prompt template
@ -3720,7 +3720,25 @@ where
store: St,
loader: PromptLoader,
) -> Result<Self, PodError> {
let mut common = prepare_pod_common(&manifest, &loader, /* parse_template */ true)?;
let cwd = current_cwd()?;
Self::from_manifest_with_context(manifest, store, loader, cwd.clone(), cwd).await
}
pub async fn from_manifest_with_context(
manifest: PodManifest,
store: St,
loader: PromptLoader,
workspace_root: PathBuf,
cwd: PathBuf,
) -> Result<Self, PodError> {
let mut common = prepare_pod_common_with_context(
&manifest,
&loader,
/* parse_template */ true,
workspace_root,
cwd,
manifest.scope.clone(),
)?;
let skill_shadows = std::mem::take(&mut common.skill_shadows);
// Segment creation is deferred to the first run (see
@ -3757,7 +3775,7 @@ where
store,
pod_metadata_writer,
segment_state: SegmentState::new(session_id, segment_id, 0),
pwd: common.pwd,
cwd: common.cwd,
workspace_root: common.workspace_root,
scope: SharedScope::new(common.scope),
delegation_scope: common.delegation_scope,
@ -3814,14 +3832,14 @@ where
loader: PromptLoader,
callback_socket: PathBuf,
) -> Result<Self, PodError> {
let pwd = current_pwd()?;
let cwd = current_cwd()?;
Self::from_manifest_spawned_with_context(
manifest,
store,
loader,
callback_socket,
pwd.clone(),
pwd,
cwd.clone(),
cwd,
)
.await
}
@ -3832,14 +3850,14 @@ where
loader: PromptLoader,
callback_socket: PathBuf,
workspace_root: PathBuf,
tool_cwd: PathBuf,
cwd: PathBuf,
) -> Result<Self, PodError> {
let mut common = prepare_pod_common_with_context(
&manifest,
&loader,
/* parse_template */ true,
workspace_root,
tool_cwd,
cwd,
manifest.scope.clone(),
)?;
let skill_shadows = std::mem::take(&mut common.skill_shadows);
@ -3865,7 +3883,7 @@ where
store,
pod_metadata_writer,
segment_state: SegmentState::new(session_id, segment_id, 0),
pwd: common.pwd,
cwd: common.cwd,
workspace_root: common.workspace_root,
scope: SharedScope::new(common.scope),
delegation_scope: common.delegation_scope,
@ -3917,6 +3935,26 @@ where
manifest: PodManifest,
store: St,
loader: PromptLoader,
) -> Result<Self, PodError> {
let cwd = current_cwd()?;
Self::restore_from_pod_metadata_with_context(
pod_name,
manifest,
store,
loader,
cwd.clone(),
cwd,
)
.await
}
pub async fn restore_from_pod_metadata_with_context(
pod_name: &str,
manifest: PodManifest,
store: St,
loader: PromptLoader,
workspace_root: PathBuf,
cwd: PathBuf,
) -> Result<Self, PodError> {
let metadata =
store
@ -3936,15 +3974,36 @@ where
session_id: active.session_id,
})?;
let manifest = match metadata.resolved_manifest_snapshot {
Some(snapshot) => serde_json::from_value(snapshot).map_err(|source| {
PodError::PodMetadataManifestSnapshot {
pod_name: pod_name.to_string(),
source,
Some(snapshot) => {
let mut restored: PodManifest =
serde_json::from_value(snapshot).map_err(|source| {
PodError::PodMetadataManifestSnapshot {
pod_name: pod_name.to_string(),
source,
}
})?;
if !manifest.scope.allow.is_empty() || !manifest.scope.deny.is_empty() {
restored.scope = manifest.scope;
}
})?,
if !manifest.delegation_scope.allow.is_empty()
|| !manifest.delegation_scope.deny.is_empty()
{
restored.delegation_scope = manifest.delegation_scope;
}
restored
}
None => manifest,
};
Self::restore_from_manifest(active.session_id, segment_id, manifest, store, loader).await
Self::restore_from_manifest_with_context(
active.session_id,
segment_id,
manifest,
store,
loader,
workspace_root,
cwd,
)
.await
}
/// Restore a Pod from an existing session log.
@ -3970,6 +4029,28 @@ where
manifest: PodManifest,
store: St,
loader: PromptLoader,
) -> Result<Self, PodError> {
let cwd = current_cwd()?;
Self::restore_from_manifest_with_context(
session_id,
segment_id,
manifest,
store,
loader,
cwd.clone(),
cwd,
)
.await
}
pub async fn restore_from_manifest_with_context(
session_id: SessionId,
segment_id: SegmentId,
manifest: PodManifest,
store: St,
loader: PromptLoader,
workspace_root: PathBuf,
cwd: PathBuf,
) -> Result<Self, PodError> {
// Read raw entries once so we can both reconstruct state and
// seed the broadcast sink's mirror with the same prefix that
@ -3982,10 +4063,12 @@ where
let mirror_entries: Vec<LogEntry> = raw_entries.clone();
let scope_config = effective_restore_scope_config(&store, &manifest)?;
let mut common = prepare_pod_common_with_scope(
let mut common = prepare_pod_common_with_context(
&manifest,
&loader,
/* parse_template */ false,
workspace_root,
cwd,
scope_config,
)?;
let skill_shadows = std::mem::take(&mut common.skill_shadows);
@ -4045,7 +4128,7 @@ where
store,
pod_metadata_writer,
segment_state: SegmentState::new(session_id, segment_id, state.entries_count),
pwd: common.pwd,
cwd: common.cwd,
workspace_root: common.workspace_root,
scope: SharedScope::new(common.scope),
delegation_scope: common.delegation_scope,
@ -4584,12 +4667,12 @@ pub enum PodError {
#[error(transparent)]
Scope(ScopeError),
#[error("pwd is not readable under the configured scope: {}", .pwd.display())]
PwdOutsideScope { pwd: PathBuf },
#[error("cwd is not readable under the configured scope: {}", .cwd.display())]
CwdOutsideScope { cwd: PathBuf },
#[error("failed to resolve pwd {}: {source}", .pwd.display())]
InvalidPwd {
pwd: PathBuf,
#[error("failed to resolve cwd {}: {source}", .cwd.display())]
InvalidCwd {
cwd: PathBuf,
#[source]
source: std::io::Error,
},
@ -4671,12 +4754,12 @@ pub enum PodError {
}
/// Bundle of resources that every high-level Pod constructor needs:
/// tool pwd, runtime workspace root, scope, an LLM client, the prompt catalog,
/// cwd, runtime workspace root, scope, an LLM client, the prompt catalog,
/// and (optionally) a parsed system-prompt template. Built once by
/// [`prepare_pod_common`] from the resolved manifest and then split into Pod
/// [`prepare_pod_common_with_context`] from the resolved manifest and then split into Pod
/// fields.
struct PodCommon {
pwd: PathBuf,
cwd: PathBuf,
workspace_root: PathBuf,
scope: Scope,
delegation_scope: DelegationScope,
@ -4752,58 +4835,42 @@ fn delegated_write_rule_to_deny(rule: PodSpawnedScopeRule) -> Option<ScopeRule>
(rule.permission == Permission::Write).then_some(rule)
}
/// Resolve pwd / scope / LLM client / prompt catalog from a validated
/// manifest. Used by `from_manifest`, `from_manifest_spawned`,
/// and `restore_from_manifest` so they share one definition of "what
/// pieces fall out of a manifest".
/// Build the runtime pieces that are derivable directly from the resolved
/// manifest. Used by new, spawned, and restored Pods so they share one
/// definition of "what pieces fall out of a manifest".
///
/// `parse_template` controls whether the manifest's instruction is
/// parsed as a system-prompt template. New Pods always parse so the
/// template is rendered at first turn; restored Pods skip parsing
/// because the saved session log replays a previously-rendered
/// `system_prompt` verbatim.
fn prepare_pod_common(
manifest: &PodManifest,
loader: &PromptLoader,
parse_template: bool,
) -> Result<PodCommon, PodError> {
let pwd = current_pwd()?;
let workspace_root = pwd.clone();
let scope = build_scope_with_memory(manifest, &workspace_root)?;
prepare_pod_common_from_scope(manifest, loader, parse_template, workspace_root, pwd, scope)
}
fn prepare_pod_common_with_scope(
manifest: &PodManifest,
loader: &PromptLoader,
parse_template: bool,
scope_config: ScopeConfig,
) -> Result<PodCommon, PodError> {
let pwd = current_pwd()?;
let workspace_root = pwd.clone();
let scope = Scope::from_config(&scope_config).map_err(PodError::Scope)?;
prepare_pod_common_from_scope(manifest, loader, parse_template, workspace_root, pwd, scope)
}
/// `parse_template` controls whether the manifest's instruction is parsed as a
/// system-prompt template. New Pods always parse so the template is rendered at
/// first turn; restored Pods skip parsing because the saved session log replays
/// a previously-rendered `system_prompt` verbatim.
fn prepare_pod_common_with_context(
manifest: &PodManifest,
loader: &PromptLoader,
parse_template: bool,
workspace_root: PathBuf,
pwd: PathBuf,
cwd: PathBuf,
scope_config: ScopeConfig,
) -> Result<PodCommon, PodError> {
let workspace_root =
std::fs::canonicalize(&workspace_root).map_err(|source| PodError::InvalidPwd {
pwd: workspace_root.clone(),
std::fs::canonicalize(&workspace_root).map_err(|source| PodError::InvalidCwd {
cwd: workspace_root.clone(),
source,
})?;
let pwd = std::fs::canonicalize(&pwd).map_err(|source| PodError::InvalidPwd {
pwd: pwd.clone(),
let cwd = std::fs::canonicalize(&cwd).map_err(|source| PodError::InvalidCwd {
cwd: cwd.clone(),
source,
})?;
let mut scope_config = scope_config;
if let Some(mem) = manifest.memory.as_ref() {
let layout = memory::WorkspaceLayout::resolve(mem, &workspace_root);
scope_config.deny.extend(memory::deny_write_rules(&layout));
scope_config
.deny
.extend(workflow_crate::deny_write_rules(&layout));
}
scope_config.allow.extend(skill_dir_read_rules(manifest));
let scope = Scope::from_config(&scope_config).map_err(PodError::Scope)?;
prepare_pod_common_from_scope(manifest, loader, parse_template, workspace_root, pwd, scope)
prepare_pod_common_from_scope(manifest, loader, parse_template, workspace_root, cwd, scope)
}
fn prepare_pod_common_from_scope(
@ -4811,16 +4878,16 @@ fn prepare_pod_common_from_scope(
loader: &PromptLoader,
parse_template: bool,
workspace_root: PathBuf,
pwd: PathBuf,
cwd: PathBuf,
scope: Scope,
) -> Result<PodCommon, PodError> {
if !scope.is_readable(&workspace_root) {
return Err(PodError::PwdOutsideScope {
pwd: workspace_root,
return Err(PodError::CwdOutsideScope {
cwd: workspace_root,
});
}
if !scope.is_readable(&pwd) {
return Err(PodError::PwdOutsideScope { pwd });
if !scope.is_readable(&cwd) {
return Err(PodError::CwdOutsideScope { cwd });
}
let delegation_scope =
DelegationScope::from_config(&manifest.delegation_scope).map_err(PodError::Scope)?;
@ -4847,7 +4914,7 @@ fn prepare_pod_common_from_scope(
};
Ok(PodCommon {
pwd,
cwd,
workspace_root,
scope,
delegation_scope,
@ -4900,28 +4967,6 @@ where
}
}
/// Build the Pod's runtime [`Scope`] from the manifest, layering the
/// memory subsystem's deny-write rules on top when `[memory]` is
/// present, and read-allow rules for any external Agent Skills
/// directories ingested. The deny rules cap generic CRUD tools so they
/// cannot touch `<workspace>/memory/` or `<workspace>/knowledge/` while
/// the memory tools (registered separately) bypass `ScopedFs` and write
/// through `std::fs` directly. Skill directories are added at
/// `Permission::Read` so the agent can `Read` `scripts/` / `references/`
/// / `assets/` referenced by the Workflow body.
fn build_scope_with_memory(manifest: &PodManifest, pwd: &Path) -> Result<Scope, PodError> {
let mut scope_config = manifest.scope.clone();
if let Some(mem) = manifest.memory.as_ref() {
let layout = memory::WorkspaceLayout::resolve(mem, pwd);
scope_config.deny.extend(memory::deny_write_rules(&layout));
scope_config
.deny
.extend(workflow_crate::deny_write_rules(&layout));
}
scope_config.allow.extend(skill_dir_read_rules(manifest));
Scope::from_config(&scope_config).map_err(PodError::Scope)
}
/// Allow-rules granting `Read` access to every skill directory the Pod
/// will ingest from the manifest's `[skills] directories`. Returned
/// rules are recursive so the entire skill bundle (`SKILL.md` +
@ -4941,17 +4986,17 @@ fn skill_dir_read_rules(manifest: &PodManifest) -> Vec<ScopeRule> {
.collect()
}
/// Snapshot the process's current working directory as the Pod's pwd,
/// Snapshot the process's current working directory as the Pod's cwd,
/// canonicalising symlinks and any `.`/`..` components. The Pod keeps
/// this value for its lifetime; changes to the process-wide cwd after
/// construction do not affect scope checks or the system prompt.
fn current_pwd() -> Result<PathBuf, PodError> {
let cwd = std::env::current_dir().map_err(|source| PodError::InvalidPwd {
pwd: PathBuf::from("."),
fn current_cwd() -> Result<PathBuf, PodError> {
let cwd = std::env::current_dir().map_err(|source| PodError::InvalidCwd {
cwd: PathBuf::from("."),
source,
})?;
cwd.canonicalize()
.map_err(|source| PodError::InvalidPwd { pwd: cwd, source })
.map_err(|source| PodError::InvalidCwd { cwd: cwd, source })
}
#[cfg(test)]
@ -4962,18 +5007,18 @@ mod spawned_context_tests {
fn spawn_pod_context_keeps_workspace_root_separate_from_tool_pwd() {
let tmp = tempfile::tempdir().unwrap();
let workspace_root = tmp.path().join("workspace-root");
let tool_cwd = tmp.path().join("child-worktree");
let cwd = tmp.path().join("child-worktree");
std::fs::create_dir_all(&workspace_root).unwrap();
std::fs::create_dir_all(&tool_cwd).unwrap();
std::fs::create_dir_all(&cwd).unwrap();
let mut manifest = minimal_manifest_for_context_test(&workspace_root, &tool_cwd);
let mut manifest = minimal_manifest_for_context_test(&workspace_root, &cwd);
manifest.memory = Some(manifest::MemoryConfig::default());
let common = prepare_pod_common_with_context(
&manifest,
&PromptLoader::builtins_only(),
false,
workspace_root.clone(),
tool_cwd.clone(),
cwd.clone(),
manifest.scope.clone(),
)
.unwrap();
@ -4982,14 +5027,14 @@ mod spawned_context_tests {
common.workspace_root,
workspace_root.canonicalize().unwrap()
);
assert_eq!(common.pwd, tool_cwd.canonicalize().unwrap());
assert_eq!(common.cwd, cwd.canonicalize().unwrap());
assert_eq!(
common.memory_layout.as_ref().unwrap().root(),
workspace_root.canonicalize().unwrap()
);
}
fn minimal_manifest_for_context_test(workspace_root: &Path, tool_cwd: &Path) -> PodManifest {
fn minimal_manifest_for_context_test(workspace_root: &Path, cwd: &Path) -> PodManifest {
let toml_str = format!(
r#"
[pod]
@ -5010,7 +5055,7 @@ target = "{}"
permission = "write"
"#,
workspace_root.display(),
tool_cwd.display()
cwd.display()
);
let mut manifest = PodManifest::from_toml(&toml_str).unwrap();
manifest.model.auth = Some(manifest::AuthRef::None);
@ -5226,10 +5271,10 @@ mod build_summary_prompt_tests {
let dir = tempfile::tempdir().unwrap();
let manifest = minimal_manifest_with_skills(vec![]);
let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap();
let pwd = dir.path().join("workspace");
std::fs::create_dir_all(&pwd).unwrap();
let scope = Scope::writable(&pwd).unwrap();
let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, pwd, scope)
let cwd = dir.path().join("workspace");
std::fs::create_dir_all(&cwd).unwrap();
let scope = Scope::writable(&cwd).unwrap();
let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, cwd, scope)
.await
.unwrap();
pod.ensure_segment_head().unwrap();
@ -5371,10 +5416,10 @@ mod build_summary_prompt_tests {
let dir = tempfile::tempdir().unwrap();
let manifest = minimal_manifest_with_skills(vec![]);
let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap();
let pwd = dir.path().join("workspace");
std::fs::create_dir_all(&pwd).unwrap();
let scope = Scope::writable(&pwd).unwrap();
let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, pwd, scope)
let cwd = dir.path().join("workspace");
std::fs::create_dir_all(&cwd).unwrap();
let scope = Scope::writable(&cwd).unwrap();
let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, cwd, scope)
.await
.unwrap();
@ -5474,24 +5519,24 @@ mod build_summary_prompt_tests {
) -> String {
let dir = tempfile::tempdir().unwrap();
let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap();
let pwd = dir.path().join("workspace");
std::fs::create_dir_all(&pwd).unwrap();
let cwd = dir.path().join("workspace");
std::fs::create_dir_all(&cwd).unwrap();
if let Some(doc) = summary_doc {
std::fs::create_dir_all(pwd.join(".yoi/memory")).unwrap();
std::fs::write(pwd.join(".yoi/memory/summary.md"), doc).unwrap();
std::fs::create_dir_all(cwd.join(".yoi/memory")).unwrap();
std::fs::write(cwd.join(".yoi/memory/summary.md"), doc).unwrap();
}
if include_knowledge {
std::fs::create_dir_all(pwd.join(".yoi/knowledge")).unwrap();
std::fs::create_dir_all(cwd.join(".yoi/knowledge")).unwrap();
std::fs::write(
pwd.join(".yoi/knowledge/resident-policy.md"),
cwd.join(".yoi/knowledge/resident-policy.md"),
knowledge_doc("knowledge resident desc"),
)
.unwrap();
}
if include_workflow {
std::fs::create_dir_all(pwd.join(".yoi/workflow")).unwrap();
std::fs::create_dir_all(cwd.join(".yoi/workflow")).unwrap();
std::fs::write(
pwd.join(".yoi/workflow/resident-flow.md"),
cwd.join(".yoi/workflow/resident-flow.md"),
workflow_doc("workflow resident desc"),
)
.unwrap();
@ -5499,15 +5544,15 @@ mod build_summary_prompt_tests {
let mut manifest = minimal_manifest_with_skills(vec![]);
manifest.memory = memory_config;
let scope = Scope::writable(&pwd).unwrap();
let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, pwd.clone(), scope)
let scope = Scope::writable(&cwd).unwrap();
let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, cwd.clone(), scope)
.await
.unwrap();
pod.memory_layout = pod
.manifest
.memory
.as_ref()
.map(|mem| memory::WorkspaceLayout::resolve(mem, &pwd));
.map(|mem| memory::WorkspaceLayout::resolve(mem, &cwd));
if let Some(layout) = pod.memory_layout.as_ref() {
pod.workflow_registry = workflow_crate::load_workflows(layout).unwrap();
}

View File

@ -228,8 +228,8 @@ pub struct SpawnPodTool {
/// memory context. SpawnPod `cwd` must not affect this value.
workspace_root: PathBuf,
/// Directory the spawned Pod's tools should use when the LLM did not
/// override it. Defaults to the spawner's tool pwd.
spawner_pwd: PathBuf,
/// override it. Defaults to the spawner's cwd.
spawner_cwd: PathBuf,
/// Optional typed runtime command injected by tests. Production resolves
/// the runtime command from `std::env::current_exe()` at launch time.
runtime_command: Option<PodRuntimeCommand>,
@ -270,7 +270,7 @@ impl SpawnPodTool {
callback_socket: PathBuf,
runtime_base: PathBuf,
workspace_root: PathBuf,
spawner_pwd: PathBuf,
spawner_cwd: PathBuf,
registry: Arc<SpawnedPodRegistry>,
parent_socket: Option<PathBuf>,
spawner_manifest: PodManifest,
@ -284,7 +284,7 @@ impl SpawnPodTool {
callback_socket,
runtime_base,
workspace_root,
spawner_pwd,
spawner_cwd,
runtime_command,
registry,
parent_socket,
@ -318,7 +318,7 @@ impl Tool for SpawnPodTool {
let scope_allow = parse_scope(&input.scope)?;
self.validate_delegation_scope(&scope_allow)?;
let child_cwd = validate_spawn_cwd(input.cwd.as_deref(), &scope_allow, &self.spawner_pwd)?;
let child_cwd = validate_spawn_cwd(input.cwd.as_deref(), &scope_allow, &self.spawner_cwd)?;
let spawn_selector =
parse_spawn_profile_selector(input.profile.as_deref()).map_err(|msg| {
@ -481,9 +481,7 @@ impl SpawnPodTool {
.arg(spawn_config_json)
.arg("--workspace")
.arg(&self.workspace_root)
.arg("--tool-cwd")
.arg(child_cwd)
.current_dir(&self.workspace_root)
.current_dir(child_cwd)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::from(stderr_file))
@ -881,7 +879,7 @@ pub fn spawn_pod_tool(
callback_socket: PathBuf,
runtime_base: PathBuf,
workspace_root: PathBuf,
spawner_pwd: PathBuf,
spawner_cwd: PathBuf,
registry: Arc<SpawnedPodRegistry>,
parent_socket: Option<PathBuf>,
spawner_manifest: PodManifest,
@ -893,7 +891,7 @@ pub fn spawn_pod_tool(
callback_socket,
runtime_base,
workspace_root,
spawner_pwd,
spawner_cwd,
registry,
parent_socket,
spawner_manifest,
@ -909,7 +907,7 @@ pub fn spawn_pod_tool_with_runtime_command(
callback_socket: PathBuf,
runtime_base: PathBuf,
workspace_root: PathBuf,
spawner_pwd: PathBuf,
spawner_cwd: PathBuf,
registry: Arc<SpawnedPodRegistry>,
parent_socket: Option<PathBuf>,
spawner_manifest: PodManifest,
@ -922,7 +920,7 @@ pub fn spawn_pod_tool_with_runtime_command(
callback_socket,
runtime_base,
workspace_root,
spawner_pwd,
spawner_cwd,
registry,
parent_socket,
spawner_manifest,
@ -937,7 +935,7 @@ fn spawn_pod_tool_impl(
callback_socket: PathBuf,
runtime_base: PathBuf,
workspace_root: PathBuf,
spawner_pwd: PathBuf,
spawner_cwd: PathBuf,
registry: Arc<SpawnedPodRegistry>,
parent_socket: Option<PathBuf>,
spawner_manifest: PodManifest,
@ -969,7 +967,7 @@ fn spawn_pod_tool_impl(
callback_socket.clone(),
runtime_base.clone(),
workspace_root.clone(),
spawner_pwd.clone(),
spawner_cwd.clone(),
registry.clone(),
parent_socket.clone(),
spawner_manifest.clone(),

View File

@ -270,7 +270,7 @@ fn clear_env() {
}
#[tokio::test]
async fn spawn_pod_launches_runtime_in_workspace_and_passes_tool_cwd() {
async fn spawn_pod_launches_runtime_in_workspace_and_process_cwd() {
let _env = EnvGuard::acquire();
let allow_root = TempDir::new().unwrap();
@ -315,7 +315,7 @@ async fn spawn_pod_launches_runtime_in_workspace_and_passes_tool_cwd() {
tool.execute(&input, Default::default()).await.unwrap();
assert!(matches!(received.await.unwrap(), Some(Method::Run { .. })));
let invocation = read_recorded_runtime_invocation(&output_path).await;
assert_eq!(invocation[0], allow_root.path().to_str().unwrap());
assert_eq!(invocation[0], child_cwd.to_str().unwrap());
assert!(
invocation
.windows(2)
@ -323,17 +323,15 @@ async fn spawn_pod_launches_runtime_in_workspace_and_passes_tool_cwd() {
"invocation should carry inherited workspace root: {invocation:?}"
);
assert!(
invocation
.windows(2)
.any(|pair| pair[0] == "--tool-cwd" && pair[1] == child_cwd.to_str().unwrap()),
"invocation should carry tool cwd separately: {invocation:?}"
!invocation.iter().any(|arg| arg == "--tool-cwd"),
"cwd should be process current directory, not a runtime argument: {invocation:?}"
);
clear_env();
}
#[tokio::test]
async fn spawn_pod_omitted_cwd_preserves_spawner_pwd() {
async fn spawn_pod_omitted_cwd_preserves_spawner_cwd() {
let _env = EnvGuard::acquire();
let allow_root = TempDir::new().unwrap();
@ -378,10 +376,8 @@ async fn spawn_pod_omitted_cwd_preserves_spawner_pwd() {
let invocation = read_recorded_runtime_invocation(&output_path).await;
assert_eq!(invocation[0], allow_root.path().to_str().unwrap());
assert!(
invocation
.windows(2)
.any(|pair| pair[0] == "--tool-cwd" && pair[1] == allow_root.path().to_str().unwrap()),
"omitted cwd should preserve spawner pwd as tool cwd: {invocation:?}"
!invocation.iter().any(|arg| arg == "--tool-cwd"),
"omitted cwd should preserve spawner cwd as process cwd: {invocation:?}"
);
clear_env();

View File

@ -76,7 +76,7 @@ pub(crate) struct BashParams {
pub(crate) struct BashTool {
/// Workspace root that every invocation starts in. Snapshot of
/// `ScopedFs::pwd()` at registration time; never mutated, since we
/// `ScopedFs::cwd()` at registration time; never mutated, since we
/// don't track `cd` across calls.
cwd: PathBuf,
/// Directory to spill long outputs into. Caller is expected to have
@ -329,7 +329,7 @@ fn shell_single_quote(s: &str) -> String {
///
/// `output_dir` is where long outputs spill to; the caller is responsible
/// for arranging that the path is in the agent's readable scope. Every
/// invocation starts at `fs.pwd()` — the tool is intentionally stateless
/// invocation starts at `fs.cwd()` — the tool is intentionally stateless
/// w.r.t. the working directory.
pub fn bash_tool(fs: ScopedFs, output_dir: PathBuf) -> ToolDefinition {
Arc::new(move || {
@ -339,7 +339,7 @@ pub fn bash_tool(fs: ScopedFs, output_dir: PathBuf) -> ToolDefinition {
.description(DESCRIPTION)
.input_schema(schema_value);
let tool: Arc<dyn Tool> = Arc::new(BashTool {
cwd: fs.pwd().to_path_buf(),
cwd: fs.cwd().to_path_buf(),
output_dir: output_dir.clone(),
spilled_outputs: std::sync::Mutex::new(Vec::new()),
});

View File

@ -52,7 +52,7 @@ impl Tool for GlobTool {
let base = params
.path
.clone()
.unwrap_or_else(|| self.fs.pwd().to_path_buf());
.unwrap_or_else(|| self.fs.cwd().to_path_buf());
let pattern = params.pattern.clone();
let scope = self.fs.scope().clone();

View File

@ -96,7 +96,7 @@ impl Tool for GrepTool {
"Grep"
);
let default_base = self.fs.pwd().to_path_buf();
let default_base = self.fs.cwd().to_path_buf();
let scope = self.fs.scope().clone();
let report = tokio::task::spawn_blocking(move || run_grep(default_base, params, &scope))
.await

View File

@ -2,7 +2,7 @@
//!
//! `ScopedFs` is the write/read gate layered on top of a [`manifest::Scope`]
//! and a Pod's working directory. The scope decides which paths are
//! readable and writable; the pwd is carried alongside for convenience
//! readable and writable; the cwd is carried alongside for convenience
//! (Glob/Grep default their search base to it).
//!
//! `ScopedFs` is cheap to clone (`Arc` inside) and carries no per-session
@ -20,7 +20,7 @@ use crate::error::ToolsError;
#[derive(Debug)]
struct ScopedFsInner {
scope: SharedScope,
pwd: PathBuf,
cwd: PathBuf,
}
/// Scope-aware filesystem handle. Clone-cheap (`Arc` inside).
@ -60,20 +60,20 @@ pub struct SymlinkInfo {
}
impl ScopedFs {
/// Create a new [`ScopedFs`] wrapping `scope` and `pwd` in a fresh
/// Create a new [`ScopedFs`] wrapping `scope` and `cwd` in a fresh
/// [`SharedScope`]. Use [`ScopedFs::with_shared_scope`] when you
/// need the resulting `ScopedFs` to share scope state with another
/// holder of the `SharedScope` (typically the Pod).
pub fn new(scope: Scope, pwd: PathBuf) -> Self {
Self::with_shared_scope(SharedScope::new(scope), pwd)
pub fn new(scope: Scope, cwd: PathBuf) -> Self {
Self::with_shared_scope(SharedScope::new(scope), cwd)
}
/// Build a [`ScopedFs`] over an existing [`SharedScope`]. The
/// resulting handle and any future updates the caller pushes to
/// `scope` are observed by every clone of this `ScopedFs`.
pub fn with_shared_scope(scope: SharedScope, pwd: PathBuf) -> Self {
pub fn with_shared_scope(scope: SharedScope, cwd: PathBuf) -> Self {
Self {
inner: Arc::new(ScopedFsInner { scope, pwd }),
inner: Arc::new(ScopedFsInner { scope, cwd }),
}
}
@ -93,8 +93,8 @@ impl ScopedFs {
/// The Pod's working directory. Glob/Grep default their search base
/// to this path when callers omit an explicit `path` parameter.
pub fn pwd(&self) -> &Path {
&self.inner.pwd
pub fn cwd(&self) -> &Path {
&self.inner.cwd
}
// =========================================================================

View File

@ -1693,9 +1693,10 @@ fn build_orchestrator_launch_context(
pod_name: &str,
) -> TicketRoleLaunchContext {
let mut context = TicketRoleLaunchContext::new(
orchestration_workspace_root.to_path_buf(),
original_workspace_root.to_path_buf(),
TicketRole::Orchestrator,
)
.with_cwd(orchestration_workspace_root.to_path_buf())
.with_original_workspace_root(original_workspace_root.to_path_buf())
.with_target_workspace_root(original_workspace_root.to_path_buf());
context.pod_name = Some(pod_name.to_string());
@ -1996,6 +1997,7 @@ async fn orchestrator_lifecycle(
match prepare_orchestration_worktree_for_restore(workspace_root) {
Ok(worktree) => {
match restore_orchestrator_pod(
workspace_root,
&worktree.layout.path,
&pod_name,
runtime_command.clone(),
@ -2083,6 +2085,7 @@ async fn restore_workspace_companion_pod(
profile: None,
ticket_role: None,
workspace_root: workspace_root.to_path_buf(),
cwd: None,
resume_from: None,
};
spawn_pod(config, |_| {}).await.map(|_| ())
@ -2099,12 +2102,14 @@ async fn spawn_workspace_companion_pod(
profile: None,
ticket_role: None,
workspace_root: workspace_root.to_path_buf(),
cwd: None,
resume_from: None,
};
spawn_pod(config, |_| {}).await.map(|_| ())
}
async fn restore_orchestrator_pod(
original_workspace_root: &Path,
workspace_root: &Path,
pod_name: &str,
runtime_command: PodRuntimeCommand,
@ -2113,8 +2118,9 @@ async fn restore_orchestrator_pod(
runtime_command,
pod_name: pod_name.to_string(),
profile: None,
ticket_role: None,
workspace_root: workspace_root.to_path_buf(),
ticket_role: Some("orchestrator".to_string()),
workspace_root: original_workspace_root.to_path_buf(),
cwd: Some(workspace_root.to_path_buf()),
resume_from: None,
};
spawn_pod(config, |_| {}).await.map(|_| ())

View File

@ -380,6 +380,7 @@ async fn wait_for_ready(
profile: form.selected_profile_selector(),
ticket_role: None,
workspace_root: form.cwd.clone(),
cwd: None,
resume_from: form.resume_from,
};
let ready = spawn_pod(config, |line| {