fix: keep SpawnPod cwd separate
This commit is contained in:
parent
3dd77079f1
commit
248744f9cd
|
|
@ -566,7 +566,7 @@ fn append_orchestrator_agent_routing_guidance(out: &mut String) {
|
|||
|
||||
fn append_coder_agent_routing_guidance(out: &mut String) {
|
||||
out.push_str("\nCoder worktree routing guidance:\n");
|
||||
out.push_str("- Implement only in the provided child worktree/branch. Use `cd <worktree>` before Bash commands and do not edit main-workspace `.yoi`, Ticket, workflow, docs, or memory records; child-worktree `.yoi` project records may be visible when they are part of the branch.\n");
|
||||
out.push_str("- Implement only in the provided child worktree/branch. SpawnPod should set `cwd` to that worktree so Bash/tool defaults already start there; do not treat `cwd` as authority, and do not edit main-workspace `.yoi`, Ticket, workflow, docs, or memory records; child-worktree `.yoi` project records may be visible when they are part of the branch.\n");
|
||||
out.push_str("- Do not create `.yoi/memory`, local/runtime state, logs, locks, caches, sockets, or secret-like files in the child worktree.\n");
|
||||
out.push_str("- Treat the intent packet, binding decisions/invariants, implementation latitude, validation expectations, and report expectations as the contract. Investigate and choose local tactics only within the recorded implementation latitude; escalate to Orchestrator rather than expanding scope when design, permission, history, prompt-context, dependency, or Ticket-boundary questions appear.\n");
|
||||
out.push_str("- Report worktree path, branch, commits/status, changed files, implementation summary, validation run, unresolved notes, and whether the branch is ready for external review. Do not merge, push, close Tickets, or delete worktrees.\n");
|
||||
|
|
|
|||
|
|
@ -493,6 +493,7 @@ where
|
|||
// 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 workspace_root = pod.workspace_root().to_path_buf();
|
||||
let task_feature = pod.task_feature();
|
||||
let session_id_for_usage = pod.segment_id().to_string();
|
||||
let memory_config = pod.manifest().memory.clone();
|
||||
|
|
@ -523,7 +524,9 @@ where
|
|||
|
||||
let mut feature_registry = FeatureRegistryBuilder::new();
|
||||
feature_registry.add_module(task_feature);
|
||||
feature_registry.add_module(crate::feature::builtin::ticket_tools_feature(&pwd));
|
||||
feature_registry.add_module(crate::feature::builtin::ticket_tools_feature(
|
||||
&workspace_root,
|
||||
));
|
||||
let _feature_install_report = pod.install_features(feature_registry);
|
||||
|
||||
let worker = pod.worker_mut();
|
||||
|
|
@ -534,7 +537,7 @@ where
|
|||
// their built-in linter. Companion deny rules on the generic CRUD
|
||||
// scope were already applied during `Pod::from_manifest`.
|
||||
if let Some(mem) = memory_config.as_ref() {
|
||||
let layout = memory::WorkspaceLayout::resolve(mem, &pwd);
|
||||
let layout = memory::WorkspaceLayout::resolve(mem, &workspace_root);
|
||||
let query_cfg = memory::tool::QueryConfig::from(mem);
|
||||
worker.register_tool(memory::tool::read_tool_with_usage(
|
||||
layout.clone(),
|
||||
|
|
@ -554,6 +557,7 @@ where
|
|||
spawner_name.clone(),
|
||||
spawner_socket,
|
||||
runtime_base.clone(),
|
||||
workspace_root.clone(),
|
||||
pwd.clone(),
|
||||
spawned_registry.clone(),
|
||||
self_parent_socket,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ 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.
|
||||
|
|
@ -96,6 +102,17 @@ 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()
|
||||
|
|
@ -350,8 +367,33 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
|
|||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
match Pod::from_manifest_spawned(manifest, store, loader, callback).await {
|
||||
Ok(p) => p,
|
||||
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(),
|
||||
)
|
||||
.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
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("error: failed to create spawned pod: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
|
|
|
|||
|
|
@ -231,8 +231,11 @@ pub struct Pod<C: LlmClient, St: Store> {
|
|||
/// `segment_id` and append tally. `self.segment_id()` is a thin
|
||||
/// wrapper over `segment_state.segment_id()`.
|
||||
segment_state: Arc<SegmentState>,
|
||||
/// Absolute working directory of the Pod.
|
||||
/// Absolute tool/process working directory of the Pod.
|
||||
pwd: PathBuf,
|
||||
/// Absolute runtime workspace root used for project records, workflow,
|
||||
/// memory, Ticket config, Profile context, and spawned-child inheritance.
|
||||
workspace_root: PathBuf,
|
||||
/// Shared, atomically-swappable view of the Pod's resolved scope.
|
||||
/// Cloned out to `ScopedFs` instances (builtin tools, fs_view,
|
||||
/// compact worker) so scope updates propagate to every consumer
|
||||
|
|
@ -417,6 +420,7 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Pod<C, St> {
|
|||
pod_metadata_writer: None,
|
||||
segment_state: self.segment_state.clone(),
|
||||
pwd: self.pwd.clone(),
|
||||
workspace_root: self.workspace_root.clone(),
|
||||
scope: self.scope.clone(),
|
||||
delegation_scope: self.delegation_scope.clone(),
|
||||
hook_builder: HookRegistryBuilder::new(),
|
||||
|
|
@ -597,6 +601,7 @@ 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,
|
||||
scope: SharedScope::new(scope),
|
||||
delegation_scope,
|
||||
|
|
@ -695,11 +700,17 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
&self.manifest
|
||||
}
|
||||
|
||||
/// The Pod's working directory.
|
||||
/// The Pod's tool/process working directory.
|
||||
pub fn pwd(&self) -> &Path {
|
||||
&self.pwd
|
||||
}
|
||||
|
||||
/// The Pod's runtime workspace root. This stays separate from `pwd` for
|
||||
/// spawned children whose SpawnPod `cwd` only changes tool defaults.
|
||||
pub fn workspace_root(&self) -> &Path {
|
||||
&self.workspace_root
|
||||
}
|
||||
|
||||
/// The Pod's directory scope, as a shared atomically-swappable
|
||||
/// handle. Clone it to share scope state with another consumer
|
||||
/// (e.g. a tool that needs to mutate scope dynamically).
|
||||
|
|
@ -1239,7 +1250,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
.map(|d| d.name)
|
||||
.collect()
|
||||
};
|
||||
let agents_md_read = read_agents_md(&self.pwd);
|
||||
let agents_md_read = read_agents_md(&self.workspace_root);
|
||||
for warning in agents_md_read.warnings {
|
||||
if let Some(n) = alerter.as_ref() {
|
||||
n.alert(AlertLevel::Warn, AlertSource::AgentsMd, warning);
|
||||
|
|
@ -2784,7 +2795,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
// `Some(0)` means disabled, same as `None`. Otherwise the
|
||||
// `tokens_since >= 0` comparison would fire on every post-run.
|
||||
let Some(threshold) = memory_cfg.extract_threshold.filter(|n| *n > 0) else {
|
||||
let layout = memory::WorkspaceLayout::resolve(&memory_cfg, &self.pwd);
|
||||
let layout = memory::WorkspaceLayout::resolve(&memory_cfg, &self.workspace_root);
|
||||
let model = memory_cfg
|
||||
.extract_model
|
||||
.as_ref()
|
||||
|
|
@ -2814,7 +2825,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
|
||||
.is_err()
|
||||
{
|
||||
let layout = memory::WorkspaceLayout::resolve(&memory_cfg, &self.pwd);
|
||||
let layout = memory::WorkspaceLayout::resolve(&memory_cfg, &self.workspace_root);
|
||||
let model = memory_cfg
|
||||
.extract_model
|
||||
.as_ref()
|
||||
|
|
@ -2870,7 +2881,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
) -> Result<ExtractDecision, PodError> {
|
||||
use memory::extract;
|
||||
|
||||
let layout = memory::WorkspaceLayout::resolve(memory_cfg, &self.pwd);
|
||||
let layout = memory::WorkspaceLayout::resolve(memory_cfg, &self.workspace_root);
|
||||
let model = memory_cfg
|
||||
.extract_model
|
||||
.as_ref()
|
||||
|
|
@ -3193,7 +3204,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
let files_threshold = memory_cfg.consolidation_threshold_files.filter(|n| *n > 0);
|
||||
let bytes_threshold = memory_cfg.consolidation_threshold_bytes.filter(|n| *n > 0);
|
||||
if files_threshold.is_none() && bytes_threshold.is_none() {
|
||||
let layout = memory::WorkspaceLayout::resolve(&memory_cfg, &self.pwd);
|
||||
let layout = memory::WorkspaceLayout::resolve(&memory_cfg, &self.workspace_root);
|
||||
let model = memory_cfg
|
||||
.consolidation_model
|
||||
.as_ref()
|
||||
|
|
@ -3221,7 +3232,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
|
||||
.is_err()
|
||||
{
|
||||
let layout = memory::WorkspaceLayout::resolve(&memory_cfg, &self.pwd);
|
||||
let layout = memory::WorkspaceLayout::resolve(&memory_cfg, &self.workspace_root);
|
||||
let model = memory_cfg
|
||||
.consolidation_model
|
||||
.as_ref()
|
||||
|
|
@ -3274,7 +3285,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
) -> Result<ConsolidateDecision, PodError> {
|
||||
use memory::consolidate;
|
||||
|
||||
let layout = memory::WorkspaceLayout::resolve(memory_cfg, &self.pwd);
|
||||
let layout = memory::WorkspaceLayout::resolve(memory_cfg, &self.workspace_root);
|
||||
let model = memory_cfg
|
||||
.consolidation_model
|
||||
.as_ref()
|
||||
|
|
@ -3730,6 +3741,7 @@ where
|
|||
pod_metadata_writer,
|
||||
segment_state: SegmentState::new(session_id, segment_id, 0),
|
||||
pwd: common.pwd,
|
||||
workspace_root: common.workspace_root,
|
||||
scope: SharedScope::new(common.scope),
|
||||
delegation_scope: common.delegation_scope,
|
||||
hook_builder: HookRegistryBuilder::new(),
|
||||
|
|
@ -3784,7 +3796,34 @@ where
|
|||
loader: PromptLoader,
|
||||
callback_socket: PathBuf,
|
||||
) -> Result<Self, PodError> {
|
||||
let mut common = prepare_pod_common(&manifest, &loader, /* parse_template */ true)?;
|
||||
let pwd = current_pwd()?;
|
||||
Self::from_manifest_spawned_with_context(
|
||||
manifest,
|
||||
store,
|
||||
loader,
|
||||
callback_socket,
|
||||
pwd.clone(),
|
||||
pwd,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn from_manifest_spawned_with_context(
|
||||
manifest: PodManifest,
|
||||
store: St,
|
||||
loader: PromptLoader,
|
||||
callback_socket: PathBuf,
|
||||
workspace_root: PathBuf,
|
||||
tool_cwd: PathBuf,
|
||||
) -> Result<Self, PodError> {
|
||||
let mut common = prepare_pod_common_with_context(
|
||||
&manifest,
|
||||
&loader,
|
||||
/* parse_template */ true,
|
||||
workspace_root,
|
||||
tool_cwd,
|
||||
manifest.scope.clone(),
|
||||
)?;
|
||||
let skill_shadows = std::mem::take(&mut common.skill_shadows);
|
||||
|
||||
// A spawned child starts its own conversation, so it mints a
|
||||
|
|
@ -3809,6 +3848,7 @@ where
|
|||
pod_metadata_writer,
|
||||
segment_state: SegmentState::new(session_id, segment_id, 0),
|
||||
pwd: common.pwd,
|
||||
workspace_root: common.workspace_root,
|
||||
scope: SharedScope::new(common.scope),
|
||||
delegation_scope: common.delegation_scope,
|
||||
hook_builder: HookRegistryBuilder::new(),
|
||||
|
|
@ -3987,6 +4027,7 @@ where
|
|||
pod_metadata_writer,
|
||||
segment_state: SegmentState::new(session_id, segment_id, state.entries_count),
|
||||
pwd: common.pwd,
|
||||
workspace_root: common.workspace_root,
|
||||
scope: SharedScope::new(common.scope),
|
||||
delegation_scope: common.delegation_scope,
|
||||
hook_builder: HookRegistryBuilder::new(),
|
||||
|
|
@ -4610,11 +4651,13 @@ pub enum PodError {
|
|||
}
|
||||
|
||||
/// Bundle of resources that every high-level Pod constructor needs:
|
||||
/// pwd, 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 fields.
|
||||
/// tool pwd, 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
|
||||
/// fields.
|
||||
struct PodCommon {
|
||||
pwd: PathBuf,
|
||||
workspace_root: PathBuf,
|
||||
scope: Scope,
|
||||
delegation_scope: DelegationScope,
|
||||
client: Box<dyn LlmClient>,
|
||||
|
|
@ -4705,8 +4748,9 @@ fn prepare_pod_common(
|
|||
parse_template: bool,
|
||||
) -> Result<PodCommon, PodError> {
|
||||
let pwd = current_pwd()?;
|
||||
let scope = build_scope_with_memory(manifest, &pwd)?;
|
||||
prepare_pod_common_from_scope(manifest, loader, parse_template, pwd, scope)
|
||||
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(
|
||||
|
|
@ -4716,17 +4760,45 @@ fn prepare_pod_common_with_scope(
|
|||
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, pwd, scope)
|
||||
prepare_pod_common_from_scope(manifest, loader, parse_template, workspace_root, pwd, scope)
|
||||
}
|
||||
|
||||
fn prepare_pod_common_with_context(
|
||||
manifest: &PodManifest,
|
||||
loader: &PromptLoader,
|
||||
parse_template: bool,
|
||||
workspace_root: PathBuf,
|
||||
pwd: PathBuf,
|
||||
scope_config: ScopeConfig,
|
||||
) -> Result<PodCommon, PodError> {
|
||||
let workspace_root =
|
||||
std::fs::canonicalize(&workspace_root).map_err(|source| PodError::InvalidPwd {
|
||||
pwd: workspace_root.clone(),
|
||||
source,
|
||||
})?;
|
||||
let pwd = std::fs::canonicalize(&pwd).map_err(|source| PodError::InvalidPwd {
|
||||
pwd: pwd.clone(),
|
||||
source,
|
||||
})?;
|
||||
let scope = Scope::from_config(&scope_config).map_err(PodError::Scope)?;
|
||||
prepare_pod_common_from_scope(manifest, loader, parse_template, workspace_root, pwd, scope)
|
||||
}
|
||||
|
||||
fn prepare_pod_common_from_scope(
|
||||
manifest: &PodManifest,
|
||||
loader: &PromptLoader,
|
||||
parse_template: bool,
|
||||
workspace_root: PathBuf,
|
||||
pwd: PathBuf,
|
||||
scope: Scope,
|
||||
) -> Result<PodCommon, PodError> {
|
||||
if !scope.is_readable(&workspace_root) {
|
||||
return Err(PodError::PwdOutsideScope {
|
||||
pwd: workspace_root,
|
||||
});
|
||||
}
|
||||
if !scope.is_readable(&pwd) {
|
||||
return Err(PodError::PwdOutsideScope { pwd });
|
||||
}
|
||||
|
|
@ -4738,7 +4810,7 @@ fn prepare_pod_common_from_scope(
|
|||
let memory_layout = manifest
|
||||
.memory
|
||||
.as_ref()
|
||||
.map(|mem| memory::WorkspaceLayout::resolve(mem, &pwd));
|
||||
.map(|mem| memory::WorkspaceLayout::resolve(mem, &workspace_root));
|
||||
let mut workflow_registry = match memory_layout.as_ref() {
|
||||
Some(layout) => workflow_crate::load_workflows(layout).map_err(PodError::WorkflowLoad)?,
|
||||
None => workflow_crate::WorkflowRegistry::empty(),
|
||||
|
|
@ -4756,6 +4828,7 @@ fn prepare_pod_common_from_scope(
|
|||
|
||||
Ok(PodCommon {
|
||||
pwd,
|
||||
workspace_root,
|
||||
scope,
|
||||
delegation_scope,
|
||||
client,
|
||||
|
|
@ -4861,6 +4934,70 @@ fn current_pwd() -> Result<PathBuf, PodError> {
|
|||
.map_err(|source| PodError::InvalidPwd { pwd: cwd, source })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod spawned_context_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
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");
|
||||
std::fs::create_dir_all(&workspace_root).unwrap();
|
||||
std::fs::create_dir_all(&tool_cwd).unwrap();
|
||||
|
||||
let mut manifest = minimal_manifest_for_context_test(&workspace_root, &tool_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(),
|
||||
manifest.scope.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
common.workspace_root,
|
||||
workspace_root.canonicalize().unwrap()
|
||||
);
|
||||
assert_eq!(common.pwd, tool_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 {
|
||||
let toml_str = format!(
|
||||
r#"
|
||||
[pod]
|
||||
name = "spawn-context-test"
|
||||
|
||||
[model]
|
||||
scheme = "anthropic"
|
||||
model_id = "claude-sonnet-4-20250514"
|
||||
|
||||
[worker]
|
||||
|
||||
[[scope.allow]]
|
||||
target = "{}"
|
||||
permission = "read"
|
||||
|
||||
[[scope.allow]]
|
||||
target = "{}"
|
||||
permission = "write"
|
||||
"#,
|
||||
workspace_root.display(),
|
||||
tool_cwd.display()
|
||||
);
|
||||
let mut manifest = PodManifest::from_toml(&toml_str).unwrap();
|
||||
manifest.model.auth = Some(manifest::AuthRef::None);
|
||||
manifest
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod memory_worker_event_tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -224,8 +224,11 @@ pub struct SpawnPodTool {
|
|||
/// Root of the `$XDG_RUNTIME_DIR/yoi/` tree, used to predict
|
||||
/// the spawned Pod's socket path before the child has bound it.
|
||||
runtime_base: PathBuf,
|
||||
/// Directory the spawned Pod should run in when the LLM did not
|
||||
/// override it. Defaults to the spawner's pwd — see module docs.
|
||||
/// Inherited runtime workspace root for Profile/project/Ticket/workflow/
|
||||
/// 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,
|
||||
/// Optional typed runtime command injected by tests. Production resolves
|
||||
/// the runtime command from `std::env::current_exe()` at launch time.
|
||||
|
|
@ -266,6 +269,7 @@ impl SpawnPodTool {
|
|||
spawner_name: String,
|
||||
callback_socket: PathBuf,
|
||||
runtime_base: PathBuf,
|
||||
workspace_root: PathBuf,
|
||||
spawner_pwd: PathBuf,
|
||||
registry: Arc<SpawnedPodRegistry>,
|
||||
parent_socket: Option<PathBuf>,
|
||||
|
|
@ -279,6 +283,7 @@ impl SpawnPodTool {
|
|||
spawner_name,
|
||||
callback_socket,
|
||||
runtime_base,
|
||||
workspace_root,
|
||||
spawner_pwd,
|
||||
runtime_command,
|
||||
registry,
|
||||
|
|
@ -470,7 +475,11 @@ impl SpawnPodTool {
|
|||
.arg(&self.callback_socket)
|
||||
.arg("--spawn-config-json")
|
||||
.arg(spawn_config_json)
|
||||
.current_dir(child_cwd)
|
||||
.arg("--workspace")
|
||||
.arg(&self.workspace_root)
|
||||
.arg("--tool-cwd")
|
||||
.arg(child_cwd)
|
||||
.current_dir(&self.workspace_root)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::from(stderr_file))
|
||||
|
|
@ -602,9 +611,8 @@ fn validate_spawn_cwd(
|
|||
/// `PodManifestConfig`'s `Serialize` impl is the single source of truth for the
|
||||
/// internal handoff shape.
|
||||
///
|
||||
/// The child's working directory is set separately via
|
||||
/// `Command::current_dir` (see [`SpawnPodTool::exec_child`]) — it is
|
||||
/// not part of the manifest.
|
||||
/// The child's tool working directory is carried separately through
|
||||
/// the child runtime entrypoint; it is not part of the manifest.
|
||||
impl SpawnPodTool {
|
||||
fn build_spawn_config_json(
|
||||
&self,
|
||||
|
|
@ -616,7 +624,7 @@ impl SpawnPodTool {
|
|||
build_spawn_config_json_for_profile(
|
||||
&self.spawner_manifest,
|
||||
&self.available_profiles,
|
||||
&self.spawner_pwd,
|
||||
&self.workspace_root,
|
||||
name,
|
||||
instruction_override,
|
||||
scope_allow,
|
||||
|
|
@ -628,7 +636,7 @@ impl SpawnPodTool {
|
|||
fn build_spawn_config_json_for_profile(
|
||||
spawner_manifest: &PodManifest,
|
||||
available_profiles: &AvailableProfiles,
|
||||
spawner_pwd: &Path,
|
||||
workspace_root: &Path,
|
||||
name: &str,
|
||||
instruction_override: Option<&str>,
|
||||
scope_allow: &[ScopeRule],
|
||||
|
|
@ -650,7 +658,7 @@ fn build_spawn_config_json_for_profile(
|
|||
SpawnProfileSelector::Inherit => unreachable!(),
|
||||
};
|
||||
let resolved = ProfileResolver::new()
|
||||
.with_workspace_base(spawner_pwd)
|
||||
.with_workspace_base(workspace_root)
|
||||
.resolve_from_registry(
|
||||
&profile_selector,
|
||||
registry,
|
||||
|
|
@ -867,6 +875,7 @@ pub fn spawn_pod_tool(
|
|||
spawner_name: String,
|
||||
callback_socket: PathBuf,
|
||||
runtime_base: PathBuf,
|
||||
workspace_root: PathBuf,
|
||||
spawner_pwd: PathBuf,
|
||||
registry: Arc<SpawnedPodRegistry>,
|
||||
parent_socket: Option<PathBuf>,
|
||||
|
|
@ -878,6 +887,7 @@ pub fn spawn_pod_tool(
|
|||
spawner_name,
|
||||
callback_socket,
|
||||
runtime_base,
|
||||
workspace_root,
|
||||
spawner_pwd,
|
||||
registry,
|
||||
parent_socket,
|
||||
|
|
@ -893,6 +903,7 @@ pub fn spawn_pod_tool_with_runtime_command(
|
|||
spawner_name: String,
|
||||
callback_socket: PathBuf,
|
||||
runtime_base: PathBuf,
|
||||
workspace_root: PathBuf,
|
||||
spawner_pwd: PathBuf,
|
||||
registry: Arc<SpawnedPodRegistry>,
|
||||
parent_socket: Option<PathBuf>,
|
||||
|
|
@ -905,6 +916,7 @@ pub fn spawn_pod_tool_with_runtime_command(
|
|||
spawner_name,
|
||||
callback_socket,
|
||||
runtime_base,
|
||||
workspace_root,
|
||||
spawner_pwd,
|
||||
registry,
|
||||
parent_socket,
|
||||
|
|
@ -919,6 +931,7 @@ fn spawn_pod_tool_impl(
|
|||
spawner_name: String,
|
||||
callback_socket: PathBuf,
|
||||
runtime_base: PathBuf,
|
||||
workspace_root: PathBuf,
|
||||
spawner_pwd: PathBuf,
|
||||
registry: Arc<SpawnedPodRegistry>,
|
||||
parent_socket: Option<PathBuf>,
|
||||
|
|
@ -930,7 +943,7 @@ fn spawn_pod_tool_impl(
|
|||
Arc::new(move || {
|
||||
let schema = schemars::schema_for!(SpawnPodInput);
|
||||
let schema_value = serde_json::to_value(schema).unwrap_or(serde_json::json!({}));
|
||||
let available_profiles = AvailableProfiles::discover(&spawner_pwd);
|
||||
let available_profiles = AvailableProfiles::discover(&workspace_root);
|
||||
let description = prompts
|
||||
.spawn_pod_tool_description(
|
||||
&available_profiles.compact_list(),
|
||||
|
|
@ -950,6 +963,7 @@ fn spawn_pod_tool_impl(
|
|||
spawner_name.clone(),
|
||||
callback_socket.clone(),
|
||||
runtime_base.clone(),
|
||||
workspace_root.clone(),
|
||||
spawner_pwd.clone(),
|
||||
registry.clone(),
|
||||
parent_socket.clone(),
|
||||
|
|
|
|||
|
|
@ -146,25 +146,26 @@ fn mock_runtime_command() -> PodRuntimeCommand {
|
|||
}
|
||||
|
||||
fn cwd_recording_runtime_command(script_path: &Path, output_path: &Path) -> PodRuntimeCommand {
|
||||
std::fs::write(script_path, "pwd > \"$1\"\n").unwrap();
|
||||
PodRuntimeCommand::new(
|
||||
which_sh(),
|
||||
vec![
|
||||
script_path.as_os_str().to_os_string(),
|
||||
output_path.as_os_str().to_os_string(),
|
||||
],
|
||||
let output = output_path.display();
|
||||
std::fs::write(
|
||||
script_path,
|
||||
format!(
|
||||
"tmp=\"{output}.tmp\"\npwd > \"$tmp\"\nprintf '%s\\n' \"$@\" >> \"$tmp\"\nmv \"$tmp\" \"{output}\"\n"
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
PodRuntimeCommand::new(which_sh(), vec![script_path.as_os_str().to_os_string()])
|
||||
}
|
||||
|
||||
async fn read_recorded_pwd(output_path: &Path) -> String {
|
||||
async fn read_recorded_runtime_invocation(output_path: &Path) -> Vec<String> {
|
||||
for _ in 0..50 {
|
||||
if let Ok(content) = std::fs::read_to_string(output_path) {
|
||||
return content.trim_end().to_string();
|
||||
return content.lines().map(str::to_owned).collect();
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
}
|
||||
panic!(
|
||||
"runtime command did not record pwd at {}",
|
||||
"runtime command did not record invocation at {}",
|
||||
output_path.display()
|
||||
);
|
||||
}
|
||||
|
|
@ -269,7 +270,7 @@ fn clear_env() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn spawn_pod_runs_child_process_in_provided_cwd() {
|
||||
async fn spawn_pod_launches_runtime_in_workspace_and_passes_tool_cwd() {
|
||||
let _env = EnvGuard::acquire();
|
||||
|
||||
let allow_root = TempDir::new().unwrap();
|
||||
|
|
@ -289,6 +290,7 @@ async fn spawn_pod_runs_child_process_in_provided_cwd() {
|
|||
spawner_socket,
|
||||
runtime_base,
|
||||
allow_root.path().to_path_buf(),
|
||||
allow_root.path().to_path_buf(),
|
||||
registry,
|
||||
None,
|
||||
dummy_manifest(allow_root.path()),
|
||||
|
|
@ -312,9 +314,19 @@ async fn spawn_pod_runs_child_process_in_provided_cwd() {
|
|||
|
||||
tool.execute(&input).await.unwrap();
|
||||
assert!(matches!(received.await.unwrap(), Some(Method::Run { .. })));
|
||||
assert_eq!(
|
||||
read_recorded_pwd(&output_path).await,
|
||||
child_cwd.to_str().unwrap()
|
||||
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] == "--workspace" && pair[1] == allow_root.path().to_str().unwrap()),
|
||||
"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:?}"
|
||||
);
|
||||
|
||||
clear_env();
|
||||
|
|
@ -340,6 +352,7 @@ async fn spawn_pod_omitted_cwd_preserves_spawner_pwd() {
|
|||
spawner_socket,
|
||||
runtime_base,
|
||||
allow_root.path().to_path_buf(),
|
||||
allow_root.path().to_path_buf(),
|
||||
registry,
|
||||
None,
|
||||
dummy_manifest(allow_root.path()),
|
||||
|
|
@ -362,9 +375,13 @@ async fn spawn_pod_omitted_cwd_preserves_spawner_pwd() {
|
|||
|
||||
tool.execute(&input).await.unwrap();
|
||||
assert!(matches!(received.await.unwrap(), Some(Method::Run { .. })));
|
||||
assert_eq!(
|
||||
read_recorded_pwd(&output_path).await,
|
||||
allow_root.path().to_str().unwrap()
|
||||
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:?}"
|
||||
);
|
||||
|
||||
clear_env();
|
||||
|
|
@ -388,6 +405,7 @@ async fn spawn_pod_delegates_scope_and_sends_run() {
|
|||
spawner_socket.clone(),
|
||||
runtime_base.clone(),
|
||||
allow_root.path().to_path_buf(),
|
||||
allow_root.path().to_path_buf(),
|
||||
registry,
|
||||
None,
|
||||
dummy_manifest(allow_root.path()),
|
||||
|
|
@ -480,6 +498,7 @@ async fn spawn_pod_requires_explicit_delegation_even_with_direct_scope() {
|
|||
spawner_socket,
|
||||
runtime_base,
|
||||
allow_root.path().to_path_buf(),
|
||||
allow_root.path().to_path_buf(),
|
||||
registry,
|
||||
None,
|
||||
manifest,
|
||||
|
|
@ -546,6 +565,7 @@ async fn spawn_pod_rejects_child_non_recursive_scope_under_parent_non_recursive_
|
|||
spawner_socket,
|
||||
runtime_base,
|
||||
allow_root.path().to_path_buf(),
|
||||
allow_root.path().to_path_buf(),
|
||||
registry,
|
||||
None,
|
||||
manifest,
|
||||
|
|
@ -597,6 +617,7 @@ async fn spawn_pod_rejects_scope_outside_spawner() {
|
|||
spawner_socket,
|
||||
runtime_base,
|
||||
allow_root.path().to_path_buf(),
|
||||
allow_root.path().to_path_buf(),
|
||||
registry,
|
||||
None,
|
||||
dummy_manifest(allow_root.path()),
|
||||
|
|
@ -670,6 +691,7 @@ async fn spawn_pod_rolls_back_reservation_when_socket_never_appears() {
|
|||
spawner_socket,
|
||||
runtime_base,
|
||||
allow_root.path().to_path_buf(),
|
||||
allow_root.path().to_path_buf(),
|
||||
registry,
|
||||
None,
|
||||
dummy_manifest(allow_root.path()),
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user