fix: separate workspace root from cwd
This commit is contained in:
parent
23a5b53807
commit
7eff9301b9
|
|
@ -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",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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| {
|
||||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
|
|
|||
|
|
@ -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(|_| ())
|
||||
|
|
|
|||
|
|
@ -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| {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user